为什么 Redis 不支持回滚?
结论:因为 Redis 的事务根本没有"执行到一半失败要撤销"的设计目标。它只保证三件事:命令入队、EXEC 时连续执行不被插队、执行结果按顺序返回。语法错误在入队阶段就会被拒绝,整个事务不执行;运行时错误(如对字符串执行
INCR)只让那一条失败,其余照常执行。不支持回滚是权衡后的取舍,不是能力缺失。
一、两类错误的不同处理
| 错误类型 | 发生阶段 | 例子 | Redis 行为 |
|---|---|---|---|
| 语法/命令不存在 | 入队(QUEUED 时) | SETT k v、参数个数错 | 标记事务失败,EXEC 返回错误,全部不执行 |
| 运行时类型错误 | 执行(EXEC 时) | 对 String 执行 LPUSH、INCR 非数字 | 该条返回错误,其余命令继续执行 |
| 键不存在 | 执行时 | LPOP 空 list | 返回 nil,不算错误 |
| Lua 运行时错误 | 脚本执行中 | redis.call 参数错 | 脚本中断,已执行的命令不回滚 |
MULTI
SET k1 "abc"
INCR k1 ← 入队成功(此时无法知道 k1 是字符串)
EXEC
→ 1) OK
→ 2) (error) ERR value is not an integer or out of range
→ k1 仍然是 "abc",但第 1 条已生效,没有回滚
二、为什么这样设计
设计取舍:
① 错误多是编码缺陷,不是运行时意外
→ 类型用错、命令拼错,回滚只会掩盖 bug,让问题延后暴露
② 回滚需要"保存旧值"或"undo log"
→ 每个 key 都要记录前像,内存与实现复杂度急剧上升
③ Redis 定位是高性能缓存/数据结构服务器
→ 为了极少发生的错误牺牲常态性能,不划算
④ 单线程顺序执行已提供隔离性
→ 事务内不会被其他客户端插入,语义足够"简单可预测"
官方文档的表述更直接:Redis 事务不支持回滚,是因为"开发者应该在开发阶段发现这些错误",而不是指望运行时撤销。这与关系型数据库的 ACID 事务是两种不同的抽象层次。
三、需要"回滚语义"时怎么办
-- 方案 A:用 Lua 把"检查 + 执行"放进一次原子调用,失败就整体不生效
-- 只有全部校验通过才写,天然避免"部分成功"
if redis.call('EXISTS', KEYS[1]) == 1 then return -1 end -- 已存在,直接失败
redis.call('HSET', KEYS[1], 'status', 'paid')
redis.call('EXPIRE', KEYS[1], 3600)
return 1
// 方案 B:WATCH + MULTI 做乐观锁,冲突则整个事务不提交
while (true) {
redis.watch("stock:1001");
int stock = Integer.parseInt(redis.get("stock:1001"));
if (stock <= 0) { redis.unwatch(); return false; }
redis.multi();
redis.decr("stock:1001");
List<Object> r = redis.exec(); // 被其他客户端改过则返回 null
if (r != null) { return true; } // 提交成功
// 返回 null 表示冲突,重试
}
// 方案 C:把强事务交给数据库,Redis 只做加速
@Transactional
public void pay(long orderId) {
orderMapper.updateStatus(orderId, "PAID"); // 真正的 ACID 在这里
redis.delete("order:" + orderId); // 缓存删除失败也不影响正确性
}
选型判断:要"全成功或全失败"的逻辑放数据库;Redis 侧只做幂等与快速失败。
四、常见追问
- 问:那 Redis 事务到底保证了什么?答:命令连续执行(不被其他客户端插入)、结果按序返回、入队阶段能发现语法错误。它提供的是"隔离性 + 批处理",不是"原子回滚"。
- 问:入队阶段的错误和运行时错误为什么区别对待?答:入队时能判定"这条命令永远不可能成功",继续执行没有意义,所以整批拒绝;运行时错误是数据相关,Redis 选择"尽量把能做的做完"。
- 问:Lua 出错会回滚吗?答:不会。脚本被中断前已执行的写命令仍然生效,所以脚本要么做完整校验,要么保证重复执行幂等。
- 问:不支持回滚会不会导致数据不一致?答:会,但通常由业务承担:用数据库事务保证核心一致性,Redis 只做缓存与辅助计数,允许最终修复。
- 问:有没有"部分回滚"的手段?答:没有原生命令。可自己在脚本里记录反操作(补偿事务),或用
WATCH提前发现冲突避免执行。 - 问:这和"Redis 事务是假事务"的说法一致吗?答:一致。它更接近"命令批处理 + 隔离",不要用关系库的 ACID 预期去理解它。
小结
- 不支持回滚是设计与定位的选择:错误应在开发期暴露,运行时不做昂贵的 undo。
- 记住两类错误的分界:入队失败全不执行,运行时失败部分执行。
- 需要强事务就把逻辑下沉到数据库;Redis 侧用 Lua、WATCH、幂等来保证"不出错"而不是"出错能撤销"。