1. 问题现象与业务背景
在电商系统中,库存管理是最核心的模块之一。我们团队最近遇到一个典型的并发问题:当用户下单后系统预扣减库存,但如果用户未在限定时间内支付,理论上应该自动释放库存。然而实际运行中,部分库存却像被"幽灵"锁定一样无法释放,导致超卖和库存不一致。
这种情况通常出现在大促期间,当并发量达到峰值时尤为明显。从业务角度看,这直接影响了转化率——真实库存被无效订单占用,新用户无法下单,而运营人员看到的库存数据又与实际可售数量不符。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术原理深度解析
2.1 传统库存扣减流程
典型的PHP库存扣减流程是这样的:
php复制// 下单时预扣减
UPDATE products SET stock = stock - 1 WHERE id = 123 AND stock >= 1;
// 支付超时后释放
UPDATE products SET stock = stock + 1 WHERE id = 123;
这个方案在低并发时没有问题,但在高并发场景下会出现多个致命问题:
- 扣减和释放不是原子操作
- 缺乏事务边界控制
- 没有版本控制机制
- 超时判定与库存释放存在时间差
2.2 问题根因分析
通过日志分析,我们发现"幽灵锁"主要发生在以下场景时序中:
- 用户A下单,库存从100扣减到99(剩余99)
- 用户A未支付,系统触发释放逻辑
- 同时用户B下单,库存从99扣减到98
- 用户A的释放逻辑执行,库存从98增加到99
- 此时用户C下单,理论上可以购买,但系统却提示库存不足
根本原因是释放操作基于当前库存值进行+1,而不是基于原始扣减值回滚。这种设计在并发场景下必然导致数据不一致。
3. 解决方案设计与实现
3.1 方案选型对比
我们评估了三种主流解决方案:
| 方案 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 预扣记录表 | 单独记录扣减明细 | 可精准回滚 | 需要联表查询 | 中小型系统 |
| 版本号控制 | 每次更新校验版本 | 实现简单 | 需要重试机制 | 读多写少 |
| 分布式锁 | 强一致性控制 | 可靠性高 | 性能损耗大 | 高并发场景 |
最终选择预扣记录表+版本号控制的混合方案,在保证精度的同时兼顾性能。
3.2 具体实现代码
库存预扣表设计:
sql复制CREATE TABLE inventory_holds (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
product_id INT NOT NULL,
order_id VARCHAR(32) NOT NULL,
quantity INT NOT NULL,
status TINYINT DEFAULT 1
