共计 2460 个字符,预计需要花费 7 分钟才能阅读完成。
背景痛点:电商库存的生死时速
去年双 11 我们系统经历了惨痛的教训:某爆款商品超卖 2000 件,直接导致 50 万元赔偿损失。事后复盘发现三个典型问题:

- 超卖现象 :1000 件库存最终卖出 1200 单,因为减库存的 SQL 没有加锁
- 数据不一致 :订单系统显示已付款,库存系统却显示有货
- 补货延迟 :凌晨 3 点库存触底,直到早上 9 点才人工补货
这促使我们开发了现在的补货 Skill 系统,其核心要解决两个问题:
- 如何保证库存操作的原子性(不超卖)
- 如何实现智能化的自动补货
技术方案选型:锁的艺术
Redis 分布式锁 vs 数据库乐观锁
我们对比了两种主流方案:
- Redis 分布式锁 (最终采用方案)
- 优点:性能高(单节点 10w+ QPS),实现简单
- 缺点:需要处理锁续期问题
-
关键代码片段(Go 版):
// 获取锁(含自动续期)func acquireLock(redisClient *redis.Client, key string, ttl time.Duration) (bool, error) {result, err := redisClient.SetNX(key, "locked", ttl).Result() if err != nil || !result {return false, err} // 启动续期 goroutine go func() {ticker := time.NewTicker(ttl / 2) defer ticker.Stop() for { select { case <-ticker.C: redisClient.Expire(key, ttl).Result() case <-stopChan: return } } }() return true, nil } -
数据库乐观锁
- 优点:无需额外组件,适合低并发场景
- 缺点:高并发时大量失败重试
- SQL 示例:
UPDATE inventory SET stock = stock - 1, version = version + 1 WHERE sku_id = '1001' AND version = 123 AND stock >= 1;
补货触发条件设计
我们的补货策略采用多维度触发机制:
- 静态阈值 :当库存低于安全库存(如总库存的 20%)
- 动态预测 :基于过去 7 天销量预测未来 3 天需求
- 时间窗口 :凌晨 2 - 4 点不触发自动补货(避开对账时段)
核心实现:消息驱动架构
库存扣减流程
- 订单服务发起扣减请求
- 库存服务获取分布式锁
- 执行 CAS 操作检查库存
- 发布库存变更事件
架构示意图:
sequenceDiagram
订单服务 ->>+ 库存服务: 扣减请求 (SKU=1001, 数量 =1)
库存服务 ->>+Redis: 获取锁 (sku:1001)
Redis-->>- 库存服务: 获取成功
库存服务 ->> 数据库: CAS 更新库存
数据库 -->> 库存服务: 更新成功
库存服务 ->>MQ: 发布库存变更事件
库存服务 ->>Redis: 释放锁
异步补货流程代码(Java 版)
// 库存监听器
@RabbitListener(queues = "inventory.queue")
public void handleLowStockEvent(InventoryEvent event) {
// 检查是否需要补货(防重复触发)if (!needReplenishment(event.getSkuId())) {return;}
// 创建补货任务(含幂等 ID)ReplenishmentTask task = new ReplenishmentTask(UUID.randomUUID().toString(),
event.getSkuId(),
calculateReplenishAmount(event.getSkuId())
);
// 持久化任务状态
taskRepository.save(task);
// 触发供应商接口调用
supplierService.requestReplenishment(task);
}
// 幂等检查(基于 Redis 实现)private boolean needReplenishment(String skuId) {
String key = "replenish:check:" + skuId;
return redisTemplate.opsForValue().setIfAbsent(key, "1", 24, TimeUnit.HOURS);
}
生产环境实战经验
性能压测数据
我们使用 JMeter 对不同方案进行了对比测试(单商品 10 万并发):
| 方案 | 平均响应时间 | 成功率 | QPS |
|---|---|---|---|
| 无锁 | 15ms | 62% | 8500 |
| 数据库悲观锁 | 210ms | 99.9% | 1200 |
| Redis 分布式锁 | 45ms | 99.8% | 6800 |
| 乐观锁 + 重试 | 75ms | 99.5% | 3200 |
故障恢复方案
我们遇到过两次严重问题及解决方案:
- 补货消息丢失
- 现象:MQ 集群故障导致补货请求丢失
- 方案:增加定时任务扫描低库存商品
-
代码:
/* 每小时执行的低库存扫描 */ SELECT sku_id FROM inventory WHERE stock < safe_stock AND last_modified > NOW() - INTERVAL 1 DAY -
ABA 问题
- 现象:取消订单回补库存时版本号被其他操作覆盖
- 方案:使用复合版本号(时间戳 + 计数器)
避坑指南
时钟漂移问题
在分布式锁场景下,我们曾因 NTP 同步问题导致:
- 服务器 A 认为锁已过期(实际未过期)
- 服务器 B 仍持有锁时被 A 强行获取
解决方案:
- 采用 Redis 的 RedLock 算法(多实例部署)
- 增加时钟漂移检测机制
补货状态机设计
为防止补货循环触发,我们设计了明确的状态流转:
stateDiagram-v2
[*] --> IDLE
IDLE --> PENDING: 库存低于阈值
PENDING --> PROCESSING: 获取补货权
PROCESSING --> SUCCESS: 供应商确认
PROCESSING --> FAILED: 供应商拒绝
FAILED --> PENDING: 30 分钟后重试
开放性问题
当前系统仍存在一个挑战:当商品同时在多个仓库有库存时,如何设计补货策略?考虑因素包括:
- 各仓库实时库存水平
- 区域销售预测差异
- 物流成本计算
- 供应商最短到货时间
我们正在试验基于强化学习的动态调配算法,期待读者分享你们的解决方案。
正文完
