
🎯 学实时库存系统的架构设计:不是简单的"减库存",而是从 Redis 原子操作、数据库最终一致性、库存预占与释放、超卖防护到大规模秒杀的多层库存架构。 ⏱️ 预计时间:40 分钟
[!tip] 💡 白话小课堂 库存系统的本质矛盾:卖出去的不能多于仓库里有的,但在100万人同时抢的时候又不能让系统卡死。解决之道是"分层"——Redis负责极速的原子扣减(告诉你抢没抢到),数据库负责最终真相(财务对账用),仓库系统负责物理库存(货真价实在哪)。每一层各司其职,互为兜底。
[!warning] ⚠️ 避坑指南 最大的坑是"用数据库行锁做秒杀库存"——1000人同时执行SELECT FOR UPDATE,连接池瞬间耗尽,整个平台挂掉。记住:高并发扣减一定用Redis DECR(原子操作),数据库只做异步持久化和最终对账。另一个坑:如果Redis崩溃,DECR回滚可能丢失——所以还需要Kafka事件作为最终兜底。
[!info] 🔑 核心秘诀 五层超卖防护的每一层都是"万一上一层失效,我来兜底":Redis DECR原子检查 → 数据库CHECK约束 → 定时对账 → 预占超时释放 → 缓存击穿防护。这种"纵深防御"思路不只在库存系统有用——支付、优惠券、任何有限资源扣减都该这么设计。
[!quote]- 📖 课程来源 本课为系统设计大师课,内容综合自真实电商工程实践经验,而非单一 Skill 文件。涵盖架构设计、分布式系统、团队管理等高级工程主题。
1. 库存系统的核心矛盾
库存的三角困境
库存系统同时面对三个互相矛盾的需求:
一致性(Consistency)
→ 库存不能超卖(卖了 101 件,实际只有 100 件) → 用户付了钱必须拿到货 → 取消订单后库存必须正确恢复
可用性(Availability)
→ 秒杀时 10000 QPS 不能挂 → 用户看商品详情页时库存不能延迟 > 100ms → 大促期间不能因为库存服务拖垮整个平台
实时性(Real-time)
→ 最后一个库存被抢走 → 立即对所有人显示"售罄" → 退货入库 → 立刻可以再次销售 → 多仓库存 → 聚合查询要准且快
传统 RDBMS 的 SELECT FOR UPDATE 在这三个维度上都崩溃: → 行锁导致并发性能断崖式下降 → 主从延迟导致读到旧库存 → 分布式事务的性能开销不可接受
2. 分层库存架构
四层库存模型
Layer 1: 缓存层(Redis)—— 热数据,高并发
→ 用途:商品详情页库存展示、购物车库存校验 → 数据结构:Hash {sku} → {available, reserved, total} → 更新方式:DECR/HINCRBY 原子操作 → TTL:无(永久),但允许短暂不一致(< 1s) → 一致性:最终一致性(从 DB 异步同步)
Layer 2: 预售层(Redis)—— 下单时扣减
→ 用途:下单时预占库存,防止超卖 → 操作:DECR stock:{sku} → 返回值 >= 0 才允许下单 → 关键:Redis DECR 是原子操作,天然防止超卖 → 超时释放:15 分钟未支付 → INCR 恢复库存
Layer 3: 持久层(PostgreSQL / MySQL)—— 真实库存
→ 用途:财务对账、仓库管理、历史追溯 → 更新:异步消费 Kafka 事件 → 批量更新 → 一致性:强一致(ACID 事务) → 策略:WAL(Write-Ahead Log)+ 幂等消费
Layer 4: 仓库层(WMS)—— 物理库存
→ 用途:与实际仓库系统同步 → 更新:定时同步(每 5 分钟或实时 Webhook) → 校验:定期对账(发现差异 > 阈值 → 告警)
3. 超卖防护机制
五层防护网
防护 1:Redis DECR 原子检查
→ DECR stock:{sku} → if result < 0: INCR stock:{sku}(回滚);返回 "售罄" → 保证:Redis 单线程模型 + DECR 原子性 = 不可能超卖
防护 2:数据库唯一约束
→ 订单表:(sku, order_id) UNIQUE → 库存日志表:CHECK (available >= 0) → 双重保险:即使 Redis 出问题,DB 层面兜底
防护 3:定时对账
→ 每 5 分钟:SUM(Redis available) vs DB available vs WMS available → 差异 > 阈值 → 告警 + 自动修复(以 DB 为准修正 Redis) → 差异 > 严重阈值 → 人工介入 + 暂停销售
防护 4:库存预占超时释放
→ 下单后 15 分钟未支付 → Timer 触发释放 → 释放流程:INCR stock:{sku} + 取消订单事件 → 防止:恶意占库存不付款
防护 5:缓存击穿防护
→ 热点 SKU 用分布式锁(Redlock)保护 DB → 非热点 SKU 用互斥锁 + 短期缓存 → 防止:缓存失效瞬间所有请求打到 DB
4. 电商案例:茅台秒杀——100万并发下的库存一致性
🛒 实战案例:某酒类电商平台策划了一场茅台1499元限时秒杀活动,库存仅500瓶,但预热期已有120万用户预约。技术团队需要保证:100万并发下库存不超卖、不超时扣减、不丢订单。历史教训:去年五粮液秒杀因MySQL行锁死锁,导致超卖37瓶,直接损失5.5万元+品牌声誉受损。
用 gstack 架构体系设计库存系统:
- /review 审查现有库存扣减逻辑 → 发现基于MySQL行锁(SELECT FOR UPDATE)的减库存在实测1000并发下死锁率高达37%,连接池瞬间耗尽
- /architect 设计新方案:Redis Lua脚本原子扣减(DECR库存→检查结果→若<0则INCR回滚)+ Kafka异步落库(FlashSaleOrderCreated事件持久化)+ 定时对账任务(每30秒比对Redis与DB库存差异)
- /cso 安全审计发现CRITICAL:Lua脚本中DECR成功后若Redis崩溃→INCR回滚丢失→引入Kafka事件作为最终兜底(即使Redis数据丢失,可从Kafka事件重建库存状态)
- /qa 生成压测脚本模拟100万并发请求(JMeter分布式集群,50台施压机)→ 验证库存最终=0且订单数=500
- /deploy 灰度发布新库存服务(先10%流量观察15分钟→P99延迟无劣化→50%→100%)
效果:
秒杀期间QPS峰值达87万,P99延迟<50ms(MySQL方案在1000并发时P99已达4.2s)
库存扣减100%准确:0超卖(最终订单数=500)、0少卖(库存最终=0)
订单落库延迟<3秒(异步写入Kafka+消费者批量写入PostgreSQL)
活动结束后与支付宝对账:¥749,500分毫不差(500瓶×1499元)
15分钟未支付自动释放:23笔订单超时释放,库存INCR回补后被排队用户成功抢到
🔑 启示:电商库存系统的核心不是"怎么减库存",而是"减完怎么确认所有人都同意这个结果"。Redis负责速度(O(1)原子操作),Kafka负责可靠(事件持久化不丢),对账负责最终正确(每30秒Redis vs DB差异自动修复)——三者缺一不可。纵深防御的工程哲学:不信任任何单一层级的正确性,每一层都是上一层失效时的安全网。
5. 动手实践
- 用 Redis 实现一个库存扣减 Demo:写一个 Lua 脚本,执行 DECR 操作并检查返回值(>= 0 扣减成功,< 0 库存不足)。测试 100 个并发请求,验证不超卖。
- 设计你的库存四层模型:为你的项目(或一个假设的电商平台)画出 Redis 预检层 → Redis 扣减层 → DB 持久层 → 对账层的架构图,标注每层的超时和降级策略。
- 模拟一次库存不一致场景:关闭 DB 持久化,让 Redis 成功扣减但 DB 写入失败,然后设计一个对账修复流程来发现和修复这个差异。
[!tip] 提示:库存系统的核心挑战不是"正常情况下的正确性",而是"各种异常情况下依然正确"。重点测试:Redis 崩溃、网络分区、重复请求、并发竞态。
6. 掌握检验
Q1:库存系统的"三角困境"中,传统 SELECT FOR UPDATE 方案在一致性、可用性、实时性三个维度上都崩溃——请指出它在每个维度上分别是怎么崩溃的?
- A) 一致性:行锁导致超卖;可用性:主从延迟导致读到旧库存;实时性:行锁排队导致响应缓慢
- B) 一致性:无法防止超卖;可用性:行锁导致并发性能断崖式下降、连接池耗尽;实时性:主从延迟导致用户看到旧库存
- C) 一致性:分布式事务正常;可用性:读写分离完美解决;实时性:缓存解决延迟问题
- D) 一致性:最终一致即可;可用性:水平扩展解决;实时性:异步更新解决
Q2:四层库存模型(缓存层/预售层/持久层/仓库层)中,每一层的数据一致性保证是什么?如果第二层(Redis 预售层)的数据与第三层(PostgreSQL 持久层)出现不一致,系统如何发现和修复?
Q3:以下 Redis Lua 脚本在库存扣减中扮演什么角色?DECR stock:{sku} 后 if result < 0 then INCR stock:{sku}; return -1 end——为什么不能把 DECR 和 if 判断分成两次独立的 Redis 调用?
- A) 脚本没有特殊作用,只是为了代码复用
- B) Lua 脚本保证 DECR 和条件判断/JNCR 回滚在 Redis 单线程中原子执行——分两次调用会在 DECR 和 INCR 之间产生竞态窗口,导致库存计数错误
- C) 分两次调用性能更好,但代码更难维护
- D) Lua 脚本只是为了减少网络往返
Q4:五层超卖防护(Redis DECR 原子检查 / 数据库 CHECK 约束 / 定时对账 / 预占超时释放 / 缓存击穿防护)是一种"纵深防御"设计。请解释"纵深防御"的工程设计思想——为什么需要五层而不是一层?如果第一层(Redis DECR)已经能保证不超卖,后面四层为什么还存在?
Q5:库存预占超时释放(15 分钟未支付自动释放)解决了什么业务问题?如果这个机制失效(Timer 服务挂了),哪些层会发现和修复这个问题?
Q6:gstack 的 /cso 安全审计在这个案例中发现了什么 CRITICAL 风险?这个风险暴露了单靠 Redis(即使有 Lua 原子脚本)有什么局限?
7. 答案
点击查看答案
Q1:B — 一致性:SELECT FOR UPDATE 行锁不能保证分布式环境下的超卖防护(多个副本、主从延迟);可用性:行锁是排他锁,1000 并发同时扣减同一行时,第 1000 个请求等待前 999 个完成,连接池瞬间耗尽,整个平台挂掉;实时性:主从复制延迟导致读副本上看到的库存与主库不一致,用户看到"有货"但实际已售罄。
Q2:缓存层(Redis 热数据):最终一致性,从 DB 异步同步,允许短暂不一致(< 1s);预售层(Redis 扣减层):原子操作保证扣减正确,但允许 Redis 崩溃时数据丢失;持久层(PostgreSQL):强一致性(ACID 事务),通过消费 Kafka 事件异步更新;仓库层(WMS):物理库存真实数据,定时或 Webhook 同步。不一致发现:定时对账(每 5 分钟 SUM Redis available vs DB available vs WMS available)→ 差异超过阈值告警。修复:以数据库为标准修正 Redis(从 DB 重新同步库存数据到 Redis)。
Q3:B — Redis 是单线程事件循环模型,Lua 脚本在执行期间独占 Redis,其他命令无法插入。DECR 后检查返回值然后决定是否 INCR 回滚这个逻辑如果分成两次独立调用:客户端 A 执行 DECR 得到 -1 → 在客户端 A 调用 INCR 之前,客户端 B 也执行了 DECR 得到 -2 → 客户端 A 调用 INCR 恢复到 -1 → 客户端 B 调用 INCR 恢复到 0。结果:本应 -2 的库存变成了 0,两个本该失败的请求都"成功"了。Lua 脚本的原子性消除了这个竞态窗口。
Q4:纵深防御(Defense in Depth)是假设每一层都可能失效,用多层兜底来保证最终安全。第 1 层 DECR 原子检查是主要防线——正常情况 99.999% 有效。第 2-5 层存在的原因:第 1 层可能因 Redis 崩溃导致 DECR 回滚(INCR)丢失(如 /cso 发现的风险);第 2 层 DB CHECK 约束在持久化时再次校验;第 3 层定时对账发现任何层级的累积偏差;第 4 层预占释放防止恶意占库存;第 5 层缓存击穿防护防止 Redis 挂掉后 DB 被打垮。每层都是一个"安全网",大多数情况下不需要后面的网起作用——但一旦需要,它们就是避免灾难的最后防线。
Q5:解决的是"恶意占库存"问题——用户下单(扣减了库存),但故意不支付,导致库存被锁死、真正想买的用户买不到。如果 Timer 服务挂了:第 3 层定时对账会发现"Redis 中 reserved 数量异常高 + 订单表中大量 PENDING_PAYMENT 超时订单"→ 触发告警和自动释放;第 4 层本身就是预占超时释放,Timer 故障意味着这层直接失效;但第 3 层(对账)作为兜底,会在 5 分钟对账窗口内发现异常。
Q6:/cso 发现 CRITICAL:Lua 脚本中 DECR 成功扣减后如果 Redis 突然崩溃,INCR 回滚可能丢失——Redis AOF/RDB 持久化间隔内可能丢失数据。这暴露了单靠 Redis 的局限:Redis 是内存数据库,崩溃可能丢失最近的数据变更。修复方案:引入 Kafka 事件作为最终一致性兜底——即使 Redis 数据丢失,Kafka 中的订单创建事件可以重建库存状态。这体现了"不依赖单一组件保证正确性"的架构原则。
(5/6 通过)
延伸阅读
- [[L7-02-事件驱动架构|上一课:事件驱动架构 —— 用"发生的事情"而不是"调用链"来设计系统]]
- [[L7-04-支付链路可靠性|下一课:支付链路可靠性 —— 每笔订单都像银行转账一样分毫不差]]
- [[L6-23-性能基准测试|相关:性能回归检测]]

评论区
共 0 条评论