redis lua脚本执行失败不会自动回滚,这是设计使然:其“原子性”仅指单线程内连续执行不被中断,不提供acid事务的全成功或全失败语义;已执行的redis.call写命令(如set、incr)一旦成功即永久生效,无undo log,不可撤销。

Redis Lua脚本执行报错后不会回滚——这不是缺陷,是设计事实。已执行的SET、DECRBY、HSET等写命令一旦成功,就永久生效,后续错误不会触发任何撤销动作。
Redis Lua的“原子性”只保证执行不被中断
所谓“原子性”,官方文档明确指:脚本在单线程中连续执行,不被其他客户端命令插队。它不承诺ACID里的“全成功或全失败”。
-
redis.call("SET", "a", "1")成功 → 数据立刻写入内存,不可逆 - 下一行
redis.call("INCR", "b")报(error) ERR value is not an integer→ 脚本立即终止,但a的值不会变回原样 - 没有 undo log,没有事务日志,也没有两阶段提交机制
为什么运行时错误有时看起来像“回滚”?
部分人观察到报错后数据没变,误以为 Redis 有回滚能力。这其实是混淆了两类错误:
-
语法错误(如括号不匹配、
redis.cal("GET",...)拼错函数名):脚本根本没开始执行,自然无写入 -
运行时错误(如对 string 执行
INCR):Redis 2.6+ 确实会在某些极简场景下“清空本次脚本所有写操作”,但这属于内部实现细节,不构成可依赖的行为;且该行为在集群模式、主从复制、AOF重放等环境下完全不可靠
真正稳定、可验证的行为只有一条:只要 redis.call 返回非 error(即命令实际执行成功),对应写操作就落地了。
用 pcall 捕获错误 ≠ 实现回滚
很多人以为加一层 pcall 就能兜底,结果反而更危险:
-
pcall(redis.call, "INCR", "counter")只捕获 Lua 层面崩溃(如 nil 运算),不捕获 Redis 命令返回的 error_reply - 若你手动写
if type(res) == "table" and res[1] == "ERR" then ...做补偿,补偿逻辑本身可能再失败(比如DEL锁 key 时网络超时) - 并发下两个脚本都读到旧余额、都通过判断、都扣款,最终资损翻倍
安全做法是把校验前置:GET → 显式检查 tonumber() 和业务条件 → 全部通过才执行写操作,避免“先改后判”。
集群与主从让“回滚幻想”彻底失效
哪怕你在单机上侥幸看到某种“回滚效果”,一上生产环境就破灭:
- Redis Cluster 中,脚本若涉及多个 key 且未用
{user:123}对齐 slot,直接报CROSSSLOT,连执行都不让开始 - 主从复制时,含
RANDOMKEY或未调redis.replicate_commands()的脚本会被主节点拒绝,报Write command attempted after non deterministic command -
SCRIPT FLUSH、实例重启、主从切换都会清空脚本缓存,EVALSHA立刻变NOSCRIPT,而你的降级逻辑若没覆盖所有路径,就会静默失败
最易被忽略的一点:Lua 脚本里所有写操作都必须基于调用时传入的 KEYS 数组,不能拼接 key 名。否则即使本地测试通过,在集群路由阶段就已失败,根本走不到“是否回滚”这一步。











