用key的过期时间替代固定窗口的时间戳
lua
-- KEYS[1]: 限流的key
-- ARGV[1]: 限流窗口大小(秒)
-- ARGV[2]: 限流阈值
local key = KEYS[1]
local window = tonumber(ARGV[1])
local limit = tonumber(ARGV[2])
-- 尝试获取当前计数
local current = redis.call("GET", key)
if current == false then
-- key不存在,初始化计数器并设置过期时间
redis.call("SET", key, 1, "EX", window)
return 1
else
-- key存在,检查是否超过限制
if tonumber(current) < limit then
redis.call("INCR", key)
return 1
else
return 0
end
end
java客户端
java
public class FixedWindowRateLimiterWithTTL {
private Jedis jedis;
private String key;
private int window; // 窗口大小(秒)
private int limit; // 限流阈值
private static final String LUA_SCRIPT =
"local key = KEYS[1]\n" +
"local window = tonumber(ARGV[1])\n" +
"local limit = tonumber(ARGV[2])\n" +
"local current = redis.call(\"GET\", key)\n" +
"if current == false then\n" +
" redis.call(\"SET\", key, 1, \"EX\", window)\n" +
" return 1\n" +
"else\n" +
" if tonumber(current) < limit then\n" +
" redis.call(\"INCR\", key)\n" +
" return 1\n" +
" else\n" +
" return 0\n" +
" end\n" +
"end";
public FixedWindowRateLimiterWithTTL(Jedis jedis, String key, int window, int limit) {
this.jedis = jedis;
this.key = key;
this.window = window;
this.limit = limit;
}
public boolean allowRequest() {
Object result = jedis.eval(LUA_SCRIPT,
Collections.singletonList(key),
Arrays.asList(
String.valueOf(window),
String.valueOf(limit)
));
return "1".equals(result.toString());
}
}
使用
java
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost");
// 创建一个每分钟最多100次请求的限流器
FixedWindowRateLimiterWithTTL limiter =
new FixedWindowRateLimiterWithTTL(jedis, "api:limit:user1", 60, 100);
for (int i = 0; i < 120; i++) {
if (limiter.allowRequest()) {
System.out.println("处理请求 " + i);
} else {
System.out.println("限流请求 " + i);
}
}
jedis.close();
}
优点:实现简单
缺点:
固定窗口算法无法解决临界问题
Redis的过期机制是惰性删除+定期删除,可能导致key实际过期时间与预期有微小差异
重启导致的窗口重置
在超高并发下会成为单点瓶颈