一、业务背景与问题
日系中古商品具有单件稀缺、库存唯一、不可再生的特性——每件商品只有一个,售出即下架。这种业务模式对库存扣减的精准度提出了极高要求,超卖即意味着严重售后事故。
在多数代拍平台中,库存管理采用下单瞬时扣减的简单逻辑:用户点击下单后系统立即将库存-1,支付失败再异步回补。在高并发抢购、多人同时操作同一稀缺商品、网络重试请求等场景下,这种方案暴露出一系列问题:
重复扣减导致超卖:同一商品在极短时间内被多个用户同时下单,由于并发控制不足,系统对每个请求都执行了库存扣减,导致一件商品被多名用户同时“购买成功”,最终只有一人能拿到货,其余用户只能退款或取消订单,引发大量售后纠纷。
库存无法及时释放:用户下单后放弃支付、页面未完成关闭、支付过程中断等场景下,已扣减的库存未能及时释放,导致商品虽然实际可售但在系统中显示为“已锁定”,错失其他真实购买意向用户的下单机会,造成库存“假死”。
状态不一致:因网络重试等原因,同一用户的同一订单可能被多次扣减库存,造成订单状态与库存数据不一致,运营需人工介入对账和修复。
二、技术方案选型
平台基于Redis实现了分布式库存预锁定机制。核心设计思路是锁定与扣减分离——用户下单时仅占用库存配额,支付成功后才执行最终扣减,支付失败或超时则自动释放锁定。
技术方案选择Redis而非数据库行锁的原因如下:Redis单线程模型天然保证原子性操作,适用于高并发场景;支持设置键过期时间(TTL),可低成本实现锁定超时自动释放;读写性能远高于数据库,可支撑大流量抢购场景。但Redis作为缓存存在数据丢失风险,需配合数据库落库做最终一致性兜底。
三、核心实现细节
原子锁定操作:使用Redis的SET NX EX命令实现原子性锁定。每个商品库存对应一个Redis键,用户下单时执行以下操作:检查当前锁定数量是否小于总库存,若是则执行原子自增操作并设置锁定过期时间,若否则返回库存不足。其中库存上限在商品上架时从数据库同步到Redis,后续锁定验证均基于Redis中的库存上限,避免跨系统调用损耗。
java
// 原子锁定操作简例
String lockKey = "product:stock:" + productId;
Long lockedCount = redisTemplate.opsForValue().increment(lockKey, 1);
if (lockedCount == 1) {
// 首次锁定,设置过期时间
redisTemplate.expire(lockKey, 600, TimeUnit.SECONDS);
}
if (lockedCount > totalStock) {
// 超出库存上限,回滚锁定
redisTemplate.opsForValue().decrement(lockKey);
return "库存不足";
}
锁定操作成功后,系统进入支付流程。此时其他用户的锁定请求会触发库存上限校验并返回“已被其他用户锁定”的明确提示。
超时自动释放:锁定键设置过期时间(TTL)作为兜底释放机制。用户在支付超时后,Redis自动删除对应键,库存配额被释放回可分配池。锁定时长根据业务场景动态适配——普通商品锁定时长适配常规支付操作节奏(如10分钟),抢购热门商品适当缩短(如5分钟),防止资源长期被无效占用。
支付成功最终扣减:用户完成支付后,系统执行最终库存扣减操作,从数据库商品表扣除对应库存,并记录流水日志。同时删除Redis中的锁定键,标识该商品已完成交易。由于Redis锁定和数据库扣减无法在同一事务中保证强一致性,系统采用“先更新数据库、再清理缓存”的策略,若缓存清理失败则通过定时任务异步重试,容忍短暂的不一致窗口,最终保证数据一致。
四、锁定异常处理
被动中断场景:用户在下单过程中因网络闪断、浏览器崩溃、App闪退等被动原因中断操作,锁定键因设置了TTL而过期自动释放,无需人工介入。
主动放弃场景:用户在支付页面主动取消订单或返回,系统实时调用释放接口,主动删除Redis锁定键并回滚锁定计数。
定时巡检兜底:针对极端情况下可能出现的锁定残留(如Redis键因异常未被正确删除),平台部署定时巡检任务,扫描超时未支付的订单并强制释放锁定,确保库存卡死残留归零。
五、落地效果与总结
上述方案上线后,日系稀缺中古商品因并发下单导致的超卖问题基本消除,库存数据与可售状态展示保持一致,因库存错乱引发的用户咨询和售后纠纷大幅减少。方案基于Redis实现,无额外硬件成本,对中小型代拍平台有较高的参考价值。核心要点总结:锁定与扣减分离是避免超卖的关键,超时释放机制是防止库存假死的保障,定时巡检是数据最终一致性的底线。