redis通过lua脚本实现原子性的本质是单线程阻塞执行:脚本在主线程中一次性、不可中断地运行完毕,期间其他客户端请求全部排队等待;需显式声明keys、避免耗时操作、慎用redis.pcall(),java客户端须处理超时重试、幂等及补偿。

Redis 通过 Lua 脚本保证多条命令原子性,核心不是“加锁”或“事务回滚”,而是让整个脚本在主线程中**一次性、不可中断地执行完**——期间其他所有客户端请求都会排队等待,完全无法插入。
单线程阻塞执行是原子性的根本
Redis 主线程采用事件循环模型,所有命令(包括 EVAL)都串行处理。当一个 Lua 脚本开始运行:
- 脚本被完整加载进内置 Lua 解释器,不拆分、不中断
- 所有
redis.call()调用直接走 Redis 内部执行队列,跳过网络解析和命令排队环节 - 从第一行到最后一行执行完毕前,不会响应任何其他客户端的 GET、SET、INCR 等请求
- 即使网络断开,脚本仍继续运行;中间状态对外不可见(比如读到的值不会被别人改掉)
脚本编写必须遵守原子前提
原子性只在脚本设计合规时才真正有效。常见错误会直接破坏语义:
-
KEYS 必须显式声明:集群模式下,Redis 需提前分析 key 所在 slot。写
redis.call("GET", KEYS[1])安全;但local k = "user:"..ARGV[1]; redis.call("GET", k)会导致CROSSSLOT报错 -
禁止耗时操作:不能做万级 for 循环、JSON 解析、正则匹配或调用未提供的函数(如
os.time),否则会阻塞整个实例 -
不用
redis.pcall()捕获异常:它会让失败命令静默跳过,破坏“全成功或全不生效”的原子预期;真要容错,应主动用redis.error_reply()中断并靠业务层兜底 -
条件判断必须内聚:比如扣库存,要用
if redis.call("decrby", KEYS[1], ARGV[1]) >= 0 then ...,而不是先get再判断再set,避免中间被篡改
Java 客户端需配合保障最终正确
脚本执行成功 ≠ 业务成功。Java 层必须处理这些情况:
- 网络超时或连接中断时,需结合 requestId 去重,防止脚本已执行却重试导致超扣
- 用
SCRIPT LOAD缓存脚本 SHA,后续用EVALSHA执行;若返回NOSCRIPT,再 fallback 到EVAL - 解析脚本返回值:比如返回
1表示扣减成功,0表示库存不足,-1表示 key 不存在,不能只看是否抛异常 - 脚本只管 Redis 状态变更;后续发 MQ、更新 DB 等步骤,需单独设计幂等与补偿机制
什么时候该用 Lua 而不是原生命令
不是所有多命令组合都需要 Lua。优先用原生命令:
- 纯递增/递减 → 用
INCRBY、DECRBY - 设置带过期的键 → 用
SET key val NX EX seconds - 获取并设新值 → 用
GETSET - 需要“读-算-写”闭环且含分支逻辑(如秒杀校验用户是否已领、库存是否充足、并发扣减并返回结果)→ 必须用 Lua











