第 7 章 · 事务、Pipeline、Lua、Pub-Sub、Stream

四个交互演示:① 事务+WATCH · ② Pipeline 对比 · ③ Lua 沙盒 · ④ Stream 消费组

事务 + WATCH 乐观锁演示

左侧客户端 A 用 WATCH + MULTI/EXEC 修改 balance;右侧客户端 B 同时操作同一个 Key。观察 A 在 EXEC 时如果 B 改过被 watch 的 key,会返回 (nil) 触发重试。

点击「场景 X」看完整动画,或自己用下方命令按钮在 A、B 两边手动操作。

🅰 客户端 A

IDLE

🅱 客户端 B

IDLE

🗄 服务端 KV 状态

WATCH 原理:服务端用 watched_keys 记录每个 key 被谁监视。任何写命令执行时,会给监视者打上 DIRTY_CAS 标记。EXEC 时如果发现自己被打标,整个事务作废返回 (nil)。这就是「乐观锁」——不阻塞别人,提交时检查冲突。

Pipeline vs 单条 vs 事务:100 个命令的 RTT 对比

以下三种方式都发送 100 条 SET 命令。蓝色方块 = 请求;绿色 = 响应。点「开始」看动画对比 RTT 总耗时。

① 单条 SET

每条都等响应才发下一条
Client Server
已完成: 0 RTT: 0 耗时: 0ms

② Pipeline (非事务)

攒一批一次发,不等单条响应
Client Server
已完成: 0 RTT: 0 耗时: 0ms

③ MULTI/EXEC 事务

攒到 EXEC 才一次执行(原子)
Client Server
已完成: 0 RTT: 0 耗时: 0ms
结论: Pipeline 把 N 次 RTT 压成 N/batch 次;事务最少只要 1 次 RTT 但服务端要排队执行;单条最慢 ≈ N × RTT。 生产建议:要原子性 → 事务 / Lua;只想加速批量操作 → Pipeline。

Lua 脚本沙盒(迷你 EVAL 模拟器)

支持 redis.call('GET'/'SET'/'INCR'/'DECR'/'INCRBY'/'DECRBY'/'EXPIRE'/'EXISTS'/'DEL', ...) 调用, KEYS[i]ARGV[i]tonumbertostringlocalif/elseif/else/endreturn。脚本在「服务端」原子执行,不会被打断。

numkeys 自动 = KEYS 数量

执行结果

点 ▶ EVAL 后这里显示返回值与日志
原子性提示:这里看不出来,但真实 Redis 上整段脚本执行期间主线程不处理别的命令,所以「读改写」绝对原子。注意 Lua 数组下标从 1 开始:KEYS[1]ARGV[1]

Stream + Consumer Group 协作消费动画

左侧 Producer 不停 XADD;右侧两个 Consumer(worker-1 / worker-2)在同一个 Group payproc 中协作,每条消息只投递给一个 worker。点 ACK 把消息从 PEL 移除;点 XCLAIM→W1 把消息抢给 worker-1。

📤 Stream: orders

length: 0 · last-id: 0-0
group: payproc · last-delivered: 0-0
ID = 时间戳-序号,单调递增;蓝色 = 未投递,橙色 = 已投递未 ACK,绿色(划线)= 已 ACK。
🛠 worker-1 已 ACK: 0
PEL(待确认列表):
🛠 worker-2 已 ACK: 0
PEL(待确认列表):
vs Pub/Sub:Pub/Sub 在 Consumer 掉线时消息直接丢失;Stream 把消息持久化在 PEL 里,Consumer 重连后用 XREADGROUP STREAMS x 0 能拿回未 ACK 消息,超时还能用 XCLAIM 转给别的 worker 接管。这就是真正的消息队列语义。