Redis 的 Lua 脚本本身原子执行,外层加事务无效且可能触发 BUSY 错误;应将读写与逻辑判断全部内聚于脚本中,合理选用 redis.call() 或 redis.pcall(),严防超时与 nil 误判,确保各分支状态一致。

为什么 EVAL 和 EVALSHA 不能随便加事务包裹
Redis 的 Lua 脚本本身就是原子执行的,外层再套 WATCH + MULTI 不仅无效,还会引发脚本中断或 (error) BUSY Redis is busy running a script。Lua 执行期间整个 Redis 实例是单线程阻塞的,事务机制在脚本内部已失效。
实操建议:
- 放弃用
WATCH监控脚本内读取的 key,改用脚本内redis.call("GET", key)直接读——读写都在同一原子上下文中 - 若需条件更新(比如“仅当 value == old 时才 SET”),必须把判断逻辑写进 Lua,不要靠客户端读-判-写三步走
- 避免在脚本里调用可能失败的命令(如
redis.call("HGET", "nonexist", "f")会直接抛异常终止脚本),用pcall包裹关键调用
脚本里用 redis.call() 还是 redis.pcall()
redis.call() 遇错立即中止脚本并向上抛出错误;redis.pcall() 则捕获错误、返回 {err="..."} 表,让脚本能继续执行后续逻辑。数据不一致常源于前者未处理的意外中断。
实操建议:
- 对非核心 key 的读写(如日志记录、统计计数)优先用
redis.pcall(),防止局部失败拖垮整个业务逻辑 - 对强一致性要求的主流程(如扣库存、转账),仍用
redis.call(),但必须确保前置条件已校验(比如用EXISTS或HEXISTS检查 key 是否存在) - 别在
pcall后忽略返回值——local res = redis.pcall("INCR", "counter")后要检查res.err是否为nil
本地变量和 redis.call() 返回值混淆导致的逻辑错乱
Lua 脚本里所有变量默认是 local,但开发者常误以为多次 redis.call("GET", k) 会自动缓存结果,或把 nil 当成 “key 不存在”,实际可能是网络抖动、key 被删、类型不匹配等不同原因。
实操建议:
- 每次
redis.call()的返回值都要显式赋给变量,不要链式调用或复用变量名 - 对可能为
nil的返回值,用if type(val) == "table" and val.err then ...做区分,而不是只判if not val - 慎用
redis.call("GET", k) == false这类判断——Redis 的GET返回false只代表 key 不存在,但其他命令(如HGET)返回false可能是 field 不存在,也可能是 key 本身不存在
脚本超时与 SCRIPT KILL 的真实风险
当 Lua 脚本执行超时(默认 lua-time-limit 5000ms),Redis 会尝试用 SCRIPT KILL 中断它。但该命令只对「纯读」脚本安全;一旦脚本已执行过 redis.call("SET", ...) 等写操作,SCRIPT KILL 就拒绝执行,只能 SHUTDOWN NOSAVE 或等脚本自然结束——此时数据已处于中间态。
实操建议:
- 上线前用生产级数据量压测脚本耗时,确保稳定低于
lua-time-limit的 70% - 脚本开头加
if redis.call("EXISTS", KEYS[1]) == 0 then return 0 end类快速出口,避免无谓计算 - 禁用
redis.call("SLEEP", ...)和大循环(如for i=1,10000 do),这类写法极易触发超时且无法被安全中断


















