应采用预计算字段+原子更新替代实时聚合,即在商品文档中维护total_stock字段并用$inc原子更新,查询时直取该字段,辅以每日一致性校验作业保障数据最终一致。

为什么直接用 $sum 聚合查实时库存会卡住
大促期间商品 SKU 数量常达百万级,每个 SKU 又关联几十个仓库库存记录。若每次请求都跑 db.inventory.aggregate([{$group: {_id: "$sku", total: {$sum: "$qty"}}}]),MongoDB 会全表扫描所有库存文档,内存占用飙升,响应动辄数秒——这不是慢,是不可用。
根本问题不在聚合语法,而在数据模型没对齐访问模式:你查的是「当前总库存」,但底层存的是「每仓每时的变更流水」。
- 库存变动本质是追加写(如
{sku: "A001", wh: "sh", qty: -2, ts: ISODate("...")}),不是覆盖更新 - 聚合必须遍历全部历史变更才能算出当前值,无法利用索引跳过无关记录
- 并发查询多时,
$sum争抢内存和 CPU,容易触发ExecutionTimeout
用预计算字段 + 原子更新替代实时聚合
把「汇总结果」作为一级字段存在商品主文档里,由库存变更操作同步维护,查询时直取字段,毫秒级返回。
关键在两点:写入时用 $inc 原子更新,读取时只查 db.items.find({sku: "A001"}, {projection: {total_stock: 1}})。
- 商品集合
items中每个文档带字段total_stock: NumberInt,初始为 0 - 扣减库存时,不只写流水,还执行
db.items.updateOne({sku: "A001"}, {$inc: {total_stock: -2}}) - 必须用
updateOne配合$inc,不能先find再set,否则并发下会超卖 - 该字段只反映「当前可用总量」,明细仍保留在
inventory流水集合中,用于对账
如何保证预计算字段和流水数据最终一致
网络分区、进程崩溃会导致 items.total_stock 和 inventory 流水不一致。不能靠人工巡检,得有自动修复机制。
方案是每日凌晨跑一次「一致性校验作业」,只扫当天有变更的 SKU,用 MapReduce 或分片聚合加速:
db.inventory.aggregate([
{$match: {ts: {$gte: yesterday, $lt: today}}},
{$group: {_id: "$sku", delta: {$sum: "$qty"}}},
{$merge: {into: "stock_reconcile", on: "_id", whenMatched: "replace"}}
])
再比对 stock_reconcile 和 items 的差值,对偏差 > 0.1% 的 SKU 触发重算(即用全量流水重新生成 total_stock)。
- 校验作业必须加
readConcern: "majority",避免读到未提交的中间状态 - 不要在校验时直接
update商品文档,先写入临时集合,人工确认后再批量刷入 - 线上严禁运行
db.items.updateMany({}, {$set: {...}})这类无条件全量更新
大促前必须压测的三个边界点
预计算能扛住流量,但压测漏掉下面三点,上线照样崩:
-
items集合的sku字段必须建唯一索引,否则高并发upsert会触发大量写冲突,日志出现WriteConflict - 单个 SKU 的
total_stock值超过NumberLong范围(9E18)时,$inc会静默截断,需在应用层做阈值拦截(如 > 1E9 就告警) - 开启
journal: true且w: "majority"时,副本集写入延迟升高,要提前调大writeConcernMajorityJournalDefault避免超时
最易被忽略的是:预计算字段只解决「读快」,但库存扣减逻辑本身仍有分布式事务问题——比如优惠券叠加、跨仓调拨,这些得靠消息队列+本地事务表兜底,别指望一个 $inc 全包圆。


















