库存超卖是电商系统中最致命的业务漏洞之一。去年双十一期间,某头部平台因瞬时超卖导致3000多笔订单无法履约,直接损失超800万。超卖的根源在于"库存数据"与"实际库存"在并发场景下的不一致性。当100个用户同时抢购最后10件商品时,若系统仅做简单的"库存>0"判断,必然导致90笔无效订单。
典型的超卖场景包含三个关键环节:
sql复制BEGIN;
SELECT stock FROM products WHERE id=123 FOR UPDATE;
-- 业务逻辑判断
UPDATE products SET stock=stock-1 WHERE id=123;
COMMIT;
优势:强一致性保证,适合高价值商品(如奢侈品)
缺陷:并发性能差,实测QPS不超过500,会导致连接池耗尽
java复制// 先查询当前版本号
Product product = productMapper.selectById(123);
if(product.getStock() > 0){
int updated = productMapper.updateStock(
123, product.getVersion(), product.getStock()-1);
if(updated == 0){
throw new OptimisticLockException();
}
}
性能对比:比悲观锁提升3-5倍吞吐量
注意事项:需要合理设置重试次数,建议配合熔断机制
lua复制-- KEYS[1]:商品ID ARGV[1]:扣减数量
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock >= tonumber(ARGV[1]) then
return redis.call('DECRBY', KEYS[1], ARGV[1])
else
return -1
end
性能数据:单节点可达5万QPS
关键点:必须配合异步库存同步机制,避免Redis与数据库长期不一致
java复制@GlobalTransactional
public void placeOrder(Long productId) {
productService.reduceStock(productId);
orderService.createOrder(productId);
}
适用场景:跨服务库存操作
成本分析:性能下降40%,需要部署事务协调器
mermaid复制graph TD
A[前端] -->|请求| B(Redis缓存层)
B -->|异步| C[数据库持久层]
C -->|定时任务| D[仓储系统]
流量分配:
库存服务核心逻辑:
java复制public boolean reduceStock(Long productId, int quantity) {
// 第一层:Redis原子扣减
Long result = redisTemplate.execute(STOCK_DEDUCTION_SCRIPT,
Collections.singletonList("stock:" + productId),
String.valueOf(quantity));
if(result == null || result < 0){
return false;
}
// 第二层:数据库最终确认
return stockTransactionService.asyncConfirm(
productId, quantity);
}
压力测试对比(单商品10万库存):
| 方案 | QPS | 错误率 | 响应时间 |
|---|---|---|---|
| 纯数据库悲观锁 | 342 | 0% | 1.2s |
| Redis+数据库异步 | 48,000 | 0.003% | 28ms |
| 纯乐观锁 | 1,200 | 0.8% | 65ms |
秒杀场景:
预售模式:
组合商品:
关键经验:任何防超卖方案都必须配合完善的监控体系,建议设置:
- 库存波动阈值告警
- 订单/库存差异检测
- 自动冻结异常商品功能
在实际项目中,我们采用Redis+Lua+异步落地的混合方案后,在大促期间实现了零超卖记录。最重要的教训是:防超卖不是单纯的技术问题,需要建立从缓存到数据库、从下单到支付的全链路协同机制。