基于Redis的限流模式准确率问题及正确实现方案咨询
核心问题根因
你当前的实现有两个核心缺陷:
- 固定窗口限流本身存在边界突刺问题:两个相邻窗口的边界处,可能出现前1秒最后500ms发5次,后1秒前500ms发5次,实际这1秒区间内有10次请求,天然就会触发上游限流。
- 分开调用
INCR和EXPIRE两个Redis命令,不仅多了一次网络往返导致校验延迟高,还不是原子操作,网络延迟放大了窗口切换时的校验误差。
最优解决方案
1. 最低改造成本方案:Lua脚本原子化固定窗口限流
把两次Redis调用合并成单次原子操作,直接把校验耗时从500ms降到单次Redis请求的250ms,大幅降低窗口切换的时间差问题,完全适配你现有架构:
- 执行如下Lua脚本,Redis执行Lua脚本天生原子性,不会出现命令之间的时间差问题:
local rate_key = KEYS[1] local max_request = tonumber(ARGV[1]) local window_second = tonumber(ARGV[2]) local current_count = redis.call('INCR', rate_key) if current_count == 1 then -- 只有第一次计数的时候设置过期时间,避免key永久残留 redis.call('EXPIRE', rate_key, window_second) end return current_count <= max_request
- 调用时传入参数:KEYS为当前秒对应的限流键(比如
rate_limit:20240521150430),ARGV1传限流阈值5,ARGV2传窗口时长1,一次调用即可拿到是否允许请求的结果。
2. 严格限流方案:原子化滑动窗口限流
如果要求任意连续1秒内的请求数都绝对不超过5,彻底解决固定窗口的边界突刺问题,用Redis ZSet实现滑动窗口,同样封装为Lua脚本原子执行:
local rate_key = KEYS[1] local max_request = tonumber(ARGV[1]) local window_second = tonumber(ARGV[2]) local current_time = tonumber(ARGV[3]) -- 先删除1秒前的所有历史请求记录 redis.call('ZREMRANGEBYSCORE', rate_key, 0, current_time - window_second * 1000) -- 统计当前窗口内的请求数 local current_count = redis.call('ZCARD', rate_key) if current_count < max_request then -- 允许请求,写入当前时间戳 redis.call('ZADD', rate_key, current_time, current_time) redis.call('EXPIRE', rate_key, window_second) return 1 else -- 限流拒绝 return 0 end
- 调用时传入当前毫秒级时间戳即可,该方案可以保证任意连续1秒区间内的请求数都不会超过阈值。
附加优化项
- 加本地限流兜底:每个实例本地先做一层令牌桶限流,比如总阈值5,部署3个实例的话每个实例本地最多放行2次/秒,本地校验通过后再请求Redis做全局校验,既减少Redis调用量,也进一步降低超发概率。
- 所有实例和Redis服务器统一做NTP时间同步,避免时钟差导致的窗口计算错误。
内容的提问来源于stack exchange,提问作者No1Lives4Ever
相关产品推荐
相关产品推荐

