代购系统库存预占机制:防止超卖的分布式锁实现
在代购、跨境电商等高并发下单场景中,库存超卖是最常见且代价极高的问题。多服务实例、多线程并发下单时,传统本地锁失效,极易导致库存校验与扣减出现竞态条件,最终出现 “无货可发、订单积压” 的故障。
本文围绕代购系统库存预占核心流程,讲解如何通过分布式锁实现安全库存预占,从原理、流程、代码实现到生产级优化,完整落地防超卖方案。
一、为什么代购系统必须做库存预占
代购业务有三大特性,决定库存必须 “先预占、后扣减”:
- 采购链路长:海外货源、清关、物流周期长,库存无法实时补货
- 并发集中:限时折扣、爆款补货瞬间流量高,并发冲突剧烈
- 支付异步:用户下单后需等待支付,未支付订单需释放库存
库存预占核心目标:下单时锁定库存,支付成功正式扣减,超时未支付自动释放,从根源杜绝超卖。
二、分布式锁解决超卖的核心原理
分布式锁的作用是跨服务、跨节点互斥,保证同一时间只有一个请求能操作同一商品库存。
核心流程:一锁二判三更新
- 加锁:对商品 ID 加分布式锁,阻塞并发请求
- 校验:查询真实可用库存,判断是否足够预占
- 预占:库存充足则写入预占记录,扣减可用库存
- 释放:业务完成释放锁,异常超时自动释放锁
满足三大特性:互斥性、防死锁、防误删,是防超卖的基础保障。
三、技术选型:Redis+Redisson 生产级方案
代购系统推荐 Redis + Redisson 实现分布式锁,理由:
- 性能高,内存操作响应快
- Redisson 封装完善,自带看门狗自动续期
- 支持可重入、公平锁、联锁,适配复杂业务
- 过期时间自动释放,避免服务宕机死锁
备选:ZooKeeper/etcd 一致性更强,但性能更低,适合强一致低并发场景。
四、库存预占完整实现流程
1. 数据结构设计
- 库存总表:
goods_id、total_stock、occupied_stock、available_stock - 预占记录表:
preempt_id、goods_id、order_id、user_id、status、expire_time - 锁 Key:
lock:goods:{goods_id}
2. 核心业务流程
- 用户提交下单请求
- 生成唯一请求 ID,尝试获取 Redisson 分布式锁
- 加锁成功:查询可用库存
- 库存不足:直接返回 “库存不足”,释放锁
- 库存充足:新增预占记录,更新可用库存与预占库存
- 开启支付超时任务(如 15 分钟)
- 释放分布式锁,返回下单成功
- 支付成功:正式扣减库存,删除预占记录
- 支付超时:定时任务回滚库存,释放预占
五、代码实现(Java + SpringBoot + Redisson)
1. 分布式锁工具类封装
java
运行
@Resource
private RedissonClient redissonClient;
/**
* 获取分布式锁
* @param goodsId 商品ID
* @param requestId 请求唯一ID
* @return 锁对象
*/
public RLock getGoodsLock(Long goodsId, String requestId) {
String lockKey = "lock:goods:" + goodsId;
RLock lock = redissonClient.getLock(lockKey);
// 看门狗自动续期,默认30秒,每10秒续期
lock.lock(30, TimeUnit.SECONDS);
return lock;
}
/**
* 释放锁
*/
public void unlock(RLock lock) {
if (lock != null && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
2. 库存预占核心方法
java
运行
@Transactional(rollbackFor = Exception.class)
public Result<?> preemptStock(Long goodsId, Integer num, String orderId, Long userId) {
String requestId = UUID.randomUUID().toString();
RLock lock = null;
try {
// 1. 加分布式锁
lock = getGoodsLock(goodsId, requestId);
// 2. 查询可用库存
GoodsStock stock = stockMapper.selectByGoodsId(goodsId);
if (stock == null || stock.getAvailableStock() < num) {
return Result.fail("库存不足");
}
// 3. 写入预占记录
StockPreempt preempt = new StockPreempt();
preempt.setGoodsId(goodsId);
preempt.setOrderId(orderId);
preempt.setUserId(userId);
preempt.setNum(num);
preempt.setStatus(0); // 0-预占中 1-已扣减 2-已释放
preempt.setExpireTime(LocalDateTime.now().plusMinutes(15));
preemptMapper.insert(preempt);
// 4. 更新库存:可用库存减少,预占库存增加
stockMapper.updateStock(goodsId, -num, num);
return Result.success("库存预占成功");
} finally {
// 5. 释放锁
unlock(lock);
}
}
3. 支付超时自动释放(定时任务)
java
运行
@Scheduled(cron = "0 */1 * * * ?")
public void clearExpirePreempt() {
// 查询超时预占记录
List<StockPreempt> list = preemptMapper.selectExpirePreempt(LocalDateTime.now());
for (StockPreempt preempt : list) {
RLock lock = getGoodsLock(preempt.getGoodsId(), UUID.randomUUID().toString());
try {
// 回滚库存:可用库存增加,预占库存减少
stockMapper.updateStock(preempt.getGoodsId(), preempt.getNum(), -preempt.getNum());
// 更新预占状态为已释放
preemptMapper.updateStatus(preempt.getId(), 2);
} finally {
unlock(lock);
}
}
}
六、生产级防坑要点
-
锁粒度精细化按商品 ID 加锁,不锁全表,提升并发能力。
-
请求 ID 防误删释放锁时校验请求 ID,避免释放其他线程的锁。
-
看门狗必开Redisson 看门狗自动续期,防止业务未执行完锁过期。
-
数据库事务与锁顺序先加锁,后开启事务,避免长事务持有锁导致性能下降。
-
库存兜底校验更新库存时 SQL 增加条件:
available_stock >= #{num},防止超卖。sql
UPDATE goods_stock SET available_stock = available_stock - #{num}, occupied_stock = occupied_stock + #{num} WHERE goods_id = #{goodsId} AND available_stock >= #{num} -
最终一致性保障定时任务 + 对账脚本,每日核对库存、预占、订单数据,修复异常。
七、方案对比与选型建议
表格
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 本地锁 | 实现简单 | 分布式失效 | 单体应用 |
| Redis 分布式锁 | 高性能、易集成 | 主从切换有极小锁失效风险 | 代购 / 电商高并发下单 |
| Redis+Lua 原子脚本 | 性能极高 | 业务逻辑复杂难维护 | 秒杀、极致高并发 |
| ZooKeeper 锁 | 强一致性 | 性能低 | 金融、强一致场景 |
代购系统首选:Redis+Redisson 分布式锁,平衡性能、可靠性与开发成本。
八、总结
代购系统防超卖的核心是库存预占 + 分布式锁,通过 Redisson 实现跨节点互斥,配合预占超时释放机制,可 100% 避免超卖。
落地关键点:
- 按商品 ID 细粒度加锁
- 先锁后查,SQL 兜底校验
- 支付超时自动回滚
- 看门狗防止锁提前释放
这套方案可直接用于生产,支撑日常下单与爆款并发,稳定可靠。
更多推荐
所有评论(0)