Taocarts 知识

跨境物流方案中的超卖防御:从库存竞态到账实一致的架构设计

📅 2026-04-10 博客文章

跨境物流方案中的超卖防御:从库存竞态到账实一致的架构设计

做库存扣减时,Redis Lua 脚本和 MySQL 行锁到底怎么选?Redisson(GitHub 22k+ stars)封装了 Redis 分布式锁,RocketMQ(GitHub 20k+ stars)提供事务消息确保最终一致性。两个方向都有成熟的开源项目支撑,但各自的性能边界和一致性保证差异很大——选错了,轻则库存不准,重则月底对账对不上。

代购系统的库存来源比标准电商复杂得多。一个 SKU 可能同时存在于自营仓、1688 供应商库存、海外仓三个维度,扣减时既要防超卖,又要保证多仓之间不会重复扣减。单纯依赖数据库行锁在日单量三位数以下能跑通,但到了大促场景,行锁的排队效应会迅速拖垮下单链路。Redis Lua 的优势在单线程模型下的原子执行,但宕机时脚本执行结果可能丢失,不适合对账要求严格的财务场景。两种方案之间没有绝对的好坏,只有适用边界的取舍。

库存扣减的原子性选型:Redis Lua 与 MySQL 行锁

MySQL 行锁的实现最直接:SELECT ... FOR UPDATE 锁住库存行,后续请求排队等待。优点是数据天然持久化,事务回滚时锁自动释放。但行锁的排队机制在热点 SKU 上会成为瓶颈——一件爆款商品每秒几百次下单请求全部串行排队,响应时间随并发线性增长。换用 UPDATE ... WHERE stock >= qty 的乐观锁方案虽然减少了锁竞争,但 CAS 冲突后的重试开销在高峰期同样不可忽视。

Redis Lua 脚本则是另一个权衡。将"检查库存→扣减库存"打包成一个原子执行单元,在 Redis 单线程模型下天然避免竞态条件。对于库存从 100 扣到 99 这种高频小额操作,比行锁方案少一次网络往返和锁争用开销。

-- Redis Lua 原子库存扣减
local stock = tonumber(redis.call('get', KEYS[1]) or 0)
local request = tonumber(ARGV[1])
if stock >= request then
    redis.call('decrby', KEYS[1], request)
    return 1
end
return 0

Lua 脚本的原子性依赖 Redis 单线程执行命令的特性,整个脚本执行期间不会被其他命令插队。但局限性同样需要正视:Redis 宕机时脚本执行结果可能丢失,纯缓存方案不适合对账要求严格的财务场景。实践中的常用折中是每次扣减同步写一份 WAL 日志到 MySQL,即使 Redis 数据丢失也能从日志回放恢复。Taocarts 的仓储模块在库存扣减路径上将 Redis Lua 作为第一道防线,扣减成功后异步入库同步,兼顾了性能与数据持久性。

自营仓出库扫描减库存,1688 代发仓靠 API 回调更新库存,海外仓的同步周期可能是小时级。三个数据源的时间差叠加在一起,就是超卖的温床。客户看到的总库存是三个仓的聚合值,但扣减时可能只有自营仓的库存是实时的。

处理多仓同步延迟,核心设计是将库存分为"可售库存"和"在途库存"两层。自营仓的实时库存直接计入可售,1688 回调未确认的库存记为在途,聚合展示时只展示可售库存。回调确认后将数量从在途迁移到可售,并触发一次全量校验。

-- 库存分层:可售库存与在途库存分离
CREATE TABLE inventory_sku (
    sku_id BIGINT PRIMARY KEY,
    available_qty INT NOT NULL DEFAULT 0 COMMENT '可售库存(已确认)',
    in_transit_qty INT NOT NULL DEFAULT 0 COMMENT '在途库存(待回调确认)',
    version INT NOT NULL DEFAULT 0,
    updated_at DATETIME(3) NOT NULL
);

回调到达时,用版本号乐观锁更新,将 in_transit_qty 减掉、available_qty 加上。回调重复到达时版本号已变,更新影响行数为 0,不会重复累加。这套逻辑与 Seata(GitHub 25k+ stars)的 AT 模式有相似之处——都是通过版本号或 undo log 实现幂等写入——只不过库存场景的事务链相对短,不需要全局事务协调器的重投入。如果业务规模不大,直接在应用中用乐观锁解决,比引入一套分布式事务框架更划算。

在多仓协同的代购商城系统部署中,Taocarts 通过维护三级库存结构(自营仓、代发仓、海外仓)和每仓独立的库存版本号,将同步延迟导致的超卖概率控制在可接受范围内。

库存扣对了,账不一定对。一笔订单的库存扣减、采购成本、物流运费是三个独立发生的事件,如果只扣了库存没记成本,或者记了成本但库存回滚时成本没跟着退,月末对账就会出现"库存平了、账上不平"的奇怪局面。

防超卖架构的闭环逻辑应该是:库存扣减成功的同时写入一条库存流水日志,记录扣减时的成本价快照。后续采购入库的成本变更、物流费用的产生都与这条流水日志做关联。月底对账时按 SKU 维度汇总库存流水中的成本金额,与财务系统的采购成本比对,差异一目了然。

这种事件溯源设计在开源社区也有参考实现。Axon Framework(GitHub 3k+ stars)将领域事件作为一等公民,所有状态变更都通过事件记录。代购系统的库存和账务模块不一定要全套引入 CQRS,但吸收事件日志的思想——让每笔库存变动都留有不可篡改的流水——对排查账实差异有直接帮助。

库存超卖的根在时序,账实不符的根在事件丢失。把库存扣减做成原子的、把同步延迟做成可见的、把成本变动做成可追溯的,这三件事做到位了,月底对账那张表上的差额自然会缩小。架构选型上,Redis Lua 处理热路径的原子扣减,MySQL 版本号控制异步回调的幂等写入,库存流水日志串联成本核算——每一层都有成熟的开源组件作支撑,剩下的拼装逻辑才是系统工程真正的价值所在。欢迎 star 和 fork,一起来完善这套跨境代购的防超卖方案。

wechat wechat qr