返回首页

Redis进阶4.3:Pipeline 批量操作

适用版本:Redis 7.2.x

很多人刚开始使用 Redis 时,关注点往往都在命令本身:SET 怎么写、GET 怎么查、HSET 怎么存对象。但真正到了线上环境,性能瓶颈常常不是某一条命令太慢,而是:

  • 客户端和 Redis 之间往返太多次
  • 明明每条命令都很快,但成百上千条命令串行发送后,总耗时明显上来了

这就是 Pipeline 的使用场景。

Pipeline 不是新的数据结构,也不是事务替代品。它解决的是:

把多条命令打包发送,减少网络往返成本,提高批量操作效率。

这篇文章会重点讲清楚:Pipeline 到底是什么、它和事务有什么区别、适合什么场景、不适合什么场景,以及在开发中如何避免误用。


一、为什么 Redis 会需要 Pipeline

先看一个看似普通的场景:批量写入 1000 个缓存键。

如果你这样做:

for 每个 key:
    SET key value
    等待返回

那么流程其实是:

  1. 客户端发送一条命令
  2. Redis 执行后返回结果
  3. 客户端收到结果后再发送下一条
  4. 循环 1000 次

问题不在于 Redis 单条命令慢,而在于每次都发生一次网络往返(RTT)。

如果网络延迟是 1ms,那么 1000 次往返理论上就可能带来约 1000ms 的额外等待;如果是在跨机房、跨可用区或者链路较复杂的环境,这个成本会更明显。

Pipeline 的思路是:

  • 客户端先把多条命令连续写到连接里
  • 不必每发一条都等返回
  • Redis 依次执行这些命令
  • 最后客户端再批量读取返回结果

这样可以显著减少等待链路上的空转时间。


二、什么是 Pipeline

可以把 Pipeline 理解成“命令流水线”。

它不是 Redis 服务端单独提供的一组新命令,而是一种客户端批量发送命令的通信方式

普通模式下,命令交互像这样:

发送一条 -> 等响应 -> 再发送下一条

Pipeline 模式下更像这样:

连续发送多条 -> Redis 顺序执行 -> 再批量收取响应

重点在于:

  • Redis 仍然是一条条执行命令
  • Pipeline 优化的是网络交互,不是把多条命令合并成一条命令

这一点非常重要。


三、一个直观例子:普通发送 vs Pipeline

1. 普通模式

SET user:1:name alice
SET user:2:name bob
SET user:3:name carol
GET user:1:name
GET user:2:name
GET user:3:name

如果客户端严格“一发一等”,会产生 6 次发送、6 次等待。

2. Pipeline 模式

客户端可以把这些命令一次性发出:

SET user:1:name alice
SET user:2:name bob
SET user:3:name carol
GET user:1:name
GET user:2:name
GET user:3:name

Redis 还是按顺序执行,但客户端不必在每条命令之间停下来等结果。

所以 Pipeline 的收益,本质来自:

  • 少了大量来回等待
  • 提高了连接利用率
  • 批量操作时吞吐更高

四、Pipeline 与事务的区别

这是最常见的混淆点。

虽然它们都像是“多条命令一起发”,但解决的问题完全不同。

对比项 Pipeline 事务(MULTI/EXEC)
主要目的 减少网络往返,提高吞吐 组织一组命令顺序提交
是否强调原子性 部分强调顺序与连续执行
是否有提交动作 通常没有单独提交语义 EXEC 提交
是否支持乐观锁 可配合 WATCH
适用场景 批量读写、批量缓存操作 条件更新、并发控制

一句话概括:

  • Pipeline 解决性能问题
  • 事务解决多命令组织与并发控制问题

不要把两者混为一谈。


五、Pipeline 不保证原子性

这点必须单独强调。

假设你通过 Pipeline 连续发送:

GET stock:item:1001
DECRBY stock:item:1001 1

这并不意味着这两条命令之间不会被其他客户端插入写操作。因为 Pipeline 只是让客户端更快地把命令发过去,不是把它们变成一个不可分割的事务。

如果业务要求是:

  • 先判断库存
  • 再安全扣减

那么你应该考虑:

  • 事务 + WATCH
  • Lua 脚本

而不是仅靠 Pipeline。


六、Pipeline 的适合场景

1. 批量写缓存

例如把一批商品详情、用户画像、排行榜片段一次性写入 Redis。

2. 批量读取多个 key

例如一次读取 100 个用户的在线状态,减少频繁往返。

3. 数据迁移与初始化

例如导入一批历史数据、批量构建测试数据。

4. 日志、埋点、计数类批量落库前缓存

如果业务逻辑上允许分批写入 Redis,Pipeline 往往能明显提高吞吐。


七、命令行下如何体验批量导入:redis-cli --pipe

在命令行环境中,redis-cli 提供了一个很实用的方式:--pipe

例如,我们准备一批 Redis 命令:

SET user:1001:name alice
SET user:1002:name bob
SET user:1003:name carol
INCR stats:import_count

可以通过标准输入喂给 redis-cli --pipe

printf 'SET user:1001:name alice\r\nSET user:1002:name bob\r\nSET user:1003:name carol\r\nINCR stats:import_count\r\n' | redis-cli --pipe

这种方式适合:

  • 快速导入测试数据
  • 脚本化执行批量命令
  • 大量初始化写入

不过要注意,--pipe 更偏向命令行批处理工具层面;日常业务代码里,通常直接使用客户端库的 pipeline 接口。


八、Python 客户端中的 Pipeline 示例

下面用 Python 展示最常见的写法。

1. 批量写入

import redis

r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True)

pipe = r.pipeline()
for i in range(1, 6):
    pipe.set(f"demo:user:{i}", f"name-{i}")

result = pipe.execute()
print(result)

可能返回:

[True, True, True, True, True]

2. 批量读取

import redis

r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True)

pipe = r.pipeline()
for i in range(1, 6):
    pipe.get(f"demo:user:{i}")

values = pipe.execute()
print(values)

返回示例:

['name-1', 'name-2', 'name-3', 'name-4', 'name-5']

这类代码的核心价值在于:

  • 命令一次批量发送
  • 结果按顺序返回
  • 客户端层面接口整洁

九、Pipeline 返回结果如何理解

Pipeline 执行后,通常会返回一个数组,数组中的每个元素,对应一条命令的执行结果。

例如:

pipe = r.pipeline()
pipe.set("k1", "v1")
pipe.incr("counter")
pipe.get("k1")
result = pipe.execute()

返回可能是:

[True, 1, 'v1']

对应关系是:

  1. SET k1 v1 -> True
  2. INCR counter -> 1
  3. GET k1 -> 'v1'

因此在使用时,你要非常清楚:

  • 第几个返回值对应第几条命令
  • 哪些命令可能返回空值
  • 哪些命令可能抛错

十、Pipeline 中的错误处理

Pipeline 并不是“要么全成功,要么全失败”。

这和事务很像的一点是:其中某条命令失败,不代表前后命令一定全部失败;但与事务不同的是,Pipeline 本来就没有原子提交语义。

例如你发送:

pipe.set("name", "alice")
pipe.lpop("name")
pipe.get("name")

这里 name 是字符串,对它执行 LPOP 会出错。

在很多客户端里,可能表现为:

  • execute() 抛异常
  • 或者返回结果中带错误对象

这取决于客户端库的实现和配置。

所以实践中要注意:

  • 批量命令最好按业务类型分组
  • 不要把高风险、不确定类型的命令和核心批量写混在同一个 pipeline 里
  • 需要时分段执行,便于定位问题

十一、Pipeline 会不会占用很多内存

会,尤其当你一次塞入太多命令时。

原因有两部分:

  1. 客户端要暂存待发送命令和待接收结果
  2. Redis 也要处理这一大批请求并生成响应

如果一次 pipeline 太大,可能导致:

  • 客户端内存上升
  • 单次请求时间过长
  • 响应缓冲区压力增大
  • 排队过久影响其他业务请求

因此,Pipeline 虽然适合批量,但不代表越大越好。


十二、批量大小怎么控制更合理

没有一个放之四海而皆准的数字,但可以遵循以下经验:

1. 从小批次开始压测

例如:

  • 每批 100 条
  • 每批 500 条
  • 每批 1000 条

结合实际网络延迟、实例规格、命令类型来观察吞吐和延迟。

2. 尽量按业务分批

不要把 10 万条命令一次性塞进一个 pipeline。更常见、更稳妥的做法是:

  • 每批 200~1000 条
  • 批量循环提交

3. 读写分开考虑

  • 批量写通常更容易评估
  • 批量读如果返回值很大,也要注意网络包体积

一个简单的伪代码示例:

def batch_write(items, batch_size=500):
    for i in range(0, len(items), batch_size):
        chunk = items[i:i + batch_size]
        pipe = r.pipeline()
        for key, value in chunk:
            pipe.set(key, value, ex=3600)
        pipe.execute()

这类“分批 pipeline”在工程实践中很常见。


十三、Pipeline 与 MGET / MSET 怎么选

这也是常见问题。

如果你的场景只是:

  • 读取多个字符串 key
  • 写入多个字符串 key

那么 Redis 其实已经提供了更直接的批量命令:

  • MGET
  • MSET

示例:

MSET user:1 alice user:2 bob user:3 carol
MGET user:1 user:2 user:3

这种情况下:

  • 如果命令语义刚好匹配,优先考虑原生命令
  • 如果命令种类复杂、需要混合多种操作,再考虑 pipeline

可以这样理解:

  • 单一类型批量读写:优先原生命令
  • 多命令组合批处理:考虑 Pipeline

十四、Pipeline 与 Lua 怎么选

两者也容易被误认为类似,其实侧重点不同。

Pipeline 关注的是

  • 减少网络往返
  • 提高批量吞吐

Lua 关注的是

  • 把多步逻辑做成一次原子执行
  • 避免并发竞争

举个例子:

批量写入 1000 个缓存键

优先考虑 Pipeline。

判断库存是否足够并扣减

优先考虑 Lua。

所以不要因为“Lua 也能一次发过去”就用它替代 Pipeline,也不要因为“Pipeline 更快”就拿它做并发安全控制。


十五、一个典型实践:批量预热缓存

假设系统上线前,需要把一批商品详情预热到 Redis。

伪代码示例:

import json
import redis

r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True)

products = [
    {"id": 1001, "name": "phone", "price": 3999},
    {"id": 1002, "name": "pad", "price": 2999},
    {"id": 1003, "name": "watch", "price": 1999},
]

pipe = r.pipeline()
for p in products:
    pipe.set(f"product:{p['id']}", json.dumps(p, ensure_ascii=False), ex=1800)

pipe.execute()

这个场景里,Pipeline 很合适,因为:

  • 写入量成批出现
  • 每条写入彼此独立
  • 不要求跨 key 强原子一致
  • 目标是尽快把数据灌入缓存

注意:上面的示例仅用于说明 pipeline 思路。不同客户端的参数写法要以实际库版本为准。


十六、实践建议

1. 把 Pipeline 当作性能优化工具,而不是并发控制工具

它的核心价值是减少 RTT,不是保证业务原子性。

2. 批次不要一味求大

批次太大可能造成:

  • 单次阻塞时间长
  • 内存占用高
  • 错误定位困难

通常“中等批次 + 循环执行”更稳。

3. 优先使用语义更直接的原生命令

如果 MGETMSETHMGET 已经能解决问题,就不一定非要上 Pipeline。

4. 对结果顺序保持清晰映射

哪条命令对应哪个返回值,一定要在代码里有明确约束。否则排查问题很痛苦。

5. 高价值操作分组执行

例如把“缓存预热”和“关键计数器更新”分开放在不同 pipeline 中,避免一个批次内混入太多不同语义操作。


十七、小结

这篇文章的核心结论如下:

  1. Pipeline 的本质,是让客户端批量发送命令,减少网络往返,提高吞吐
  2. 它优化的是通信成本,不是把多条命令变成一个原子事务
  3. 批量写缓存、批量读取、多命令批处理,是 Pipeline 最典型的场景
  4. 遇到需要并发安全、条件判断、原子更新的业务时,应考虑事务或 Lua,而不是仅靠 Pipeline
  5. 工程上要注意批次大小、内存占用、错误处理和返回值映射

如果把事务、Lua、Pipeline 放在一起比较,可以这样记:

  • 事务:组织多条命令提交
  • Lua:把关键逻辑变成服务端原子执行
  • Pipeline:减少批量命令的网络往返

三者解决的问题不同。真正掌握 Redis,并不是把它们都背下来,而是知道:面对当前场景,究竟该拿哪一把工具。


📝 版权声明:本文为原创技术博客,转载请注明出处。

如文章中存在错误或不准确之处,欢迎在评论区指正,感谢您的阅读与支持!

上一篇

Redis进阶4.2: Lua 脚本入门

下一篇

Redis进阶4.4: 原子操作实践