Redis 单条命令的执行极快,但当你需要一次性发送成百上千条命令,或要求多条命令"要么全做要么全不做"时,事情就变复杂了。Pipeline、事务(MULTI/EXEC)、Lua 脚本是 Redis 提供的三种"批量/原子"机制。它们容易被混为一谈——都涉及"多条命令一起执行"——但解决的其实是不同维度的问题:Pipeline 解决网络往返,事务和 Lua 解决原子性。
场景:RTT 才是隐形杀手
先看一个数字直觉。Redis 处理一条命令可能只要几微秒,但一次网络往返(RTT)在同机房也要零点几毫秒,跨机房更高。也就是说,网络延迟往往比命令本身的执行时间高几个数量级。
如果你要执行 1000 条 SET,逐条发送,就是 1000 次 RTT。哪怕 Redis 本身处理只花了几毫秒,光等网络就可能花掉几百毫秒。这就是 Pipeline 要解决的问题。
Pipeline:把 N 次往返压成 1 次
Pipeline(管道)的原理很朴素:客户端把多条命令一次性打包发给服务器,服务器依次执行后把所有响应一次性返回。它把 N 次 RTT 压缩成 1 次。
1 | # 非 pipeline:3 次往返 |
关键要理解 Pipeline 不保证原子性。这 1000 条命令在服务器端仍是逐条执行的,中间可能穿插其他客户端的命令。Pipeline 只是网络层的优化,不是事务。
工程权衡:
- Pipeline 里的命令越多,省下的 RTT 越多,但响应会在服务端和客户端缓冲区里堆积。一次塞十万条命令,缓冲区内存会暴涨。实践中应分批(batch),比如每批 100~1000 条。
- Pipeline 中途某条命令报错(比如对 String 执行 List 操作),不会中断后续命令,错误只反映在那一条的响应里。客户端要逐个检查响应。
事务:MULTI/EXEC 的"伪原子"
Redis 事务用 MULTI 开启,中间的命令先入队(QUEUED),EXEC 时一次性顺序执行。
1 | MULTI |
事务提供两个保证:顺序执行和不被其他客户端打断(EXEC 期间 Redis 不会插入别的命令)。但它和关系型数据库的事务有本质区别:
Redis 事务不支持回滚。 如果 EXEC 中某条命令运行时出错(比如对字符串执行 INCR),这条命令失败,但其他命令照常执行,已经执行的不会撤销。Redis 官方的解释是:运行时错误通常是编程 bug,生产代码里不该出现,为此引入回滚机制会增加复杂度和性能开销,不划算。
要区分两类错误:
- 入队时语法错误(命令不存在、参数个数错):整个事务会在 EXEC 时被拒绝,所有命令都不执行。
- 执行时类型错误(命令合法但运行报错):只有出错的那条失败,其余照常,没有回滚。
WATCH 实现乐观锁。 事务本身无法实现"读后判断再写"。WATCH 可以监视一个或多个 key,如果在 EXEC 之前这些 key 被其他客户端修改了,EXEC 会直接失败返回 nil。这就是 CAS(Compare And Set)式的乐观锁,常用来实现安全的"读-改-写":
1 | WATCH balance:A |
事务的局限很明显:逻辑判断在客户端做,涉及多次往返(WATCH、读、MULTI/EXEC),且冲突时要重试。这正是 Lua 脚本登场的地方。
Lua 脚本:服务端原子计算
Lua 脚本把一段逻辑发到服务端执行,Redis 以单线程、原子的方式运行整个脚本,期间不会被任何其他命令打断。它同时解决了三件事:原子性、减少网络往返、在服务端做复杂逻辑判断。
1 | # EVAL 脚本:KEYS[1]=库存 key,ARGV[1]=扣减数量 |
这段"判断库存 → 扣减"的逻辑如果用事务做,需要 WATCH + 重试;用 Lua 则天然原子,一次往返搞定。这是实现分布式锁释放、限流、秒杀扣库存等场景的首选。
机制要点与权衡:
- 脚本必须保证确定性。 同样的输入在主从、在不同时间执行,结果必须一致。所以脚本里不能用
TIME、随机数等不确定来源去写数据(否则主从不一致)。需要时间/随机数应通过 ARGV 从外部传入。 - 脚本会阻塞整个 Redis。 因为单线程原子执行,一个跑很久的脚本(比如循环几百万次)会卡死所有其他客户端。脚本必须短小快速,严禁在脚本里写重循环或大范围 KEYS 扫描。
- EVALSHA 节省带宽。 第一次用
SCRIPT LOAD把脚本加载进服务端缓存得到 SHA1,之后用EVALSHA <sha>只传哈希值而非整段脚本,减少网络传输。客户端通常会自动处理 NOSCRIPT 的回退。
三者对比
| 机制 | 原子性 | 减少 RTT | 支持逻辑判断 | 典型场景 |
|---|---|---|---|---|
| Pipeline | 否 | 是 | 否 | 批量写入/读取 |
| 事务(MULTI/EXEC) | 是(无回滚) | 部分 | 仅 WATCH 乐观锁 | 简单的多命令打包 |
| Lua 脚本 | 是 | 是 | 是 | 读-改-写、扣库存、限流 |
常见误区
- 以为 Pipeline 是事务。 Pipeline 命令之间可能被其他客户端的命令插入,绝不能用它来保证原子性。
- 指望事务回滚。 把钱从 A 扣了、给 B 加时其中一步运行时报错,Redis 不会帮你还原。涉及强一致的资金类逻辑,要么用 Lua 把判断和写入捆成原子单元,要么在应用层做补偿。
- 在 Lua 里写慢逻辑。 Lua 的原子性来自单线程独占,代价是它一旦慢,整个实例都被它拖住。脚本要"轻进轻出"。
- Lua 脚本里的副作用不确定。 在脚本中调用产生随机或时间相关副作用并写回 Redis,会破坏主从复制一致性。
小结
记住一句话:Pipeline 治网络,事务和 Lua 治原子。Pipeline 是纯粹的吞吐优化,与原子无关;事务提供了无回滚的弱原子和 WATCH 乐观锁;Lua 脚本则是服务端的原子计算单元,是处理"读-改-写"竞态最干净的工具。三者不是替代关系,而是面向不同问题的互补武器。