必须用Lua脚本做Redis分布式限流,因为INCR+EXPIRE两步操作不原子,易致计数错误;Lua在Redis中原子执行,可一次性完成读、判、写、设过期,避免竞态。

为什么必须用 Lua 脚本做 Redis 分布式限流
因为单靠 INCR + EXPIRE 两步操作无法保证原子性:并发请求可能绕过过期时间检查,导致窗口计数错误。Lua 脚本在 Redis 中是原子执行的,所有逻辑(读、判断、写、设过期)都在服务端一次完成,避免竞态。
常见错误现象:(integer) 1 返回了但实际没限流,或同一秒内计数突增到远超阈值——基本都是没用 Lua,而是客户端分步调用造成的。
- 不要在客户端做“先
GET再INCR”的判断 - 脚本里必须用
redis.call("INCR", key)而非redis.call("GET", key)后再手动加,否则失去原子性 - 过期时间必须用
redis.call("EXPIRE", key, window)在首次 INCR 后立即设置,不能依赖外部定时任务
一个能直接用的限流 Lua 脚本(令牌桶模型)
这个脚本实现的是“固定窗口 + 令牌桶混合逻辑”:每秒重置桶容量,但允许突发流量在桶未空时通过。适用于 API 网关、登录接口等场景。
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = redis.call("INCR", key)
if current == 1 then
redis.call("EXPIRE", key, window)
end
if current > limit then
return 0
end
return 1
说明:KEYS[1] 是限流 key(如 "rate:login:192.168.1.100"),ARGV[1] 是最大请求数,ARGV[2] 是窗口秒数(比如 60 表示 1 分钟)。返回 1 表示放行,0 表示拒绝。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 注意:该脚本不处理“令牌补充”,纯固定窗口;若需平滑限流(如每秒最多 5 次),得改用更复杂的滑动窗口脚本
-
INCR自动创建 key 并初始化为 1,所以current == 1是判断是否为窗口内首次请求的可靠方式 - 别把
window设成毫秒——Redis 的EXPIRE单位是秒,毫秒要用PEXPIRE,且 Lua 中需传整数毫秒值
Java(Lettuce)中调用这个脚本的要点
用 Lettuce 时,不能直接传字符串脚本每次执行,而应先 SCRIPT LOAD 缓存 SHA1,后续用 EVALSHA 调用,减少网络和计算开销。
- 首次加载:
String sha = connection.sync().scriptLoad(luaScript) - 后续执行:
Long result = connection.sync().evalsha(sha, ScriptOutputType.INTEGER, keys, args) - 如果返回
nil(Lettuce 里是null),说明脚本未加载或被驱逐,要 fallback 到EVAL - key 和 arg 都必须是
String类型数组,数值要提前String.valueOf(100)转换,不然 Lettuce 会报ERR Error running script
容易被忽略的 key 设计细节
限流效果好不好,一半取决于 key 是否真正隔离了请求维度。Key 写错,要么全站被限,要么完全不起作用。
- 按用户限流?用
"rate:user:" .. userId,别漏了前缀,也别拼错字段名(比如把userId写成user_id导致不同格式 key 并存) - 按 IP 限流?取真实 IP(Nginx 透传的
X-Real-IP),不是request.getRemoteAddr()—— 后者在有代理时总是127.0.0.1 - 组合维度?比如“用户 + 接口路径”,key 可写成
"rate:api:" .. md5(userId .. ":" .. path),避免特殊字符(/、?)污染 key 结构 - 别用太长的 key:Redis 查找性能与 key 长度相关,超过 100 字符建议哈希截断
滑动窗口精度高但脚本复杂、内存占用大;固定窗口简单高效,适合大多数业务。选哪种,得看你的错误预算和监控能力——真要毫秒级控制,就别只靠 Redis Lua,该上 Sentinel 或专门的限流中间件了。

















