Skip to content

Redis事务整理 ​

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


一、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 的结果做条件判断。因为 GET 在 EXEC 前不会真正返回值给客户端。

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

  • 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.