电商系统设计(三):库存系统
电商系统设计系列(篇次与(一)推荐阅读顺序一致)
本文是电商系统设计系列的第三篇,聚焦库存系统的设计与实现。
一、背景与挑战
1.1 多品类库存差异
在数字电商/本地生活平台中,不同品类的库存特性差异极大:
| 品类 | 库存特点 | 扣减时机 | 典型示例 |
|---|---|---|---|
| 电子券 (Deal) | 券码制,每个券码唯一 | 下单预订 | 星巴克电子券 |
| 虚拟服务券 (OPV) | 数量制,分平台统计 | 下单预订 | 美甲/按摩服务券 |
| 酒店 | 时间维度,按日期管理 | 支付成功 | Agoda 酒店房间 |
| 机票/票务 | 座位/场次制 | 支付成功 | 航班座位、电影票 |
| 礼品卡 (Giftcard) | 实时生成或预采购卡密 | 支付成功 | Google Play 充值卡 |
| 话费充值 (TopUp) | 无限库存 | 无需扣减 | 手机话费 |
| 本地生活套餐 | 组合型,多子项联动 | 下单预订 | 火锅双人套餐 |
1.2 核心痛点
- 模型割裂:每个品类独立设计库存逻辑,无法复用。
- 数据不一致:Redis 与 MySQL 之间、预订数量 (booking) 与实际状态脱节。
- 供应商策略不统一:有的实时查询,有的定时同步,有的无需管理。
- 缺乏统一服务:各业务方直接操作 DB/Redis,维护成本高。
- 监控缺失:超卖、库存差异、供应商同步延迟难以发现。
1.3 设计目标
| 目标 | 说明 | 优先级 |
|---|---|---|
| 统一模型 | 多品类共用一套库存模型 | P0 |
| 高性能 | 支持万级 QPS 秒杀场景 | P0 |
| 灵活扩展 | 新品类接入无需修改核心代码 | P0 |
| 最终一致 | Redis 与 MySQL 数据最终一致 | P0 |
| 供应商集成 | 支持实时/定时/推送多种同步策略 | P1 |
二、库存分类体系
2.1 两个核心维度
设计统一库存模型的关键是将所有品类抽象为 两个正交维度:
维度一:谁管库存?(Management Type)
1 | const ( |
维度二:库存长什么样?(Unit Type)
1 | const ( |
2.2 品类分类矩阵
| 品类 | 管理类型 | 单元类型 | 扣减时机 |
|---|---|---|---|
| 电子券 (Deal) | Self | Code | 下单 |
| 虚拟服务券 (OPV) | Self | Quantity | 下单 |
| 本地服务 | Self | Quantity | 下单 |
| 酒店 | Supplier | Time | 支付 |
| 机票 | Supplier | Quantity | 支付 |
| 话费充值 | Unlimited | - | 无 |
| 礼品卡(预采购) | Self | Code | 下单 |
| 礼品卡(实时生成) | Supplier | Code | 支付 |
| 套餐组合 | Self | Bundle | 下单 |
核心洞察:任何新品类接入时,只需确定它属于哪个
(ManagementType, UnitType)组合,即可复用对应的库存策略,无需修改核心代码。
2.3 商品库存 vs 营销库存
在实际电商场景中,除了上述的商品库存(Product Inventory)外,还存在营销库存(Campaign Inventory)的概念。两者服务于不同的业务目标,需要独立设计:
| 维度 | 商品库存 | 营销库存 |
|---|---|---|
| 本质 | 实物/虚拟商品的可售数量 | 营销活动的参与配额 |
| 驱动因素 | 采购成本、仓储能力 | 营销预算、活动目标 |
| 管理维度 | SKU、仓库、批次 | 活动 ID、SKU、用户、时段 |
| 典型场景 | 正常售卖、预售、拼团 | 秒杀、优惠券、限时折扣 |
| 扣减单位 | 实际商品数量 | 活动参与次数/名额 |
| 补充策略 | 采购补货 | 预算追加、时段分配 |
| 约束类型 | 硬约束(卖完即止) | 软约束(可超卖或追加) |
| 用户维度 | 无 | 单用户限购、防刷 |
关键差异点:
- **商品库存关注”有没有货”**,营销库存关注”能不能参与活动”
- 商品库存扣减不可逆(售出即消耗),营销库存可动态调整(追加预算)
- 营销库存需要多维度管控:
- 总配额控制(活动总预算)
- 单用户限额(防刷)
- 时段配额(错峰)
- SKU 维度配额(指定商品参与)
实际案例:
某商品 SKU 有 10,000 件商品库存,但参与秒杀活动时只开放 500 件营销库存:
- 商品库存 = 10,000:正常售卖可买 10,000 件
- 营销库存 = 500:秒杀活动只能买 500 件
- 扣减规则:秒杀下单时需要同时扣减商品库存和营销库存,任一不足则失败
三、统一数据模型(商品库存)
3.1 库存配置表(inventory_config)
每个 SKU 一条配置,决定该商品使用哪种库存策略:
1 | CREATE TABLE inventory_config ( |
3.2 核心库存表(inventory)
所有品类共用一张库存表,通过不同字段组合适配不同场景:
1 | CREATE TABLE inventory ( |
库存恒等式:
1 | total_stock = available_stock + booking_stock + locked_stock + sold_stock |
可售库存计算(不同管理类型计算方式不同):
1 | func CalcAvailable(inv *Inventory, cfg *Config) int32 { |
3.3 券码池表(inventory_code_pool,分 100 张表)
仅用于券码制商品(Deal、Giftcard 预采购模式):
1 | CREATE TABLE inventory_code_pool_00 ( |
3.4 库存操作日志表(inventory_operation_log)
所有库存变更留痕,用于对账和审计:
1 | CREATE TABLE inventory_operation_log ( |
四、策略模式:核心架构(商品库存)
4.1 整体架构
1 | ┌─────────────────────────────────────────────────┐ |
4.2 策略接口定义
1 | // InventoryStrategy 库存管理策略接口 |
五、自管理策略:券码制(Deal / Giftcard)
5.1 Redis 存储结构
1 | Key: inventory:code:pool:{itemID}:{skuID}:{batchID} |
5.2 核心流程:出货 + 补货
1 | 用户下单 |
出货 Lua 脚本(原子性保证):
1 | -- 原子取出 N 个券码 |
补货流程(加分布式锁防并发):
1 | func (s *SelfManagedStrategy) replenish(ctx context.Context, itemID, skuID, batchID uint64) error { |
六、自管理策略:数量制(OPV / 本地服务)
6.1 Redis 存储结构
1 | Key: inventory:qty:stock:{itemID}:{skuID} |
6.2 预订 Lua 脚本
1 | local key = KEYS[1] |
6.3 支付成功 / 取消订单
1 | -- 支付成功:booking → issued |
七、供应商管理策略(酒店 / 机票)
7.1 同步策略
| 策略 | 适用场景 | 实时性 | 实现方式 |
|---|---|---|---|
| 实时查询 | 库存变化快(机票) | 高 | 每次请求调供应商 API(30s 缓存) |
| 定时同步 | 库存变化中等(酒店) | 中 | 定时任务每 5 分钟拉取 |
| Webhook | 供应商主动推送 | 高 | 接收推送更新本地缓存 |
7.2 实时查询流程
1 | func (s *SupplierManagedStrategy) CheckStock(ctx context.Context, req *CheckStockReq) (*CheckStockResp, error) { |
7.3 预订流程(供应商管理)
7.3.1 同步预订(理想情况)
供应商 API 质量好,预订接口同步返回结果:
1 | func (s *SupplierManagedStrategy) BookStock(ctx context.Context, req *BookStockReq) (*BookStockResp, error) { |
7.3.2 异步预订(供应商系统较差)
场景:部分供应商系统不稳定,预订流程为:
- 创建 booking 单 → 立即返回
booking_id(状态PENDING) - 轮询查询 booking 状态 → 最终返回
CONFIRMED/FAILED - 只有
CONFIRMED后才能继续下单
挑战:
- 用户不能等待轮询完成(可能需要 10-30 秒)。
- 需要异步处理 + 状态机 + 补偿机制。
状态机设计:
1 | 用户下单 |
数据库表设计:
1 | CREATE TABLE supplier_booking ( |
实现流程:
1 | // 1. 用户下单时:创建 booking 单,立即返回"处理中" |
7.3.3 用户体验优化
问题:用户下单后看到”预订处理中”,体验不佳。
优化方案:
- 前端轮询展示进度:
1 | // 用户下单后,前端每 2 秒轮询订单状态 |
- WebSocket / SSE 推送:
1 | // 服务端:booking 确认后推送消息 |
- 短信/Push 通知:
1 | // 预订成功后 1 分钟内发送通知 |
7.3.4 异常场景处理
场景 1:轮询期间用户取消订单
1 | func PollBookingStatus(task *BookingPollTask) { |
场景 2:供应商 API 持续超时
1 | // 连续 3 次查询超时 → 降级到人工处理 |
场景 3:供应商确认后用户未支付
1 | // booking 成功后设置 15 分钟支付超时 |
7.3.5 监控指标
| 指标 | 阈值 | 说明 |
|---|---|---|
| booking 成功率 | > 95% | 供应商库存准确性 |
| 平均确认时长 | < 10s | P99 < 30s |
| 超时率 | < 1% | 需要人工介入的比例 |
| 取消率 | < 5% | 用户等待期间取消订单 |
1 | // Prometheus Metrics |
八、无限库存策略(TopUp / 保险)
最简单的策略,只记录操作日志:
1 | type UnlimitedStrategy struct{} |
九、核心流程汇总
9.1 统一预订流程
1 | 用户下单 |
9.2 支付成功流程
1 | 支付回调 |
9.3 取消/超时释放流程
1 | 订单取消 / 超时未支付 |
十、库存系统与订单系统的交互
本章详细说明库存系统(主要以商品库存为例)如何与订单系统协作完成下单、支付、取消等核心流程。
说明:本章涉及商品库存和营销库存的协同,其中营销库存的详细设计请参阅电商系统设计(四):营销系统第5章。本章重点展示系统交互边界和事务协调模式。
10.1 交互边界设计原则
核心原则:
单一职责:
- 订单系统:负责订单生命周期管理(创建、支付、取消、退款)
- 库存系统:负责库存数据管理和原子操作(校验、预扣、确认、释放)
- 边界清晰:订单系统不直接操作库存数据,库存系统不关心订单业务逻辑
幂等性保障:
- 所有库存操作以
order_id为幂等键 - 重复调用返回相同结果,避免重复扣减
- 所有库存操作以
超时管理:
- 预扣操作带 TTL(通常 15 分钟)
- 超时自动释放,无需订单系统主动清理
最小依赖:
- 库存系统故障不阻塞订单创建(降级方案)
- 订单系统故障不影响库存同步(事件驱动)
10.2 方案选择:混合模式(A + B)
根据业务场景选择不同的交互模式:
| 场景 | 模式 | 特点 | 原因 |
|---|---|---|---|
| 常规下单 | 方案 B(订单编排) | 订单系统调用库存系统多个原子 API | 流程透明,易调试,灵活性高 |
| 秒杀/大促 | 方案 A(库存主导) | 库存系统提供聚合 API,一次调用完成双重扣减 | 减少网络开销,集中限流防护 |
| 异步通知 | 方案 C(事件驱动) | 通过 Kafka 事件解耦 | 非关键路径,降低耦合 |
10.3 常规下单流程(方案 B:订单编排)
10.3.1 整体流程
sequenceDiagram
participant User as 用户
participant Order as 订单系统
participant ProdInv as 商品库存服务
participant CampInv as 营销库存服务
participant Payment as 支付系统
User->>Order: 1. 创建订单
Order->>ProdInv: 2. CheckStock(sku, qty)
ProdInv-->>Order: available=yes
Order->>CampInv: 3. CheckQuota(campaign, user, sku)
CampInv-->>Order: quota_ok=yes
Note over Order: Saga 开始
Order->>ProdInv: 4. ReserveStock(order_id, sku, qty, ttl=15min)
ProdInv-->>Order: reserved
Order->>CampInv: 5. ReserveQuota(order_id, campaign, user, qty)
CampInv-->>Order: reserved
Order->>Order: 6. CreateOrderRecord()
Order-->>User: 订单创建成功,请支付
alt 支付成功
User->>Payment: 7a. 支付
Payment-->>Order: PaymentCallback
Order->>ProdInv: 8a. ConfirmStock(order_id)
Order->>CampInv: 8a. ConsumeQuota(order_id)
Order-->>User: 订单完成
else 支付超时/取消
Note over Order: 15分钟后或用户取消
Order->>ProdInv: 8b. ReleaseStock(order_id)
Order->>CampInv: 8b. ReleaseQuota(order_id)
Order-->>User: 订单已取消
end
10.3.2 订单系统 Saga 实现
1 | type OrderSaga struct { |
10.3.3 Saga 状态机
1 | type SagaState int |
10.3.4 支付成功处理
1 | func (s *OrderSaga) HandlePaymentSuccess(ctx context.Context, orderID uint64) error { |
10.3.5 超时/取消处理
1 | func (s *OrderSaga) HandleOrderTimeout(ctx context.Context, orderID uint64) error { |
10.4 秒杀/大促流程(方案 A:库存主导)
10.4.1 Facade 服务设计
为秒杀场景设计专门的聚合服务,提供高性能 API:
1 | type FlashSaleInventoryFacade struct { |
10.4.2 Confirm 聚合 API
1 | func (f *FlashSaleInventoryFacade) Confirm(ctx context.Context, req *ConfirmReq) error { |
10.4.3 Cancel 聚合 API
1 | func (f *FlashSaleInventoryFacade) Cancel(ctx context.Context, req *CancelReq) error { |
10.5 关键操作详解
10.5.1 校验库存 vs 预扣库存
为什么需要两步?
1 | // ❌ 错误做法:只校验不预扣 |
CheckStock 的使用场景:
- 快速失败:在用户点击”立即购买”前,前端调用 CheckStock 提前提示
- 降级方案:ReserveStock 失败时,CheckStock 可用于兜底
- 只读查询:不改变状态,可以高频调用
10.5.2 预扣超时管理
三种超时机制:
1 | // 1. Redis TTL 自动过期 |
推荐方案:Redis TTL + 延时队列
- Redis TTL 作为第一道防线(快速释放)
- 延时队列作为兜底(防止 Redis 数据丢失)
10.5.3 幂等性保障
所有库存操作都以 order_id 为幂等键:
1 | func ReserveStock(ctx context.Context, req *ReserveStockReq) (*ReserveStockResp, error) { |
10.5.4 库存服务降级
当库存服务故障时,订单系统的降级策略:
1 | func (s *OrderSaga) CreateOrderSaga(ctx context.Context, req *CreateOrderReq) (*CreateOrderResp, error) { |
十一、数据一致性保障
10.1 Redis 与 MySQL 双写策略
| 操作 | Redis | MySQL | 一致性保障 |
|---|---|---|---|
| 预订 (Book) | 同步扣减(Lua 原子) | Kafka 异步更新 | 最终一致 |
| 支付 (Sell) | 同步更新 | Kafka 异步更新 | 最终一致 |
| 营销锁定 (Lock) | 同步 | 同步(DB 事务) | 强一致 |
| 补货 (Replenish) | 同步写入 | 不变 | - |
核心原则:
- Redis 是热路径:所有高频读写走 Redis,保证毫秒级响应。
- MySQL 是权威数据源:故障恢复以 MySQL 为准。
- Kafka 异步持久化:Book/Sell 等操作通过 MQ 异步落库,不阻塞主流程。
10.2 定时对账(每小时)
1 | func Reconcile() { |
10.3 降级方案
1 | Redis 可用 → 正常读写 Redis |
11.4 营销库存一致性保障
11.4.1 Redis-MySQL 双写策略
与商品库存类似,营销库存也采用 Redis 热路径 + MySQL 持久化的架构:
1 | func (s *CampaignInventoryService) ReserveQuota(ctx context.Context, req *ReserveQuotaReq) error { |
Kafka Consumer 批量写入:
1 | func (c *CampaignInventoryConsumer) Consume(ctx context.Context, messages []*kafka.Message) error { |
11.4.2 定时对账
1 | func ReconcileCampaignInventory() { |
11.5 跨库存类型一致性
秒杀场景需要同时扣减商品库存和营销库存,如何保证两者的一致性?
11.5.1 问题场景
1 | 用户下单秒杀商品: |
11.5.2 Saga 模式协调
1 | func (s *OrderSaga) CreateFlashSaleOrder(ctx context.Context, req *CreateOrderReq) error { |
11.5.3 分布式锁兜底
对于极端场景(如补偿失败),使用分布式锁保证最终一致性:
1 | func (s *OrderSaga) compensateWithLock(ctx context.Context, orderID uint64) error { |
11.5.4 对账修复
每小时对账时,检查两种库存的一致性:
1 | func ReconcileOrderInventory() { |
十二、Kafka 事件设计
1 | message InventoryEvent { |
Topic 设计:
inventory.book— 预订inventory.unbook— 释放inventory.sell— 售出inventory.refund— 退款inventory.sync— 供应商同步
十三、Giftcard 特殊设计
Giftcard 横跨三种库存模式,是统一模型的最佳验证:
| 模式 | 管理类型 | 流程 | 适用场景 |
|---|---|---|---|
| 预采购卡密 | Self + Code | 批量导入 → Redis 出货 | 高频热销卡 |
| 实时生成 | Supplier + Code | 支付成功 → 调 API 生成 → 存入 code_pool | 长尾低频卡 |
| 无限库存 | Unlimited | 直接成功 | 供应商保证库存 |
卡密安全:
- 存储时 AES-256 加密卡号和 PIN。
- 管理后台脱敏显示(
XXXX-XXXX-XXXX-1234)。 - 所有访问记录审计日志。
供应商 API 超时处理:
- 支付成功后异步生成,完成后推送通知用户。
- 指数退避重试(1s, 2s, 4s),3 次失败后人工补发。
十四、监控与告警
14.1 关键指标
| 指标 | 阈值 | 告警级别 |
|---|---|---|
| 超卖次数 | > 0 | P0 |
| Redis vs MySQL 差异 | > 100 | P1 |
| 库存服务错误率 | > 1% | P1 |
| 库存扣减 P99 | > 200ms | P2 |
| 补货失败率 | > 5% | P2 |
| 供应商同步延迟 | > 10min | P2 |
| 低库存商品数 | > 100 | P3 |
13.2 Prometheus Metrics
1 | # 操作计数 |
13.4 跨系统监控
13.4.1 库存一致性监控
监控商品库存和营销库存的协同状态:
| 指标 | 含义 | 阈值 | 告警级别 |
|---|---|---|---|
| 双扣成功率 | 商品+营销双重扣减成功比例 | < 98% | P1 |
| 补偿执行率 | Saga 补偿成功率 | < 99% | P1 |
| 库存状态不一致订单数 | 商品已扣但营销未扣(或反之) | > 10 | P0 |
| 对账修复次数 | 每小时对账发现并修复的差异数 | > 50 | P2 |
Prometheus Metrics:
1 | # 双扣成功率 |
13.4.2 订单与库存交互监控
监控订单系统与库存系统的 API 调用:
1 | # 库存API调用次数 |
13.4.3 秒杀场景专项监控
1 | # 秒杀并发数 |
13.4.4 告警规则配置
1 | groups: |
十五、边界场景与容错
本章详细说明各种异常场景的处理策略。
15.1 商品库存充足但营销库存不足
场景:用户参与秒杀,商品库存有 1000 件,但营销配额只剩 0 件。
处理流程:
1 | func (s *OrderSaga) CreateFlashSaleOrder(ctx context.Context, req *CreateOrderReq) error { |
用户体验优化:
前端在用户点击”立即购买”前,先调用检查接口:
1 | func CheckFlashSaleEligibility(ctx context.Context, req *CheckReq) (*CheckResp, error) { |
15.2 预扣成功但支付超时
场景:用户下单后 15 分钟未支付,预扣的库存需要释放。
三重保障机制:
14.2.1 Redis TTL 自动过期
1 | func ReserveStock(orderID, sku, qty, ttl int) { |
14.2.2 延时队列触发释放
1 | func CreateOrder(ctx context.Context, req *CreateOrderReq) error { |
14.2.3 定时任务兜底
1 | func ReconcileTimeoutOrders() { |
15.3 Redis 宕机降级方案
14.3.1 降级策略
1 | type InventoryService struct { |
14.3.2 Redis 恢复后全量同步
1 | func RecoverFromDegradation() { |
15.4 超卖防护的最后一道防线
即使有以上所有保障,仍可能出现超卖(如 Redis 数据损坏、网络分区)。最后一道防线在支付成功确认时:
1 | func (s *InventoryService) ConfirmStock(ctx context.Context, req *ConfirmStockReq) error { |
十六、新品类接入指南
三步接入:
- 评估分类:确定
(ManagementType, UnitType, DeductTiming)。 - 写配置:在
inventory_config表插入一条记录。 - 调接口:使用统一
InventoryManager.BookStock()即可。
1 | // 示例:接入新品类"演唱会门票" |
十七、生产环境实战数据
17.1 业务规模
| 指标 | 数值 | 说明 |
|---|---|---|
| 秒杀峰值 QPS | 20,000 | 单个爆款商品,持续 5-10 分钟 |
| 日均 QPS | 50 | 常态流量 |
| 日均订单量 | 2,000,000 | 支付成功订单 |
| 日均库存扣减 | 6,700,000 | 含预订、支付、取消等操作 |
| 峰值/日均比 | 870:1 | 流量极度不均匀 |
容量规划推算:
1 | 日均订单 2M / 86400s ≈ 23 TPS |
17.2 集群配置
Redis 集群
1 | 拓扑: Redis Cluster (3 主 3 从) |
容量规划:
- 券码池:100 万张券码 × 8 字节 ≈ 8 MB(单商品)
- 热点商品预热:10 个商品 × 8MB = 80 MB
- 数量制商品:1 万个 SKU × 1 KB ≈ 10 MB
- 总计:**< 200 MB**(核心数据),32GB 绰绰有余
应用服务
1 | 实例数: 10 台 (Kubernetes Pod) |
为什么 10 台能抗 2w QPS?
- Redis 操作 RT < 5ms,单线程 QPS = 1000/5 = 200
- 500 线程 × 200 QPS = 100k QPS 理论上限(实际 2k QPS,留足余量)
MySQL 集群
1 | 架构: 1 主 2 从(半同步复制) |
容量规划:
- 券码池:1 亿张券码 × 500 字节 ≈ 50 GB(分 100 张表,单表 500 MB)
- inventory 表:10 万条记录 × 1 KB ≈ 100 MB
- operation_log:日增 670 万条 × 200 字节 ≈ 1.3 GB/天(保留 30 天 ≈ 40 GB)
Kafka 集群
1 | Broker: 3 台 |
吞吐量验证:
- 秒杀峰值写入 Kafka: 20k TPS × 500 字节 = 10 MB/s
- Kafka 单分区吞吐 > 50 MB/s,6 分区 = 300 MB/s 理论上限
- 实际使用 **< 5%**,非常充裕
17.3 性能指标实测
| 操作 | P50 | P99 | P999 | 备注 |
|---|---|---|---|---|
| 券码制预订 | 15ms | 50ms | 150ms | 含 Redis + MySQL 同步更新 |
| 数量制预订 | 8ms | 30ms | 100ms | 仅 Redis Lua 脚本 |
| 供应商库存查询 | 200ms | 500ms | 2s | 第三方 API,30s 缓存 |
| Redis 单次操作 | 1ms | 5ms | 10ms | LIST/HASH 操作 |
| MySQL 券码状态更新 | 10ms | 50ms | 200ms | 主库写入 |
| Kafka 异步消费延迟 | 50ms | 200ms | 1s | 非秒杀场景 |
秒杀场景优化后:
- 券码提前预热到 Redis(活动前 1 小时)
- P99 降至 30ms(无 DB 补货开销)
17.4 真实案例与优化
案例 1:秒杀 2w QPS 热点 Key 瓶颈
问题:
- 单个爆款商品,所有请求打到同一个 Redis Key。
- Redis 单线程模型,QPS 上限 10 万(理论值),但 网卡带宽 先打满。
- 实测单 Key 极限 5 万 QPS(1KB 数据 × 5w = 50 MB/s,接近千兆网卡上限)。
解决方案:
**本地缓存 (Caffeine)**:
- 应用层缓存库存数(非强一致,允许轻微超卖)。
- 本地缓存拦截 80% 读请求,Redis 只承担 4k QPS。
Key 分散(适用于读多写少):
- 将热点 Key 复制 10 份:
stock:item_123:0 ~ stock:item_123:9。 - 读请求随机路由,写请求同步更新所有副本。
- 将热点 Key 复制 10 份:
限流前置:
- 网关层按
item_id限流,单商品最大 2.5w QPS(留 20% 余量)。 - 超出部分直接返回”繁忙”,避免击穿 Redis。
- 网关层按
案例 2:券码补货锁超时
问题:
- 补货时加分布式锁(10s 超时),从 MySQL 查 3000 张券码。
- DB 慢查询导致补货耗时 12s,锁提前过期。
- 另一个进程拿到锁,重复补货,导致 券码重复出货。
根因:
- MySQL
inventory_code_pool_xx表数据量大(千万级),status=1索引选择性差。 - 执行计划走了全表扫描。
解决方案:
优化 SQL:
1
2
3
4
5
6
7-- 增加复合索引
KEY idx_item_status_id (item_id, status, id)
-- 查询改为游标分页
SELECT id FROM inventory_code_pool_xx
WHERE item_id=? AND status=1 AND id > ?
ORDER BY id LIMIT 3000耗时从 12s 降至 50ms。
锁续期:
- 补货时启动守护线程,每 5s 检查锁是否需要续期。
- 避免长事务导致锁过期。
异步补货:
- 检测库存低于阈值(1000 张)时,提前异步补货。
- 避免用户请求阻塞在补货逻辑。
案例 3:Kafka 消费积压
问题:
- 秒杀活动结束后,Kafka 积压 50 万条消息(2.5 万 QPS × 20s)。
- 6 个 Consumer 消费速度跟不上,MySQL 写入成为瓶颈。
瓶颈分析:
- Consumer 逐条更新 MySQL:
UPDATE inventory SET booking_stock = booking_stock + 1 - MySQL 单线程提交,TPS < 5000(主从半同步复制延迟)。
解决方案:
批量写入:
1
2
3// 攒批 100 条,批量 INSERT
INSERT INTO inventory_operation_log (item_id, operation_type, quantity, ...)
VALUES (?, ?, ?), (?, ?, ?), ... -- 100 rowsTPS 从 5k 提升至 8 万(提升 16 倍)。
降低一致性要求:
inventory_operation_log日志表改为异步从库写入。- 主库只更新
inventory核心表。
削峰:
- Kafka 设置
linger.ms=100ms,Producer 端攒批发送。 - 减少消息数量。
- Kafka 设置
案例 4:对账发现的典型问题
统计数据(3 个月):
- 对账次数:2160 次(每小时 1 次)
- 发现差异:87 次(4% 频率)
- 差异 > 100:3 次(严重)
主要根因:
| 原因 | 占比 | 说明 |
|---|---|---|
| Kafka 消费延迟 | 60% | 秒杀后消费积压,MySQL 未及时更新 |
| Redis 补货未同步 MySQL | 25% | 券码补货只更新 Redis,DB 未记录 |
| 人工后台操作 | 10% | 运营手动修改 DB 库存 |
| Redis 重启丢数据 | 5% | AOF 未及时刷盘(appendfsync everysec) |
优化措施:
- Kafka 消费延迟告警:lag > 1000 立即告警。
- Redis 补货同步:补货时同步更新 MySQL
total_stock。 - 后台操作审计:所有库存修改必须通过 API,禁止直接改 DB。
- Redis 持久化增强:改为
appendfsync always(性能下降 30%,换取强一致)。
15.5 成本分析
| 资源 | 配置 | 数量 | 月成本(美元) |
|---|---|---|---|
| Redis Cluster | 32GB × 6 节点 | 1 套 | $800 |
| MySQL | 64GB 主库 + 32GB × 2 从库 | 1 套 | $1,200 |
| 应用服务 | 4C8G Pod | 10 台 | $600 |
| Kafka | 8C16G Broker | 3 台 | $900 |
| 总计 | - | - | $3,500/月 |
日均订单成本:$3,500 / 2,000,000 = $0.00175/单(0.175 美分)
15.6 核心设计决策
| 决策 | 选择 | 原因 |
|---|---|---|
| 统一 vs 独立 | 统一模型 + 策略模式 | 复用逻辑,新品类零代码接入 |
| Redis vs MySQL | Redis 优先,MySQL 持久化 | 高并发性能 + 数据可靠 |
| 同步 vs 异步 | 扣减同步,落库异步 | 热路径极速,冷路径可靠 |
| 券码出货方式 | Lazy Loading(按需补货) | 节省内存,避免一次性加载全量 |
| 对账策略 | 每小时自动对账,MySQL 为准 | 兜底一致性 |
| 降级策略 | Redis 宕机切 MySQL | 性能下降 10 倍,但业务不中断 |
15.7 业界对比
| 维度 | 淘宝/京东 | Amazon | 本设计 |
|---|---|---|---|
| 库存单元 | SKU 数量 | ASIN + FBA | SKU + 批次/日期 |
| 扣减时机 | 下单预订 | 支付成功 | 可配置 |
| 虚拟商品 | 部分支持 | 完善 | 核心场景 |
| 时间维度 | 不支持 | 不支持 | 支持 |
| 券码管理 | 部分 | 完善 | 核心能力 |
| 供应商集成 | 少量 | FBA 模式 | 多策略 |
| 峰值 QPS | 100 万+ | 50 万+ | 2 万(中型平台) |
十八、总结与展望
18.1 核心设计总结
本文详细介绍了电商库存系统的完整设计,重点聚焦商品库存管理:
商品库存核心设计:
- 统一模型:
(ManagementType, UnitType)两个维度抽象所有品类(Deal、OPV、酒店、机票、TopUp等) - 策略模式:自管理(券码制/数量制)、供应商管理、无限库存三种策略独立实现
- 性能优化:Redis 热路径 + MySQL 持久化,券码补货按需加载
- 核心流程:预占(Book)→ 确认(Sell)→ 释放(Unbook)→ 退款(Refund)
系统交互设计:
- 订单集成:订单系统通过 Saga 模式编排库存扣减、支付、履约等步骤
- 超时处理:三重保障机制(Redis TTL + 延时队列 + 定时任务)确保预占超时自动释放
- 幂等性:使用
order_id作为幂等键,防止重复扣减
一致性保障:
- 双写策略:Redis 同步操作,MySQL 异步写入(通过 Kafka)
- 定时对账:每小时校验 Redis 与 MySQL 差异,自动修复不一致
- 最后防线:支付确认时 MySQL 二次校验,防止超卖
营销库存:
- 本文保留了商品库存与营销库存的对比(2.3 节),营销库存的详细设计请参阅电商系统设计(四):营销系统
18.2 适用场景建议
适合采用本设计的场景:
- 中大型电商平台:日订单量 > 10 万,有秒杀/大促需求
- 多品类支持:实物商品、虚拟商品、服务类商品混合销售
- 营销活动丰富:频繁的秒杀、限时折扣、优惠券活动
- 供应商集成需求:有第三方供应商,需要实时/定时同步库存
不适合的场景:
- 单一品类小店:可简化为单一策略
- 无库存管理需求:如知识付费、SaaS 订阅
- 极简 MVP 阶段:早期创业项目
系列导航
库存与价格在下单时的协作流程,详见(一)全景概览与领域划分中的 C 端用户旅程章节。
营销系统如何利用库存锁定实现秒杀活动,详见(四)营销系统深度解析。
订单系统如何编排库存扣减流程,详见(七)订单系统。