Redis 系列 4. 事务、Pipeline 与发布订阅


1. 本文要解决的问题

前面已经看过 Redis 的数据类型和持久化。本文继续看三个容易混在一起的能力:

能力 解决什么问题 不解决什么问题
事务 EXEC 后按顺序连续执行事务队列中的命令 不提供数据库那种回滚机制
Pipeline 减少客户端和服务端多次往返的 RTT 开销 不保证原子性,也不改变命令执行语义
发布订阅 消息广播,发布者和订阅者解耦 不保证消息持久化、ACK、重试

从本质上看,它们分别对应三个不同问题:

  1. 事务关注执行顺序。
  2. Pipeline 关注网络往返。
  3. 发布订阅关注消息分发。

2. Redis 事务是什么

Redis 事务可以一次执行多个命令。事务中的命令会按顺序进入队列,等执行 EXEC 时再依次执行。

这里先强调一个边界:Redis 事务不等于“失败自动回滚”的原子事务。它主要保证事务队列在 EXEC 阶段按顺序连续执行,不提供关系型数据库那种执行期 rollback 能力。

Redis 事务的核心特征:

  1. 使用 MULTI 开启事务。
  2. 后续命令先入队,不会立刻执行。
  3. 使用 EXEC 触发执行。
  4. 使用 DISCARD 放弃事务。
  5. 执行 EXEC 后,事务队列里的命令会在 Redis 单线程执行期间连续执行,不会被其他客户端命令插入。

一句话概括:Redis 事务保证的是“一组命令入队后,在 EXEC 阶段连续执行”,不是关系型数据库那种完整 ACID 事务,也不保证执行阶段某条命令失败后整体回滚。

3. Redis 事务和数据库事务的区别

这里是最容易出错的地方。

对比项 Redis 事务 数据库事务
开启方式 MULTI BEGIN 或框架事务
提交方式 EXEC COMMIT
回滚方式 没有执行期自动回滚 ROLLBACK
隔离级别 没有 Read Committed、Repeatable Read 这类隔离级别 有隔离级别
执行顺序 EXEC 阶段事务队列连续执行,不被插队 由数据库事务和锁机制控制
执行错误 已执行成功的命令不会自动撤销 可以整体回滚

所以不要说“Redis 事务和 MySQL 事务一样”。Redis 事务更像是一个命令队列加连续执行机制。

入队阶段错误和执行阶段错误要分开看:

错误阶段 示例 结果
入队阶段错误 命令参数数量错误 EXEC 直接失败,事务队列不执行
执行阶段错误 对字符串执行 INCR 但值不是数字 该命令失败,其它已执行命令不会回滚

4. Redis 事务常用命令

命令 说明
MULTI 标记事务开始
EXEC 执行事务队列里的命令
DISCARD 放弃事务队列
WATCH key [key …] 监视 key,实现乐观锁
UNWATCH 取消监视

5. Case 1:正常执行

典型流程:

multi
set k1 v1
set k2 v2
get k1
exec

在 EXEC 之前,命令只是入队;执行 EXEC 后,Redis 才依次执行队列中的命令。

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> incr count
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) OK
3) OK
4) (integer) 1
127.0.0.1:6379> get k3
"v3"

6. Case 2:放弃事务

如果事务已经开启,但不想执行,可以使用 DISCARD。

multi
set k1 v1
set k2 v2
discard
get k1

DISCARD 会清空事务队列。

127.0.0.1:6379> multi
OK

127.0.0.1:6379(TX)> set k1 v11
QUEUED

127.0.0.1:6379(TX)> set k2 v22
QUEUED

127.0.0.1:6379(TX)> incr count
QUEUED

127.0.0.1:6379(TX)> discard
OK

127.0.0.1:6379> get k2
"v2"

7. Case 3:入队阶段出错

如果命令在入队阶段就发生语法错误,比如参数个数不对,Redis 会标记事务失败。后续执行 EXEC 时,整个事务不会执行。

示例:

multi
set k1 v1
set k2
set k3 v3
exec

set k2 参数错误,这类错误会导致 EXEC 阶段直接失败,事务队列不执行。

这里叫“全体连坐”:

127.0.0.1:6379> multi                         # 开启事务
OK

127.0.0.1:6379(TX)> set k1 v11                # 将 set k1 v11 加入事务队列,暂时不会立即执行
QUEUED

127.0.0.1:6379(TX)> set k2 v2222              # 将 set k2 v2222 加入事务队列,暂时不会立即执行
QUEUED

127.0.0.1:6379(TX)> set k3                    # 错误 1:SET 命令参数不完整,缺少 value
(error) ERR wrong number of arguments for 'set' command

127.0.0.1:6379(TX)> exec                      # 错误 2:事务中前面已经出现命令入队错误,所以 EXEC 执行失败
(error) EXECABORT Transaction discarded because of previous errors.

127.0.0.1:6379> get k1                        # 因为整个事务被丢弃,set k1 v11 没有真正执行

8. Case 4:执行阶段出错

另一类错误是在 EXEC 后执行命令时才发现,比如对 String 执行 List 命令。

示例:

set k1 v1

multi
incr k1
set k2 v2
exec

INCR k1 会因为 k1 不是整数而失败,但 set k2 v2 仍然会执行成功。

这就是 Redis 事务和数据库事务最大的不同:Redis 不会因为执行阶段某条命令失败,就把已经成功执行的命令自动回滚。

127.0.0.1:6379> set email 123@qq.com                    # 设置 email 的值为字符串 123@qq.com

OK

127.0.0.1:6379> multi                                   # 开启事务

OK

127.0.0.1:6379(TX)> set k1 v22                          # 将 set k1 v22 加入事务队列,暂时不执行

QUEUED

127.0.0.1:6379(TX)> set k2 v33                          # 将 set k2 v33 加入事务队列,暂时不执行

QUEUED

127.0.0.1:6379(TX)> incr email                          # 将 incr email 加入事务队列,暂时不执行

QUEUED

127.0.0.1:6379(TX)> set k3 v44                          # 将 set k3 v44 加入事务队列,暂时不执行

QUEUED

127.0.0.1:6379(TX)> exec                                # 执行事务队列中的所有命令
1) OK                                                   # set k1 v22 执行成功
2) OK                                                   # set k2 v33 执行成功
3) (error) ERR value is not an integer or out of range  # incr email 执行失败,因为 email 的值是字符串,不是整数
4) OK                                                   # set k3 v44 执行成功

127.0.0.1:6379> get k1                                  # 查看 k1 的值
"v22"

9. WATCH 乐观锁

WATCH 用来实现乐观锁,本质上是 key 级别的 CAS 思想:先观察 key,提交前检查 key 是否被改过。

流程是:

  1. 客户端执行 WATCH key。
  2. 客户端读取 key 当前值。
  3. 客户端开启事务 MULTI。
  4. 客户端把修改命令入队。
  5. 如果 EXEC 前被监视的 key 被其他客户端改过,EXEC 返回 (nil) 或空数组,事务不执行。

示例:

watch stock:1001
get stock:1001
multi
decr stock:1001
exec

如果在 WATCH 和 EXEC 之间,另一个客户端修改了 stock:1001,本次 EXEC 就不会执行。

9.1 WATCH 监控 key

WATCH 命令是 Redis 中实现乐观锁的一种方式。

它的作用是:在事务执行之前,先监控一个或多个 key。如果在执行 EXEC 之前,被监控的 key 被其他客户端修改了,那么当前事务就会执行失败。

简单来说:

WATCH 监控 key
MULTI 开启事务
EXEC 提交事务
如果 WATCH 的 key 被别人改过,EXEC 返回 (nil) 或空数组,事务执行失败

9.1.1 key 被其他客户端修改,事务执行失败

客户端 1 先监控 balance:

127.0.0.1:6379> watch balance          # 监控 balance
OK

127.0.0.1:6379> MULTI                  # 开启事务
OK

127.0.0.1:6379(TX)> set k1 abc3        # 将 set k1 abc3 加入事务队列
QUEUED

127.0.0.1:6379(TX)> get k1             # 将 get k1 加入事务队列
QUEUED

127.0.0.1:6379(TX)> set balance 150    # 将修改 balance 的命令加入事务队列
QUEUED

此时客户端 2 修改了 balance:

127.0.0.1:6379> set balance 120        # client2 修改了 balance
OK

127.0.0.1:6379> get balance            # 查看 balance 当前值
"120"

客户端 1 再执行事务:

127.0.0.1:6379(TX)> EXEC               # 提交事务
(nil)                                  # 返回 nil,表示事务执行失败

127.0.0.1:6379> get balance            # 查看 balance
"120"

因为 balance 已经被客户端 2 修改过,所以客户端 1 的事务执行失败。

也就是说,WATCH 监控的 key 一旦在事务提交前发生变化,后面的 EXEC 就不会真正执行事务中的命令。

9.1.2 使用 UNWATCH 取消监控

如果不想继续监控某个 key,可以使用 UNWATCH 取消监控。

客户端 1 先监控 balance:

127.0.0.1:6379> WATCH balance          # 监控 balance
OK

127.0.0.1:6379> get balance            # 查看当前 balance
"400"

此时客户端 2 修改 balance:

127.0.0.1:6379> set balance 200        # client2 修改 balance
OK

127.0.0.1:6379> get balance            # 查看修改后的 balance
"200"

客户端 1 执行 UNWATCH:

127.0.0.1:6379> UNWATCH                # 取消对 balance 的监控
OK

取消监控后,再开启事务:

127.0.0.1:6379> MULTI                  # 开启事务
OK

127.0.0.1:6379(TX)> set balance 300    # 修改 balance
QUEUED

127.0.0.1:6379(TX)> get balance        # 查看 balance
QUEUED

127.0.0.1:6379(TX)> EXEC               # 提交事务
1) OK
2) "300"

由于在开启事务前已经执行了 UNWATCH,取消了对 balance 的监控,所以即使之前 balance 被其他客户端修改过,也不会影响后续事务执行。

9.1.3 小结

WATCH 是 Redis 乐观锁的实现方式。

它的核心逻辑是:

先监控 key
再开启事务
提交事务时检查 key 是否被修改
如果被修改,事务失败
如果没有被修改,事务成功

需要注意:

  • WATCH 必须在 MULTI 之前执行;
  • 被 WATCH 的 key 如果在 EXEC 前被其他客户端修改,事务会失败;
  • EXEC 或 DISCARD 执行后,Redis 会自动清除 WATCH 状态;
  • UNWATCH 可以手动取消所有已监控的 key;
  • WATCH 适合用于余额扣减、库存扣减等需要防止并发修改的场景。

简单理解:

WATCH 就是在提交事务前,检查数据有没有被别人改过。
如果别人改过,事务就不执行。

使用 WATCH 时要注意:

  1. EXEC 或 DISCARD 后会自动取消 WATCH。
  2. 也可以显式执行 UNWATCH。
  3. WATCH 必须在 MULTI 前生效,冲突后需要业务自己重试。

10. Pipeline 要解决什么问题

看一个典型问题:客户端要连续执行 10000 条 Redis 命令,如果每条命令都经历一次请求和响应,网络往返时间会成为主要开销。

Redis 是一种基于客户端-服务端模型以及请求/响应协议的 TCP 服务。

一个请求会遵循以下步骤:

  1. 客户端向服务端发送命令,过程包括:发送命令 → 命令排队 → 命令执行 → 返回结果。
  2. 客户端会监听 Socket 返回,通常以阻塞模式等待服务端响应。
  3. 服务端处理命令,并将结果返回给客户端。
  4. 也就是数据包往返于两端所消耗的时间。

Redis 客户端与服务端的交互模型

10.1 存在的问题

如果同时需要执行大量命令,那么客户端需要等待上一条命令应答后,再继续执行下一条命令。

这样中间不仅增加了很多 RTT,也会频繁调用系统 IO,发送网络请求。

同时,Redis 需要多次调用 read() 和 write() 系统方法。系统方法会将数据从用户态转移到内核态,这样会对进程上下文产生比较大的影响,导致性能变差。

简单来说:命令越多,请求响应次数越多,RTT 开销越大,性能越差。

这也是 Redis Pipeline 出现的原因。Pipeline 可以一次性发送多条命令,减少客户端和服务端之间的网络往返次数,从而提升批量命令执行效率。

Pipeline 的思路是:客户端把多条命令连续发给 Redis,Redis 依次执行,再把响应按顺序返回。它不是 Redis 服务端的事务语义,而是客户端批量发送和批量读取响应的用法。

Redis pipeline 的交互模型

管道(pipeline)可以一次性发送多条命令给服务端,服务端依次处理后按顺序返回结果,客户端再按发送顺序读取这些响应。

通过减少客户端与 Redis 的通信次数,可以降低往返延时时间。

Redis 单线程执行命令,配合客户端按发送顺序读取响应,可以保证同一个连接上的 Pipeline 响应顺序和命令顺序一致。

普通模式:一条命令一次请求,一条命令一次响应

Pipeline:多条命令批量发送,多个结果按顺序读取

Pipeline 的主要作用是减少网络请求次数,降低 RTT 带来的性能损耗,适合批量执行大量 Redis 命令的场景。

11. Pipeline 的本质

Pipeline 只解决网络往返问题,不改变 Redis 命令执行语义。

它有几个关键点:

  1. 命令仍然在 Redis 服务端按顺序执行。
  2. Pipeline 不是事务,不保证原子性。
  3. 某条命令失败,不影响后续命令继续执行。
  4. 响应结果按命令顺序返回。
  5. Pipeline 需要客户端库支持,是客户端行为,不是单独的 Redis 事务能力。

下面使用 redis-cli –pipe 演示批量发送命令:

[root@hadoop100 myredis]# ls -l cmd.txt  # 查看 cmd.txt 文件的详细信息
-rw-r--r--. 1 root root 87 cmd.txt

[root@hadoop100 myredis]# cat cmd.txt  # 查看 cmd.txt 文件中的 Redis 命令
set k100 v100  # 设置字符串 key:k100,value:v100
set k200 v200  # 设置字符串 key:k200,value:v200
hset k300 name haha  # 设置 Hash 类型 key:k300,其中 field 为 name,value 为 haha
hset k300 age 20  # 设置 Hash 类型 key:k300,其中 field 为 age,value 为 20
hset k300 gender male  # 设置 Hash 类型 key:k300,其中 field 为 gender,value 为 male

[root@hadoop100 myredis]# cat cmd.txt | redis-cli -a 111111 --pipe  # 使用管道模式批量执行 cmd.txt 中的 Redis 命令
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
All data transferred. Waiting for the last reply... # 命令已经全部发送到 Redis,等待服务端最后的响应
Last reply received from server.  # 已收到 Redis 服务端最后的响应
errors: 0, replies: 5  # 执行结果:0 个错误,5 条命令都有响应

[root@hadoop100 myredis]# redis-cli -a 111111  # 使用密码连接 Redis 客户端
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.

127.0.0.1:6379> get k100 # 查看字符串 key:k100 的值
"v100"

127.0.0.1:6379> hget k300 name   # 查看 Hash 类型 key:k300 中 name 字段的值
"haha"

127.0.0.1:6379>

严格说,redis-cli –pipe 是把符合 Redis 协议格式的数据一次性写入 socket,并批量读取响应,适合导入大量命令。它能演示批量发送效果,但不等同于业务客户端里的 pipeline API。

12. Pipeline 和批量命令的区别

比如 MGET、MSET 是 Redis 原生命令,Pipeline 是客户端批量发送多条命令。

对比项 原生批量命令 Pipeline
示例 MGET k1 k2 k3 GET k1、HGET h1 f1、INCR c1 一起发
命令类型 通常同一种命令 可以混合多种命令
实现位置 Redis 服务端命令,属于 server-side batching 客户端批量发送,属于 client-side batching
原子性 取决于具体命令语义 不保证原子性
返回结果 一个命令的返回 多个命令的返回列表

如果 Redis 已经提供了合适的批量命令,比如 MGET,优先使用原生命令。Pipeline 更适合多类型命令混合或客户端批处理场景。

13. Pipeline 和事务的区别

对比项 Pipeline 事务
目的 减少 RTT 连续执行一组命令
是否需要 MULTI 不需要 需要
是否一次发送 通常一次发送多条命令 命令入队,EXEC 后执行
是否防止其他命令插入 不保证 事务队列执行时不会被插入
是否回滚 不回滚 也不提供执行期自动回滚
是否优化 RTT 不是主要目标,事务本身不减少多次命令入队的网络往返
隔离语义 没有事务隔离语义 EXEC 阶段连续执行,不被插队

要避免一个误区:事务和 Pipeline 都不是“失败就全部回滚”的机制。

14. Pipeline 使用注意事项

Pipeline 不是越大越好。命令太多会带来几个问题:

  1. 客户端要缓存大量命令和响应。
  2. Redis 服务端也要准备大量返回结果。
  3. 单次响应太大可能导致客户端阻塞。
  4. 某批命令耗时太长会影响延迟。

实践上可以分批,比如每 500 或 1000 条命令提交一次,具体大小要按命令复杂度、value 大小和网络情况测试。batch 越大,RTT 摊薄效果越明显,但单批响应越大、客户端等待时间越长,也更容易造成延迟尖峰。生产上要在吞吐和尾延迟之间做平衡。

15. 发布订阅是什么

Redis 发布订阅是一种消息通信模式:

  1. 发布者使用 PUBLISH 往 channel 发消息。
  2. 订阅者使用 SUBSCRIBE 订阅 channel。
  3. 消息发布后,当前在线订阅者会收到消息。

从实现模型上看,Redis 会在内存里维护 channel 和订阅客户端之间的关系。发布消息时,Redis 把消息 fan-out 给当前订阅该 channel 或匹配 pattern 的在线客户端。

Redis PubSub

发布订阅更像一种轻量广播机制。它不保存历史消息,也没有 ACK 和重试能力。客户端断开后,这段时间内发布的消息不可恢复,一般只适合处理实时性较高、丢失可接受的通知类消息。

16. 发布订阅常用命令

命令 说明
SUBSCRIBE channel [channel …] 订阅频道
UNSUBSCRIBE [channel …] 取消订阅
PUBLISH channel message 发布消息
PSUBSCRIBE pattern [pattern …] 按模式订阅
PUNSUBSCRIBE [pattern …] 取消模式订阅
PUBSUB CHANNELS [pattern] 查看活跃频道
PUBSUB NUMSUB channel [channel …] 查看频道订阅数

PSUBSCRIBE 使用的是 glob 风格 pattern,例如 news:* 可以匹配多个频道。PUBSUB CHANNELS 适合单机或普通排查;在 Cluster 模式下,Pub/Sub 的传播和可见范围与具体 Redis 版本、是否使用 sharded Pub/Sub 有关,不能简单按单机行为推断。

终端 1 订阅:

subscribe news:sport

终端 2 发布:

publish news:sport "hello redis pubsub"

订阅方会收到消息。

17. 为什么发布订阅只能了解

发布订阅适合轻量广播,不适合作为可靠消息队列。

它的限制很明确:

  1. 消息不持久化。
  2. 订阅者不在线时收不到历史消息。
  3. 没有 ACK 确认,也没有消费确认机制。
  4. 没有失败重试。
  5. 没有消费组、offset、replay 语义。
  6. 消息处理失败后不能从服务端重新拉取。

所以它适合做:

  1. 本地通知
  2. 简单广播
  3. 低价值实时消息
  4. 日志推送、UI 状态更新这类允许偶发丢失的事件
  5. 订阅者掉线可接受的场景

如果需要可靠消息,优先考虑 Redis Stream、Kafka、RabbitMQ、RocketMQ 等更合适的方案。

18. 三者对比总结

能力 关键词 典型命令 适合场景
事务 连续执行、不被插队 MULTI、EXEC、WATCH 多命令按顺序提交,乐观锁
Pipeline 减少 RTT 客户端 pipeline API 批量写入、批量读取、多命令批处理
发布订阅 实时广播 PUBLISH、SUBSCRIBE 在线通知、临时广播

可以用一句话区分三者:

能力 统一模型 主要权衡
Transaction execution grouping 强调连续执行,不负责失败回滚
Pipeline network optimization 降低 RTT,但不提供隔离和可靠性
PubSub messaging fan-out 延迟低,但可靠性弱

最重要的几个结论:

  1. Redis 事务不等于数据库事务,不提供执行期自动回滚。
  2. Pipeline 不是事务,只是减少网络往返。
  3. 发布订阅不是可靠 MQ,离线消息会丢。
  4. 如果要在 Redis 内做可靠一些的消息流,优先看 Stream。

下一篇开始进入 Redis 高可用基础:主从复制。


文章作者: hnbian
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hnbian !
评论
  目录