代购系统库存预占机制:防止超卖的分布式锁实现
在代购、跨境电商等高并发下单场景中库存超卖是最常见且代价极高的问题。多服务实例、多线程并发下单时传统本地锁失效极易导致库存校验与扣减出现竞态条件最终出现 “无货可发、订单积压” 的故障。本文围绕代购系统库存预占核心流程讲解如何通过分布式锁实现安全库存预占从原理、流程、代码实现到生产级优化完整落地防超卖方案。一、为什么代购系统必须做库存预占代购业务有三大特性决定库存必须 “先预占、后扣减”采购链路长海外货源、清关、物流周期长库存无法实时补货并发集中限时折扣、爆款补货瞬间流量高并发冲突剧烈支付异步用户下单后需等待支付未支付订单需释放库存库存预占核心目标下单时锁定库存支付成功正式扣减超时未支付自动释放从根源杜绝超卖。二、分布式锁解决超卖的核心原理分布式锁的作用是跨服务、跨节点互斥保证同一时间只有一个请求能操作同一商品库存。核心流程一锁二判三更新加锁对商品 ID 加分布式锁阻塞并发请求校验查询真实可用库存判断是否足够预占预占库存充足则写入预占记录扣减可用库存释放业务完成释放锁异常超时自动释放锁满足三大特性互斥性、防死锁、防误删是防超卖的基础保障。三、技术选型RedisRedisson 生产级方案代购系统推荐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锁 Keylock:goods:{goods_id}2. 核心业务流程用户提交下单请求生成唯一请求 ID尝试获取 Redisson 分布式锁加锁成功查询可用库存库存不足直接返回 “库存不足”释放锁库存充足新增预占记录更新可用库存与预占库存开启支付超时任务如 15 分钟释放分布式锁返回下单成功支付成功正式扣减库存删除预占记录支付超时定时任务回滚库存释放预占五、代码实现Java SpringBoot Redisson1. 分布式锁工具类封装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() { // 查询超时预占记录 ListStockPreempt 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}防止超卖。sqlUPDATE goods_stock SET available_stock available_stock - #{num}, occupied_stock occupied_stock #{num} WHERE goods_id #{goodsId} AND available_stock #{num}最终一致性保障定时任务 对账脚本每日核对库存、预占、订单数据修复异常。七、方案对比与选型建议表格方案优点缺点适用场景本地锁实现简单分布式失效单体应用Redis 分布式锁高性能、易集成主从切换有极小锁失效风险代购 / 电商高并发下单RedisLua 原子脚本性能极高业务逻辑复杂难维护秒杀、极致高并发ZooKeeper 锁强一致性性能低金融、强一致场景代购系统首选RedisRedisson 分布式锁平衡性能、可靠性与开发成本。八、总结代购系统防超卖的核心是库存预占 分布式锁通过 Redisson 实现跨节点互斥配合预占超时释放机制可 100% 避免超卖。落地关键点按商品 ID 细粒度加锁先锁后查SQL 兜底校验支付超时自动回滚看门狗防止锁提前释放这套方案可直接用于生产支撑日常下单与爆款并发稳定可靠。