1. 本文要解决的问题
前面已经看过 Redis 的数据类型和持久化。本文继续看三个容易混在一起的能力:
| 能力 | 解决什么问题 | 不解决什么问题 |
|---|---|---|
| 事务 | EXEC 后按顺序连续执行事务队列中的命令 | 不提供数据库那种回滚机制 |
| Pipeline | 减少客户端和服务端多次往返的 RTT 开销 | 不保证原子性,也不改变命令执行语义 |
| 发布订阅 | 消息广播,发布者和订阅者解耦 | 不保证消息持久化、ACK、重试 |
从本质上看,它们分别对应三个不同问题:
- 事务关注执行顺序。
- Pipeline 关注网络往返。
- 发布订阅关注消息分发。
2. Redis 事务是什么
Redis 事务可以一次执行多个命令。事务中的命令会按顺序进入队列,等执行 EXEC 时再依次执行。
这里先强调一个边界:Redis 事务不等于“失败自动回滚”的原子事务。它主要保证事务队列在 EXEC 阶段按顺序连续执行,不提供关系型数据库那种执行期 rollback 能力。
Redis 事务的核心特征:
- 使用 MULTI 开启事务。
- 后续命令先入队,不会立刻执行。
- 使用 EXEC 触发执行。
- 使用 DISCARD 放弃事务。
- 执行 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 是否被改过。
流程是:
- 客户端执行 WATCH key。
- 客户端读取 key 当前值。
- 客户端开启事务 MULTI。
- 客户端把修改命令入队。
- 如果 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 时要注意:
- EXEC 或 DISCARD 后会自动取消 WATCH。
- 也可以显式执行 UNWATCH。
- WATCH 必须在 MULTI 前生效,冲突后需要业务自己重试。
10. Pipeline 要解决什么问题
看一个典型问题:客户端要连续执行 10000 条 Redis 命令,如果每条命令都经历一次请求和响应,网络往返时间会成为主要开销。
Redis 是一种基于客户端-服务端模型以及请求/响应协议的 TCP 服务。
一个请求会遵循以下步骤:
- 客户端向服务端发送命令,过程包括:发送命令 → 命令排队 → 命令执行 → 返回结果。
- 客户端会监听 Socket 返回,通常以阻塞模式等待服务端响应。
- 服务端处理命令,并将结果返回给客户端。
- 也就是数据包往返于两端所消耗的时间。

10.1 存在的问题
如果同时需要执行大量命令,那么客户端需要等待上一条命令应答后,再继续执行下一条命令。
这样中间不仅增加了很多 RTT,也会频繁调用系统 IO,发送网络请求。
同时,Redis 需要多次调用 read() 和 write() 系统方法。系统方法会将数据从用户态转移到内核态,这样会对进程上下文产生比较大的影响,导致性能变差。
简单来说:命令越多,请求响应次数越多,RTT 开销越大,性能越差。
这也是 Redis Pipeline 出现的原因。Pipeline 可以一次性发送多条命令,减少客户端和服务端之间的网络往返次数,从而提升批量命令执行效率。
Pipeline 的思路是:客户端把多条命令连续发给 Redis,Redis 依次执行,再把响应按顺序返回。它不是 Redis 服务端的事务语义,而是客户端批量发送和批量读取响应的用法。

管道(pipeline)可以一次性发送多条命令给服务端,服务端依次处理后按顺序返回结果,客户端再按发送顺序读取这些响应。
通过减少客户端与 Redis 的通信次数,可以降低往返延时时间。
Redis 单线程执行命令,配合客户端按发送顺序读取响应,可以保证同一个连接上的 Pipeline 响应顺序和命令顺序一致。
普通模式:一条命令一次请求,一条命令一次响应
Pipeline:多条命令批量发送,多个结果按顺序读取
Pipeline 的主要作用是减少网络请求次数,降低 RTT 带来的性能损耗,适合批量执行大量 Redis 命令的场景。
11. Pipeline 的本质
Pipeline 只解决网络往返问题,不改变 Redis 命令执行语义。
它有几个关键点:
- 命令仍然在 Redis 服务端按顺序执行。
- Pipeline 不是事务,不保证原子性。
- 某条命令失败,不影响后续命令继续执行。
- 响应结果按命令顺序返回。
- 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 不是越大越好。命令太多会带来几个问题:
- 客户端要缓存大量命令和响应。
- Redis 服务端也要准备大量返回结果。
- 单次响应太大可能导致客户端阻塞。
- 某批命令耗时太长会影响延迟。
实践上可以分批,比如每 500 或 1000 条命令提交一次,具体大小要按命令复杂度、value 大小和网络情况测试。batch 越大,RTT 摊薄效果越明显,但单批响应越大、客户端等待时间越长,也更容易造成延迟尖峰。生产上要在吞吐和尾延迟之间做平衡。
15. 发布订阅是什么
Redis 发布订阅是一种消息通信模式:
- 发布者使用 PUBLISH 往 channel 发消息。
- 订阅者使用 SUBSCRIBE 订阅 channel。
- 消息发布后,当前在线订阅者会收到消息。
从实现模型上看,Redis 会在内存里维护 channel 和订阅客户端之间的关系。发布消息时,Redis 把消息 fan-out 给当前订阅该 channel 或匹配 pattern 的在线客户端。

发布订阅更像一种轻量广播机制。它不保存历史消息,也没有 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. 为什么发布订阅只能了解
发布订阅适合轻量广播,不适合作为可靠消息队列。
它的限制很明确:
- 消息不持久化。
- 订阅者不在线时收不到历史消息。
- 没有 ACK 确认,也没有消费确认机制。
- 没有失败重试。
- 没有消费组、offset、replay 语义。
- 消息处理失败后不能从服务端重新拉取。
所以它适合做:
- 本地通知
- 简单广播
- 低价值实时消息
- 日志推送、UI 状态更新这类允许偶发丢失的事件
- 订阅者掉线可接受的场景
如果需要可靠消息,优先考虑 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 | 延迟低,但可靠性弱 |
最重要的几个结论:
- Redis 事务不等于数据库事务,不提供执行期自动回滚。
- Pipeline 不是事务,只是减少网络往返。
- 发布订阅不是可靠 MQ,离线消息会丢。
- 如果要在 Redis 内做可靠一些的消息流,优先看 Stream。
下一篇开始进入 Redis 高可用基础:主从复制。