第4章 原子性与消息传递 章节摘要:本章跟着一个抢购脚本走。多个客户端并发修改同一批键,谁保证中间状态不被别人看见?答案依次是事务队列、Lua 脚本、订阅通知——三种不同粒度的原子与解耦方案,各自对应一类并发问题。 一条主线 秒杀场景:库存 100 件,五千个请求同一毫秒到达。判断库存、扣减、记录买家三步必须整体执行,否则超卖。本章顺着"怎么把多步焊成一步"展开:先看 MULTI 事务能焊到什么程度(它不回滚,能干什么不能干什么要说清),再看 Lua 脚本这个终极方案(判断逻辑进脚本,条件原子化),最后跳出抢购看发布订阅——问题从"互斥"变成"通知"。 沿途站点 4.1 事务:命令入队、EXEC 一次执行,队列机制与 WATCH 乐观锁。 4.
章节摘要:本章跟着一个抢购脚本走。多个客户端并发修改同一批键,谁保证中间状态不被别人看见?答案依次是事务队列、Lua 脚本、订阅通知——三种不同粒度的原子与解耦方案,各自对应一类并发问题。
秒杀场景:库存 100 件,五千个请求同一毫秒到达。判断库存、扣减、记录买家三步必须整体执行,否则超卖。本章顺着"怎么把多步焊成一步"展开:先看 MULTI 事务能焊到什么程度(它不回滚,能干什么不能干什么要说清),再看 Lua 脚本这个终极方案(判断逻辑进脚本,条件原子化),最后跳出抢购看发布订阅——问题从"互斥"变成"通知"。
三者的能力边界:
两个转折点。第一个在 4.1:Redis 事务没有回滚,执行期出错前面的命令照样生效——这与关系型数据库的直觉相反,理解它才能用对 WATCH。第二个在 4.2:Lua 不是"高级事务",而是"把业务逻辑搬进服务端执行",脚本慢就是全局慢,原子性的代价是共享了那唯一的线程。
三种工具对应的故障现场,先在脑子里挂上号:
| 现象 | 根因层 | 对的工具 |
|---|---|---|
| 两步写被并发插队,出现脏数据 | 原子性缺口 | MULTI/EXEC 或 Lua |
| 先读后写,判断依据已过期 | 读写到写之间的窗口 | Lua(判断入脚本) |
| 抢锁解锁,删了别人的锁 | 释放条件不原子 | Lua 对比再删 |
| 一份数据要通知多个在线方 | 通知而非任务 | 发布订阅 |
| 任务不能丢、可重试、多下游独立消费 | 可靠投递 | Stream |
这张表也是排错时的分诊台:同一个"数据不对了"工单,按表现定位到行,工具与章节就都出来了。
学本章时有个容易走偏的倾向要先拦住:不是所有并发问题都要上重型工具。多数"写写冲突"其实靠好的键设计就能消掉——把共享的一个计数键拆成按请求方隔离的多个键,冲突自然消失;真正需要原子语义的是"多键必须保持一致"或"读后写不可分"的场景。先问能不能改设计,再问用哪把锁,这个顺序能省掉一半的复杂度。
本章的动手主线是同一个秒杀场景的三轮改造:第一轮用 pipeline 裸写(制造超卖)、第二轮上 WATCH 事务(压住低并发、重试风暴现形)、第三轮进 Lua(彻底焊死)。三轮跑下来,三种工具的能力边界不是背下来的,是被数据打出来的。
原子性管住了"写与写"的竞争,但内存不是无限的。第 5 章讲键的死亡:过期怎么删、内存满了淘汰谁——那是另一套精巧的结构与算法。