Java 无法直接原子判断再 set 是因网络延迟和多节点并发引发竞态,而 Redis Lua 脚本单线程执行可保证 HMGET、HSET 和条件判断整体原子性。

为什么不能直接在 Java 里做原子判断再 set
因为网络往返 + 多节点并发会导致竞态:Client A 读到 curr_permits=2,Client B 同时也读到 curr_permits=2,两者都判断“够用”,各自减 1 后写回 1,实际应为 0。结果就是多放行一次请求——这在秒杀或支付场景里是致命的。
Redis 单线程执行 Lua 脚本天然规避这个问题:EVAL 内所有操作串行、不可打断,HMGET + HSET + 条件判断全部锁在一次调用里。
acquire 函数必须自己计算令牌增量,不能依赖定时任务
分布式环境下没有全局时钟同步,也不能靠客户端时间戳。脚本里必须用 redis.call('TIME') 拿服务端当前秒+微秒,再换算成毫秒(注意除以 1000),然后和 last_mill_second 做差,乘以 rate 得到应补充的令牌数。
常见错误是直接用 os.time() 或 Java 传入的时间,会导致不同节点时间偏差引发漏放或误拒。
-
last_mill_second初始为空时,要主动HSET一次,否则后续计算会因 nil 报错 - 补令牌后要取
math.min(expect_curr_permits, max_permits),防止桶溢出 - 申请失败时仍要
HSET curr_permits,否则下次调用会基于过期值计算
Java 层传参必须严格对齐 Lua 的 KEYS 和 ARGV 顺序
比如脚本定义 acquire(key, apply),Java 调用 redisTemplate.execute() 时:
— KEYS[1] 必须是业务 key(如 "rate:uid:1001")
— ARGV[1] 是申请令牌数(通常为 1)
— ARGV[2] 是 max_permits,ARGV[3] 是 rate,ARGV[4] 是初始化用的默认 last_mill_second(可选)
顺序错一位,整个逻辑就崩了,且错误现象是“永远返回 -1”或“令牌数乱跳”,很难 debug。
当用户要求为 Java 代码添加注释、修改注释、审查注释、整理注释格式,或要求按照代码注释规范处理 Java 文件时,使用本技能。本技能用于统一生成和维护 Java 类注释模板、按 Git 对比 release 判断版本号、补充每行代码上方中文块注释、调整注释空行规则,并保持 Java 文件 UTF-8 无 BOM 编码。典型触发场景包括:补充类注释、更新 @version、检查 @author 来源、整理行尾注释、把注释放到代码上一行、检查注释是否符合规范、构建前检查 Java 注释风格。
- 不要把
rate硬编码在 Lua 里,它得由 Java 控制——不同接口 QPS 阈值不同 - key 命名需带业务上下文(如
"rate:api:/order/create"),避免多个限流器共用一个 key 导致干扰 - 首次调用前不用手动
HSET初始化,Lua 里已处理 nil 分支
超时和降级必须在 Java 层兜底,Lua 不负责重试
Redis 执行 Lua 是原子的,但网络超时、连接中断、Lua 运行报错(比如 tonumber 转换失败)这些都发生在 Java 层。如果只依赖 Lua 返回值,遇到 JedisConnectionException 就直接抛给上游,用户看到的是 500 而不是限流提示。
正确做法是:Java 层捕获 Redis 异常后,走本地缓存 fallback(如 Guava RateLimiter)或直接放行,并打告警日志——不能让限流组件变成系统单点故障源。
立即学习“Java免费学习笔记(深入)”;
最易被忽略的一点:Lua 脚本里所有 redis.pcall 都该包住,而不是裸写 redis.call;一旦某个 HSET 失败(比如 key 被人为 del),后续逻辑可能因访问 nil 字段 crash,整个脚本返回 nil,Java 层若没判空就会 NPE。

















