为什么 Redis 不支持回滚?

#中间件#面试八股

为什么 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、幂等来保证"不出错"而不是"出错能撤销"。
笔记加载中…