Skip to content

Redis事务整理

Redis 事务不是传统关系型数据库那种“强隔离 + 可回滚”的事务模型,它更像是一组命令的批量顺序执行机制。理解 Redis 事务,重点不在于背诵 MULTIEXEC 这些命令,而在于搞清楚它到底保证了什么、又没有保证什么。


一、Redis 事务是什么

Redis 事务的核心作用是:把多个命令打包起来,按顺序一次性执行,执行过程中不会被其他客户端的命令插入。

Redis 事务主要依赖下面几个命令:

  • MULTI:开启事务。
  • EXEC:提交事务,按顺序执行事务队列中的所有命令。
  • DISCARD:取消事务,清空事务队列。
  • WATCH:监听一个或多个 key,实现乐观锁。
  • UNWATCH:取消监听。

一个最基本的事务示例如下:

shell
MULTI
SET order:1001:status paid
INCR user:1:points
EXEC

执行流程是:

  1. 客户端发送 MULTI 后,连接进入事务状态。
  2. 后续命令不会立即执行,而是进入事务队列。
  3. 客户端发送 EXEC 后,Redis 按入队顺序依次执行这些命令。

二、Redis 事务保证了什么

1. 命令串行执行

事务提交后,队列中的命令会由 Redis 主线程按顺序执行。执行这一批命令时,不会有其他客户端命令插进来。

这意味着 Redis 事务可以保证:

  • 顺序性:事务中的命令按照入队顺序执行。
  • 隔离性的一部分:执行期间不会被其他命令打断。

2. 队列整体提交

事务在 EXEC 之前只是“排队”,并没有真正修改数据。只有执行 EXEC 时,Redis 才会统一开始处理这些命令。

这带来的效果是:要么这批命令都还没执行,要么从 EXEC 开始按顺序全部进入执行阶段,不会出现“执行到一半被其他客户端插队”的情况。


三、Redis 事务不保证什么

这部分才是 Redis 事务最容易被误解的地方。

1. 不支持回滚

Redis 事务没有 rollback 机制。一旦 EXEC 开始执行,即使其中某条命令运行时报错,前面已经成功执行的命令也不会撤销,后面的命令仍然会继续执行。

例如:

shell
SET balance "abc"
MULTI
INCR balance
SET order:1001:status paid
EXEC

这里 INCR balance 会报错,因为 balance 的值不是整数,但后面的 SET order:1001:status paid 仍然会成功执行。

原因很现实:

  • Redis 追求高性能和实现简单。
  • 回滚需要额外记录旧值、维护 undo log,代价较高。
  • Redis 官方认为很多场景下可以通过更合理的数据建模、Lua 脚本或业务补偿来解决,而不是把系统复杂度压给内核。

2. 不具备关系型数据库那种完整 ACID

如果套数据库事务的标准去看,Redis 事务并不等价于 MySQL/InnoDB 事务:

  • Atomicity(原子性):只在“事务作为一个整体开始执行”这个层面上有一定意义,但不支持命令失败后的整体回滚,因此不是严格意义上的原子性。
  • Consistency(一致性):更多依赖业务自己保证。
  • Isolation(隔离性):只能保证执行时不被插队,但不能像数据库那样提供读已提交、可重复读、串行化等隔离级别。
  • Durability(持久性):取决于 Redis 持久化配置,例如 AOF/RDB,而不是事务本身天然保证。

所以更准确的说法是:

Redis 事务提供的是批量、串行、隔离执行,而不是传统数据库意义上的完整事务能力。


四、事务中的两类错误

Redis 事务里的错误要分成两类看。

1. 入队阶段错误

如果命令在入队阶段就有问题,例如命令不存在、参数个数不对,那么 Redis 会在排队时直接报错。

shell
MULTI
SET name zhangsan
INCR
EXEC

这里 INCR 参数不完整,属于语法错误。Redis 会标记这个事务队列有问题,最终 EXEC 时整个事务都不会执行。

2. 执行阶段错误

如果命令语法没问题,但真正执行时因为数据类型等原因出错,那么 Redis 不会回滚。

shell
SET count "hello"
MULTI
INCR count
SET flag 1
EXEC

结果通常是:

  • INCR count 执行失败。
  • SET flag 1 仍然执行成功。

这个区别很重要:

  • 排队时报错:整个事务不会执行。
  • 执行时报错:失败的那条命令报错,但其他命令继续执行。

五、WATCH 机制与乐观锁

Redis 事务本身并不能解决“先读后改”的并发冲突问题,这时要配合 WATCH

WATCH 的语义是:在事务提交前,如果被监听的 key 被其他客户端改过,那么当前事务提交时会失败。

示例:余额扣减

shell
WATCH account:1:balance
GET account:1:balance

MULTI
DECRBY account:1:balance 100
EXEC

执行逻辑如下:

  1. 先用 WATCH 监听 account:1:balance
  2. 客户端读取当前余额,并在本地做业务判断。
  3. 开启事务,把扣减命令放进队列。
  4. 如果在 EXEC 之前,这个 key 被别人改过,那么 EXEC 返回 nil,表示事务提交失败。

这就是 Redis 的乐观锁模型:默认假设冲突少,真正提交时再校验数据是否被改动过。

WATCH 的特点

  • 适合“读 -> 判断 -> 写”这种场景。
  • 不会阻塞其他客户端。
  • 如果提交失败,通常由业务代码重试。

一个典型伪代码如下:

java
while (true) {
    WATCH balanceKey;
    int balance = GET balanceKey;
    if (balance < 100) {
        UNWATCH;
        return "余额不足";
    }

    MULTI;
    DECRBY balanceKey 100;
    List<Object> result = EXEC;
    if (result != null) {
        return "扣减成功";
    }
}

六、为什么很多场景更推荐 Lua 脚本

如果你的需求是“读取一个值,做判断,然后更新多个 key”,很多时候 Lua 脚本比 Redis 事务更合适。

原因有三个:

1. Lua 脚本天然原子执行

Lua 脚本在 Redis 中执行时,会作为一个整体运行,中间不会插入其他命令。

2. 避免多次网络往返

WATCH + MULTI + EXEC 往往需要多次客户端与 Redis 交互,而 Lua 可以把读取、判断、更新都放在服务端一次完成。

3. 逻辑更完整

Lua 可以在脚本内部做条件判断,失败时直接返回,不会出现 Redis 事务那种“中间某条命令报错,但后续继续执行”的局面。

例如库存扣减就很适合 Lua:

lua
local stock = tonumber(redis.call("GET", KEYS[1]))
if not stock or stock < tonumber(ARGV[1]) then
    return 0
end

redis.call("DECRBY", KEYS[1], ARGV[1])
return 1

因此可以这样理解:

  • Redis 事务:更偏向“把几条命令打包顺序执行”。
  • Lua 脚本:更偏向“把一段完整业务逻辑原子执行”。

七、Redis 事务的典型使用场景

1. 多个写命令需要连续执行

例如:

  • 更新订单状态。
  • 记录操作日志。
  • 给用户增加积分。

这些操作希望在 Redis 内部连续执行,中间不被其他命令插入。

2. 配合 WATCH 做 CAS

CAS(Compare And Set)类更新非常适合用 WATCH

  • 读取旧值。
  • 校验是否符合预期。
  • 提交更新。
  • 如果中途被人改动,则重试。

3. 对事务一致性要求没那么强,但要求操作批量化

如果业务允许失败后做补偿,而不是强依赖数据库式回滚,那么 Redis 事务是够用的。


八、使用 Redis 事务时的常见坑

1. 把 Redis 事务当成数据库事务

这是最常见的误区。Redis 事务不支持回滚,也没有复杂隔离级别。不能拿它直接替代 MySQL 事务。

2. 在事务里做依赖前一个结果的判断

Redis 事务在 EXEC 前只是排队,不会提前执行,所以事务内部不能像下面这样做“边执行边判断”:

shell
MULTI
GET stock
DECRBY stock 1
EXEC

这里 DECRBY 并不能基于 GET stock 的结果做条件判断。因为 GETEXEC 前不会真正返回值给客户端。

如果需要“读后判断再写”,应使用:

  • WATCH + MULTI + EXEC
  • 或 Lua 脚本

3. WATCH 后忘记重试

WATCH 不是加锁,只是冲突检测。提交失败并不代表 Redis 出问题,而是说明有并发修改,需要业务自己决定是否重试。

4. 事务里命令太重

Redis 虽然是单线程执行命令,但单条命令如果很重,仍然会阻塞其他请求。事务中堆太多重命令,会让阻塞时间更长。


九、和 Pipeline 的区别

很多人会把事务和 Pipeline 混淆,这两个概念完全不同。

对比项Redis事务Pipeline
目的保证一组命令按顺序集中执行减少网络往返,提高吞吐
是否有事务语义有一定事务语义没有
是否保证执行期间不被插队
是否支持乐观锁可以配合 WATCH不支持
核心价值一致性和顺序性性能优化

一句话区分:

  • 事务解决的是“这些命令要一起按顺序执行”。
  • Pipeline解决的是“这些命令发得更快”。

十、总结

Redis 事务的本质可以概括成三句话:

  1. MULTI + EXEC 提供的是命令队列的顺序执行,不是数据库式强事务。
  2. WATCH 提供的是乐观锁能力,适合处理并发更新。
  3. 需要更强原子逻辑时,优先考虑 Lua 脚本,而不是指望 Redis 事务回滚。

如果面试里被问到 Redis 事务,可以直接抓住下面这几个关键词:

  • 先入队,后执行
  • 执行期间不会被插队
  • 不支持回滚
  • WATCH 实现乐观锁
  • 复杂原子逻辑优先 Lua

Released under the MIT License.