题目
设计电商系统的库存扣减服务
信息
- 类型:问答
- 难度:⭐⭐
考点
并发控制,分布式事务,缓存与数据库一致性,服务容错
快速回答
设计电商库存扣减系统的核心要点:
- 并发控制:使用分布式锁(如Redis)或数据库乐观锁防止超卖
- 扣减策略:采用预扣库存(冻结库存)模式,订单创建时预占,支付成功后实际扣减
- 数据一致性:数据库与缓存通过双写+失效策略同步,使用事务消息保证最终一致性
- 服务设计:独立库存服务,接口幂等,熔断降级机制
- 扩展性:分库分表策略,商品库存按SKU分片
1. 核心挑战与设计原则
核心问题:高并发场景下保证库存准确性,避免超卖(如秒杀场景10万QPS)
设计原则:
- 最终一致性优先于强一致性
- 读多写少场景使用缓存优化
- 接口必须幂等(相同请求重复处理结果一致)
2. 架构设计
┌─────────────┐ ┌─────────────┐ ┌───────────┐
│ 订单服务 │───┬──▶│ 库存服务 │───┬──▶│ 数据库 │
└─────────────┘ │ └─────────────┘ │ └───────────┘
│ │
│ └──▶│ Redis缓存 │
│ └───────────┘
│
└───▶ [MQ: 事务消息]3. 关键实现方案
3.1 扣减流程(预扣库存模式)
- 用户下单:订单服务调用库存服务扣减接口(含SKU ID、数量)
- 预扣库存:库存服务执行
UPDATE inventory SET available = available - #{qty}, locked = locked + #{qty} WHERE sku_id = #{skuId} AND available >= #{qty} - 返回结果:成功则生成订单,失败提示库存不足
- 支付回调:支付成功后执行实际扣减
UPDATE inventory SET locked = locked - #{qty} WHERE sku_id = #{skuId}
3.2 并发控制方案
| 方案 | 实现方式 | 适用场景 |
|---|---|---|
| 乐观锁 | SQL中带版本号条件更新 | 中等并发(1万QPS) |
| 分布式锁 | Redis SETNX + Lua脚本 | 超高并发(Redis集群支撑) |
| 队列串行化 | RocketMQ顺序消息 | 保证绝对顺序但延迟高 |
Redis分布式锁示例:
// 使用Redisson实现
RLock lock = redisson.getLock("stock_lock:" + skuId);
try {
if (lock.tryLock(100, 10, TimeUnit.MILLISECONDS)) {
// 执行库存操作
}
} finally {
lock.unlock();
}3.3 缓存与数据库一致性
双写+失效策略:
- 更新数据库后删除Redis缓存
- 读取时若缓存不存在则回源数据库
- 使用
canal监听binlog异步更新缓存
防击穿方案:缓存空值或使用Bloom过滤器
3.4 分布式事务保障
支付成功后的最终一致性:
1. 订单服务提交本地事务(订单状态更新)
2. 发送事务消息到MQ(RocketMQ Half Message)
3. 库存服务消费消息执行实际扣减
4. 失败时重试+人工补偿4. 容错与扩展设计
- 熔断降级:Hystrix/Sentinel保护库存服务
- 分库分表:按sku_id哈希分片(如1024个分片)
- 热点库存:本地缓存+Redis分片前缀(stock:sku_{id})
- 数据冗余:异步统计总库存到Elasticsearch供查询
5. 常见错误
- 超卖风险:未在SQL中校验可用库存(WHERE available >= qty)
- 缓存穿透:未处理不存在的SKU查询导致压垮数据库
- 事务过大:扣减操作与业务逻辑放在同一事务导致锁竞争
- 重试机制缺失:网络抖动导致扣减失败无自动补偿
6. 扩展知识
- 库存分层:总库存 = 可用库存 + 锁定库存 + 预占库存
- 秒杀优化:库存预热到Redis,扣减仅操作缓存异步落库
- 动态扩容:Kubernetes HPA基于库存服务CPU自动扩缩容