多品类统一库存系统设计:电商·虚拟商品·本地生活
一、背景与挑战
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)组合,即可复用对应的库存策略,无需修改核心代码。
三、统一数据模型
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 | 订单取消 / 超时未支付 |
十、数据一致性保障
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 |
十一、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 次失败后人工补发。
十三、监控与告警
13.1 关键指标
| 指标 | 阈值 | 告警级别 |
|---|---|---|
| 超卖次数 | > 0 | P0 |
| Redis vs MySQL 差异 | > 100 | P1 |
| 库存服务错误率 | > 1% | P1 |
| 库存扣减 P99 | > 200ms | P2 |
| 补货失败率 | > 5% | P2 |
| 供应商同步延迟 | > 10min | P2 |
| 低库存商品数 | > 100 | P3 |
13.2 Prometheus Metrics
1 | # 操作计数 |
十四、新品类接入指南
三步接入:
- 评估分类:确定
(ManagementType, UnitType, DeductTiming)。 - 写配置:在
inventory_config表插入一条记录。 - 调接口:使用统一
InventoryManager.BookStock()即可。
1 | // 示例:接入新品类"演唱会门票" |
十五、生产环境实战数据
15.1 业务规模
| 指标 | 数值 | 说明 |
|---|---|---|
| 秒杀峰值 QPS | 20,000 | 单个爆款商品,持续 5-10 分钟 |
| 日均 QPS | 50 | 常态流量 |
| 日均订单量 | 2,000,000 | 支付成功订单 |
| 日均库存扣减 | 6,700,000 | 含预订、支付、取消等操作 |
| 峰值/日均比 | 870:1 | 流量极度不均匀 |
容量规划推算:
1 | 日均订单 2M / 86400s ≈ 23 TPS |
15.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%**,非常充裕
15.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 补货开销)
15.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 万(中型平台) |