
库存防超卖方案解析从需求到落地的技术选型适合正在处理多库存源并发问题的后端开发者如果只关注业务逻辑可以跳过代码部分直接看思路。事故现场同一件衣服卖出了三次有次客户凌晨两点在后台看到一件爆款卫衣库存显示3件他下单了3件结果第二天系统只采购到1件——另外两件被另一个渠道的订单抢走了。这单亏了大概两百块运费和客户信任。这不是个例。跨境代购场景下库存超卖比普通电商更棘手因为库存源不止一个自营仓、1688代发仓、海外仓每个源的更新延迟不同同步频率不同竞态条件无处不在。为什么定时任务解决不了大部分系统用定时任务同步库存比如每5分钟从1688 API拉一次库存数据更新到本地数据库。这个方案在低并发下勉强能用但有几个致命问题同步窗口期内数据不一致5分钟间隔里1688那边的库存可能已经被其他买家消耗了多实例并发更新如果系统部署了多个实例同时读取1688库存后写入本地后写入的会覆盖前一个造成数据丢失大促期间延迟放大1688大促期间接口库存数据延迟最高可达30秒未做防抖的系统超卖概率大概0.6%左右说白了吧定时任务本质上是猜测库存——它假设两次同步之间库存没变但这个假设在高并发场景下站不住脚。技术方案从被动同步到主动锁仓Taocarts在库存防超卖上走了三条路不是选其一而是三层叠加第一层Redis分布式锁 Lua脚本原子扣减// 库存扣减的Lua脚本保证原子性$luaScriptLUAlocal key KEYS[1] -- 库存Redis key local quantity tonumber(ARGV[1]) -- 扣减数量 local current redis.call(GET, key) if not current then return -1 -- 库存不存在 end current tonumber(current) if current quantity then return 0 -- 库存不足 end redis.call(DECRBY, key, quantity) return 1 -- 扣减成功LUA;$resultRedis::eval($luaScript,1,stock:sku:{$skuId},$quantity);if($result0){// 库存不足进入等待队列或直接拒绝thrownewInsufficientStockException(SKU{$skuId}库存不足);}这个方案解决的是单节点内的并发问题。Lua脚本在Redis中原子执行不会出现两个请求同时读到库存为3然后都扣减成功的竞态条件。第二层库存预占 订单确认双阶段Redis扣减只是预占真正的库存释放是在订单确认后。流程是用户下单 → Redis预占库存Lua脚本订单进入采购流程 → 向1688发送采购请求1688返回采购成功 → 本地数据库库存正式扣减1688返回采购失败缺货 → Redis回滚预占库存// 订单确认后的库存正式扣减publicfunctionconfirmStockDeduction(string$orderId,string$skuId,int$quantity):void{DB::beginTransaction();try{// 数据库层扣减使用乐观锁$affectedDB::table(inventory)-where(sku_id,$skuId)-where(stock,,$quantity)-decrement(stock,$quantity);if($affected0){thrownewInventoryConflictException(数据库库存不足SKU:{$skuId});}// 更新订单状态DB::table(orders)-where(id,$orderId)-update([stock_statusconfirmed]);DB::commit();}catch(\Exception$e){DB::rollBack();// Redis回滚预占Redis::incrby(stock:sku:{$skuId},$quantity);throw$e;}}这个双阶段设计的关键在于Redis预占是快速响应数据库确认是最终一致性。即使Redis挂了数据库还有一份准确数据兜底。第三层1688代采实时状态机前面两层解决的是本地库存问题但库存超卖最头疼的是上游数据源不准。1688的库存数据不是实时的供应商可能同时接到多个渠道的订单。Taocarts的1688自动代采模块用状态机处理这个问题订单状态流转 pending → purchasing → purchased → failed/success ↓ stock_conflict → rollback当采购请求发出去后系统会轮询1688的订单状态。如果1688返回库存不足或部分发货系统自动触发库存回滚并通知客户。// 1688采购状态轮询publicfunctionpollPurchaseStatus(string$purchaseOrderId):void{$status$this-get1688OrderStatus($purchaseOrderId);if($statuspartially_shipped){// 部分发货回滚未发货部分的库存$unshippedQty$this-getUnshippedQuantity($purchaseOrderId);$this-rollbackStock($purchaseOrderId,$unshippedQty);// 通知客户event(newStockConflictDetected($purchaseOrderId,$unshippedQty));}if($statusout_of_stock){// 完全缺货全量回滚$this-rollbackAllStock($purchaseOrderId);event(newOrderFailedDueToStock($purchaseOrderId));}}效果对比| 指标 | 改造前 | 改造后 | |||| | 库存超卖事故月均 | 2-3次 | 0次 | | 单次事故平均损失 | 约2000元 | 0元 | | 订单确认时间 | 依赖定时任务最长5分钟 | 毫秒级响应 | | 客户投诉 | 每月约5-8起 | 0起 |有个数据值得注意库存同步超卖率从大概0.6%降至接近零。不是完全消除因为1688上游的延迟依然存在但系统已经把风险控制在可接受范围内。一些真实的trade-off这个方案不是没有代价Redis内存消耗增加每个SKU的库存缓存需要额外内存如果SKU数量超过10万内存占用大概在2-3GB左右双阶段设计增加了代码复杂度预占和确认之间的状态管理需要额外处理比如订单超时未确认时的库存释放1688轮询增加了接口调用量每个采购订单需要多次轮询如果订单量大API调用成本会上升这些trade-off在系统设计时就要想清楚。如果你的平台日均订单量在1000以下用简单的定时任务加数据库乐观锁就够了。但如果你做的是代购平台订单量上去了这套方案是必要的。开放讨论你们现在用什么方式管订单有没有什么土办法比系统还好用欢迎说说。