使用Redis实现分布式限流是一种常见且有效的方法,可以防止系统过载并确保公平的资源分配。Redis的高性能和丰富的数据结构使其成为实现分布式限流的理想选择。常见的限流算法包括固定窗口计数、滑动窗口计数和令牌桶算法。
1. 固定窗口计数算法
固定窗口计数算法将时间划分为固定长度的窗口,并在每个窗口内计数请求的数量。
示例代码
以下示例展示了如何使用Redis实现固定窗口计数算法的分布式限流:
java
import redis.clients.jedis.Jedis;
public class FixedWindowRateLimiter {
private Jedis jedis;
private int maxRequests;
private int windowSize; // 窗口大小,单位为秒
public FixedWindowRateLimiter(String host, int port, int maxRequests, int windowSize) {
this.jedis = new Jedis(host, port);
this.maxRequests = maxRequests;
this.windowSize = windowSize;
}
public boolean isAllowed(String clientId) {
String key = "rate_limiter:" + clientId;
long currentWindow = System.currentTimeMillis() / 1000 / windowSize;
String windowKey = key + ":" + currentWindow;
long requestCount = jedis.incr(windowKey);
if (requestCount == 1) {
jedis.expire(windowKey, windowSize);
}
return requestCount <= maxRequests;
}
public void close() {
jedis.close();
}
public static void main(String[] args) {
FixedWindowRateLimiter rateLimiter = new FixedWindowRateLimiter("localhost", 6379, 5, 60);
for (int i = 0; i < 10; i++) {
boolean allowed = rateLimiter.isAllowed("client1");
System.out.println("Request " + (i + 1) + " allowed: " + allowed);
try {
Thread.sleep(500); // 模拟请求间隔
} catch (InterruptedException e) {
e.printStackTrace();
}
}
rateLimiter.close();
}
}
2. 滑动窗口计数算法
滑动窗口计数算法通过记录多个小窗口内的请求数,计算滑动窗口内的总请求数。
示例代码
以下示例展示了如何使用Redis实现滑动窗口计数算法的分布式限流:
java
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
import java.util.List;
public class SlidingWindowRateLimiter {
private Jedis jedis;
private int maxRequests;
private int windowSize; // 窗口大小,单位为秒
private int interval; // 时间间隔,单位为秒
public SlidingWindowRateLimiter(String host, int port, int maxRequests, int windowSize, int interval) {
this.jedis = new Jedis(host, port);
this.maxRequests = maxRequests;
this.windowSize = windowSize;
this.interval = interval;
}
public boolean isAllowed(String clientId) {
String key = "rate_limiter:" + clientId;
long currentTime = System.currentTimeMillis() / 1000;
long windowStart = currentTime - windowSize;
Transaction transaction = jedis.multi();
transaction.zadd(key, currentTime, String.valueOf(currentTime));
transaction.zremrangeByScore(key, 0, windowStart);
transaction.zcard(key);
transaction.expire(key, windowSize + interval);
List<Object> results = transaction.exec();
long requestCount = (long) results.get(2);
return requestCount <= maxRequests;
}
public void close() {
jedis.close();
}
public static void main(String[] args) {
SlidingWindowRateLimiter rateLimiter = new SlidingWindowRateLimiter("localhost", 6379, 5, 60, 1);
for (int i = 0; i < 10; i++) {
boolean allowed = rateLimiter.isAllowed("client1");
System.out.println("Request " + (i + 1) + " allowed: " + allowed);
try {
Thread.sleep(500); // 模拟请求间隔
} catch (InterruptedException e) {
e.printStackTrace();
}
}
rateLimiter.close();
}
}
3. 令牌桶算法
令牌桶算法通过生成令牌来控制请求的速率。每次请求需要消耗一个令牌,如果桶中没有令牌,则请求被拒绝。
示例代码
以下示例展示了如何使用Redis实现令牌桶算法的分布式限流:
java
import redis.clients.jedis.Jedis;
public class TokenBucketRateLimiter {
private Jedis jedis;
private int maxTokens;
private int refillRate; // 令牌生成速率,单位为令牌/秒
public TokenBucketRateLimiter(String host, int port, int maxTokens, int refillRate) {
this.jedis = new Jedis(host, port);
this.maxTokens = maxTokens;
this.refillRate = refillRate;
}
public boolean isAllowed(String clientId) {
String key = "rate_limiter:" + clientId;
long currentTime = System.currentTimeMillis() / 1000;
long lastRefillTime = jedis.hget(key, "lastRefillTime") == null ?
0 : Long.parseLong(jedis.hget(key, "lastRefillTime"));
int tokens = jedis.hget(key, "tokens") == null ?
maxTokens : Integer.parseInt(jedis.hget(key, "tokens"));
long tokensToAdd = (currentTime - lastRefillTime) * refillRate;
tokens = Math.min(maxTokens, tokens + (int) tokensToAdd);
lastRefillTime = currentTime;
if (tokens > 0) {
jedis.hset(key, "tokens", String.valueOf(tokens - 1));
jedis.hset(key, "lastRefillTime", String.valueOf(lastRefillTime));
return true;
} else {
jedis.hset(key, "tokens", String.valueOf(tokens));
jedis.hset(key, "lastRefillTime", String.valueOf(lastRefillTime));
return false;
}
}
public void close() {
jedis.close();
}
public static void main(String[] args) {
TokenBucketRateLimiter rateLimiter = new TokenBucketRateLimiter("localhost", 6379, 5, 1);
for (int i = 0; i < 10; i++) {
boolean allowed = rateLimiter.isAllowed("client1");
System.out.println("Request " + (i + 1) + " allowed: " + allowed);
try {
Thread.sleep(500); // 模拟请求间隔
} catch (InterruptedException e) {
e.printStackTrace();
}
}
rateLimiter.close();
}
}
4. 令牌桶算法与Lua脚本
为了确保限流操作的原子性,可以使用Redis的Lua脚本。以下示例展示了如何结合Lua脚本和令牌桶算法来实现分布式限流。
Lua脚本
保存为token_bucket.lua:
lua
local key = KEYS[1]
local maxTokens = tonumber(ARGV[1])
local refillRate = tonumber(ARGV[2])
local currentTime = tonumber(ARGV[3])
local tokens = tonumber(redis.call("hget", key, "tokens") or maxTokens)
local lastRefillTime = tonumber(redis.call("hget", key, "lastRefillTime") or 0)
local tokensToAdd = math.floor((currentTime - lastRefillTime) * refillRate)
tokens = math.min(maxTokens, tokens + tokensToAdd)
lastRefillTime = currentTime
if tokens > 0 then
redis.call("hset", key, "tokens", tokens - 1)
redis.call("hset", key, "lastRefillTime", lastRefillTime)
return 1
else
redis.call("hset", key, "tokens", tokens)
redis.call("hset", key, "lastRefillTime", lastRefillTime)
return 0
end
Java代码
java
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
public class TokenBucketRateLimiterWithLua {
private JedisPool jedisPool;
private String luaScript;
private String scriptSha;
public TokenBucketRateLimiterWithLua(String host, int port, String scriptPath) throws IOException {
this.jedisPool = new JedisPool(host, port);
this.luaScript = new String(Files.readAllBytes(Paths.get(scriptPath)));
try (Jedis jedis = jedisPool.getResource()) {
this.scriptSha = jedis.scriptLoad(luaScript);
}
}
public boolean isAllowed(String clientId, int maxTokens, int refillRate) {
String key = "rate_limiter:" + clientId;
long currentTime = System.currentTimeMillis() / 1000;