在代购、跨境电商等高并发下单场景中,库存超卖是最常见且代价极高的问题。多服务实例、多线程并发下单时,传统本地锁失效,极易导致库存校验与扣减出现竞态条件,最终出现 “无货可发、订单积压” 的故障。
本文围绕代购系统库存预占核心流程,讲解如何通过分布式锁实现安全库存预占,从原理、流程、代码实现到生产级优化,完整落地防超卖方案。
一、为什么代购系统必须做库存预占
代购业务有三大特性,决定库存必须 “先预占、后扣减”:
- 采购链路长:海外货源、清关、物流周期长,库存无法实时补货
- 并发集中:限时折扣、爆款补货瞬间流量高,并发冲突剧烈
- 支付异步:用户下单后需等待支付,未支付订单需释放库存
库存预占核心目标:下单时锁定库存,支付成功正式扣减,超时未支付自动释放,从根源杜绝超卖。
二、分布式锁解决超卖的核心原理
分布式锁的作用是跨服务、跨节点互斥,保证同一时间只有一个请求能操作同一商品库存。
核心流程:一锁二判三更新
- 加锁:对商品 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 兜底校验
- 支付超时自动回滚
- 看门狗防止锁提前释放
这套方案可直接用于生产,支撑日常下单与爆款并发,稳定可靠。