redis+lua+固定窗口实现分布式限流
用key的过期时间替代固定窗口的时间戳
-- 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 thenredis.call("INCR", key)return 1elsereturn 0end
end
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());}
}
使用
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实际过期时间与预期有微小差异
重启导致的窗口重置
在超高并发下会成为单点瓶颈