redis lua脚本原子性源于其单线程事件循环机制,eval执行时独占主线程,所有redis.call()直调内部函数,其他客户端命令排队等待;脚本须显式传keys、禁用耗时操作、条件判断内置,java端应预加载脚本sha1并用evalsha调用。

Java 中 Redis 通过 Lua 脚本保证多条命令原子性,核心不是“调用脚本”,而是把读、判、写逻辑全部收束到 Redis 服务端单线程执行——整个脚本运行期间,其他客户端命令必须排队等待,天然无竞态。
为什么 Lua 脚本能真正原子执行
Redis 主线程采用单事件循环模型,EVAL 或 EVALSHA 触发后,Lua 脚本独占主线程直至结束: • 脚本内所有 redis.call() 直接调用内部函数,不走网络解析和命令队列 • 其他客户端的任何命令(包括 SET、GET、SCRIPT KILL)都会被挂起,无法插入 • 这种原子性是 Redis 自身调度机制决定的,不依赖锁,也不需要 Java 层同步
脚本编写必须遵守的关键规则
违反以下任一规则,都可能破坏原子语义或导致集群报错:
• KEYS 必须显式传入,不能用字符串拼接生成 key(如 "user:"..ARGV[1]),否则集群下触发 CROSSSLOT 错误
• 所有 Redis 操作必须用 redis.call()(失败即中断)或 redis.pcall()(需手动检查返回值)
• 禁止耗时操作:不许 for 循环上万次、不做 JSON 解析、不调用 os.time() 或 math.random()
• 条件判断必须在脚本内完成,例如用 if redis.call("GET", KEYS[1]) >= ARGV[1] then ...,不能先 Java get 再判断
Java 客户端推荐调用方式
避免每次传输脚本内容,提升性能与稳定性:
• Jedis:先 scriptLoad 获取 SHA1,后续用 evalsha 执行;若返回 NOSCRIPT,则 fallback 到 eval
• Lettuce:更推荐 ScriptCommands.scriptLoad + evalSha 组合,并指定 ScriptOutput 类型处理返回值
• 返回值需做语义解析:比如约定 1=成功、0=库存不足、-1=key 不存在,不能仅靠“执行无异常”判断业务成功
典型场景:库存扣减防超卖
这是最常见也最能体现必要性的例子:
• 错误做法:Java 先 get stock,再 if (stock > 0) decr(stock) → 并发时两个请求同时读到 1,都扣成 -1
• 正确脚本(传 KEYS[1]="stock", ARGV[1]="1"):
if redis.call("GET", KEYS[1]) >= ARGV[1] then
return redis.call("DECRBY", KEYS[1], ARGV[1])
else
return -1
end
• Java 层拿到返回值后,再决定是否发 MQ、更新 DB,这些外部动作需单独保障最终一致性
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











