Redis不支持分布式事务回滚,MULTI/EXEC仅保障单实例原子性,无UNDO LOG或2PC;Lua脚本只能实现“条件化原子写入”,失败时预防性拒绝而非撤回已写数据。

Redis 本身不支持真正的分布式事务回滚
Redis 的 MULTI/EXEC 只保证单实例内命令的原子性执行,不具备跨节点一致性,也没有类似关系型数据库的 UNDO LOG 或两阶段提交(2PC)机制。所谓“回滚”,只能靠业务层在失败时主动补偿——Lua 脚本只是把多个操作封装成一个原子执行单元,但它无法自动感知上层业务逻辑是否成功,更不会反向撤销已写入的数据。
Lua 脚本能做的只是“条件化原子写入”
利用 redis.call() 的原子执行 + if 判断 + return 提前退出,可以实现“全成功才写,任一失败则不写任何数据”。但这不是回滚,而是预防性拒绝写入。
- 脚本内所有
redis.call()调用在服务端串行执行,中间不被中断 - 若某步
redis.call("GET", KEYS[1])返回 nil,后续逻辑可直接return {err="xxx"},前面没做SET就不会发生 - 一旦某步
redis.call("SET", ...)成功,它就已生效,脚本无法撤回 - 示例:扣库存前检查余额,用
if tonumber(remain)
真正需要回滚语义时必须引入外部协调者
当操作涉及多个 Redis key(甚至跨集群)、或需联动 MySQL、Kafka 等外部系统时,Lua 无能为力。此时应考虑:
- 基于
SET key value EX seconds NX实现乐观锁,配合客户端重试与幂等写入 - 用 TCC 模式:先
try(预占资源,如冻结余额),再confirm(正式扣减)或cancel(解冻) - 记录事务日志到独立存储(如 PostgreSQL 表),由后台任务扫描超时未 confirm 的记录触发 cancel
- 避免在 Lua 中调用
redis.pcall()捕获错误后“假装回滚”——比如已INCR了 A,又想DECRB 来抵消?这违反原子性,且 B 的 DECR 可能失败,状态彻底不一致
容易被忽略的关键细节
很多人以为把多条命令塞进 Lua 就等于“事务安全”,但实际踩坑点很隐蔽:
-
EVAL脚本中不能使用KEYS以外的变量做键名(如拼接"user:"..ARGV[1]),否则集群模式下会路由失败,报错CROSSSLOT Keys in request don't hash to the same slot - 脚本运行超时(默认 5 秒)会导致连接断开,但已执行的部分无法回退——必须设计成幂等或可重试
-
redis.call()抛异常会终止脚本,而redis.pcall()会返回状态表,但后者无法阻止前面已成功的写操作 - 没有 WATCH 机制的 Lua 脚本,在并发读-改-写场景下仍可能覆盖对方结果,得靠 CAS(
GETSET或SET ... XX)自己保证


















