25. 管道 Pipeline 与批处理
本教程共 40 篇 · 第 25 篇 · 更新于 2026-08-02
25. 管道 Pipeline 与批处理
Redis 是一个基于 TCP、采用请求/响应(request/response)协议的服务器。默认情况下,客户端每发出一条命令,都要经历「发送命令 → 服务端执行 → 返回结果 → 客户端接收」这样一个完整的往返,期间客户端通常会阻塞等待响应。当我们需要连续执行很多条命令时,这种「一条一等一」的模式会累积大量网络往返时间(RTT,Round Trip Time),成为性能瓶颈。
管道(Pipeline) 就是为了解决这个问题而生的:它允许客户端在没有收到前一条命令回复的情况下,连续把多条命令发送给服务端,最后一次性读取所有响应。换句话说,管道把 N 次网络往返压缩成了 1 次,从而显著提升吞吐量。下面码上学带你从原理到实战把管道彻底弄懂。
本节目标
- 理解 Redis 请求/响应模型与 RTT 对性能的影响。
- 掌握管道的工作机制,能说出「一次往返批量发送多条命令」的本质。
- 会用原始 TCP(nc)和 redis-cli 演示管道。
- 区分管道与事务(MULTI/EXEC)的异同,避免误用。
- 了解管道的适用边界与注意事项。
为什么需要管道
在普通的请求/响应模式下,假设客户端与服务端之间的网络往返延迟是 1 毫秒,那么 1 秒内最多大约只能完成 1000 次「请求+响应」。即使 Redis 本身处理一条命令只需要微秒级,瓶颈也卡在了网络上。
举个例子,我们要依次执行 PING、SET、GET、INCR 这几条命令。普通模式下时间线是:
客户端 -> 发送 PING ----------------------------------> 服务端
客户端 <- 收到 PONG ----------------------------------- 服务端
客户端 -> 发送 SET ------------------------------------> 服务端
客户端 <- 收到 OK ------------------------------------- 服务端
客户端 -> 发送 GET ------------------------------------> 服务端
客户端 <- 收到 value ---------------------------------- 服务端
...每条命令都要等上一次往返完成
可以看到,每条命令都要独自承担一次 RTT。当命令数量达到成千上万时,累积延迟非常可观。
管道的思路是:客户端把多条命令一股脑写进同一个 TCP 连接,不等待每条命令的回复,等服务端把所有命令都处理完,再一次性把全部响应读回来。于是 N 条命令的 RTT 从 N 次变成了 1 次。
管道的工作机制
管道并没有改变 Redis 的服务端协议,它只是改变了客户端与服务端之间命令和回复的发送节奏。服务端依然是一条一条地执行命令、把回复写回连接;区别仅在于客户端不再「发一条就阻塞等一条」,而是批量发送、批量读取。
注意:管道只是把多个命令打包在一次网络往返里传输,并不保证这些命令的原子性。每条命令在服务端仍然是独立、按顺序执行的。原子性要靠事务(MULTI/EXEC)或 Lua 脚本/EVAL 来保证(见第 22、23 章)。
可运行示例:原始 TCP 演示管道
最直观的管道演示,是直接用 nc(netcat)往 Redis 的 6379 端口写入一组以 \r\n 分隔的命令。这组命令会被服务端当作一次请求流批量处理,然后一次性返回所有结果。
(echo -en "PING\r\nSET p:k1 redis\r\nGET p:k1\r\nINCR p:visitors\r\nINCR p:visitors\r\nINCR p:visitors\r\n"; sleep 1) | nc localhost 6379
上面的命令向服务端提交了 PING、SET、GET 以及 3 次 INCR,随后我们 sleep 一会儿等待服务端返回,得到的响应如下:
+PONG
+OK
$5
redis
:1
:2
:3
其中 +PONG、+OK 是状态回复,$5 redis 是批量回复(长度 5 的字符串 redis),:1、:2、:3 是整数回复——对应 3 次自增的结果。可以看到,所有命令的回复是一次性返回给客户端的,这正是管道的效果。
可运行示例:redis-cli 中的等价命令
为了对照,下面用交互式 redis-cli 把同样的几条命令「逐条」执行一遍。注意这里每条命令都带 127.0.0.1:6379> 提示符,且每条都会单独得到回复——这正是对比「非管道」模式。
127.0.0.1:6379> PING
PONG
127.0.0.1:6379> SET p:k1 redis
OK
127.0.0.1:6379> GET p:k1
"redis"
127.0.0.1:6379> INCR p:visitors
(integer) 1
在真实客户端里(如 redis-py、Jedis、Lettuce),管道通常是一个 API 方法,例如 pipeline()。它们内部做的就是「攒一批命令 → 一次性 flush 到 socket → 一次性读取全部回复」。第 33、34 章会给出 Java 与 Python 的管道写法。
管道 vs 事务:别混淆
很多初学者会把管道和事务混为一谈,这里码上学帮你划清界限:
| 维度 | 管道 Pipeline | 事务 MULTI/EXEC |
|---|---|---|
| 目的 | 减少网络 RTT,提升吞吐量 | 保证一组命令原子执行 |
| 原子性 | 不保证,命令各自独立执行 | 保证(EXEC 前命令排队,一起执行) |
| 是否阻塞其他客户端 | 否,只是一次发了多条 | MULTI 期间服务端会排队命令 |
| 中途能否读到中间结果 | 不能,要等全部读完 | 不能,EXEC 前读不到 |
| 失败处理 | 某条命令出错不影响其它 | 入队期错误整体取消;执行期错误不回滚 |
一句话记忆:管道解决的是「网络慢」,事务解决的是「并发安全」。二者可以叠加使用:在 MULTI/EXEC 事务外用管道包裹,既保证原子性又减少往返。
管道 vs 脚本(EVAL)
Redis 还提供了 Lua 脚本(EVAL,见第 23 章)。脚本的优势是可以在服务端完成「读—计算—写」的闭环,且整个脚本原子执行。管道的优势则是简单——不需要写脚本逻辑,纯粹把已有命令批量发送。
经验法则:
- 如果只是「批量发命令、彼此独立」→ 用管道。
- 如果命令之间需要「先读后写、依赖前一条结果做判断」→ 用 Lua 脚本或函数(FUNCTION)。
- 如果要求「要么全做要么全不做」的原子性 → 用事务或脚本。
提示(Redis 8.x 新基线说明):本文基于 Redis 8.x(最新稳定版)行文。Redis 7.0 起,原本用于小集合的
ziplist底层结构已被listpack取代;管道本身属于客户端协议层能力,与版本无关,在所有 8.x 版本中行为一致。
管道的注意事项
- 不要一次攒太多命令:管道里的命令会先缓存在客户端内存、再一次性发给服务端。如果一次塞入几十万条命令,会占用大量内存,且服务端也要一次性处理并返回大量回复。建议按批次拆分(例如每批 1000~10000 条)。
- 回复顺序与发送顺序一致:管道保证「第 N 条命令的回复」出现在回复流的第 N 个位置,客户端据此一一对应,无需额外标识。
- 管道内命令不共享上下文:因为管道不等待中间结果,所以不能在管道里写「先 GET 一个值,再根据它 SET」的依赖逻辑——这种依赖要用脚本。
- pub/sub 与阻塞命令慎用管道:订阅、阻塞弹出(BLPOP)等会改变连接状态的命令,混在管道里容易让连接状态混乱,通常单独使用。
redis-cli --pipe用于批量导入:当你要从文件批量灌数据(如redis-cli --pipe < data.txt),底层用的就是管道,这适合初始化大批量数据。
小结
- 管道通过「一次往返发送多条命令」来削减 RTT,是提升吞吐量的性价比最高的手段之一。
- 它只优化网络,不提供原子性;原子性请交给事务或脚本。
- 原始 TCP(
nc)演示能最直观地看到「批量发送、一次性返回」;业务代码里直接用客户端库的 pipeline API 即可。 - 用管道记得控制单批数量,避免内存暴涨和回复过大。
提示:官方 redis.io 下载页另将 8.8 标注为 “Latest stable”,而 GitHub 上的最新发布 tag 为 8.10.0。二者同属 8.x 系列,命令行与类型差异对本教程影响极小,本文以 8.10.0 为基线版本。