直接 $inc 会引发写冲突,因其原子性仅限于操作本身,而业务层依赖非原子读取判断更新条件(如先查后判再更新),导致竞态;需将约束条件嵌入 update 的 $expr 表达式或复合匹配条件中实现原子校验。

为什么直接 $inc 会引发写冲突?
很多人以为 $inc 天然线程安全,就直接在高并发场景(比如点赞数、浏览量、库存扣减)里无脑用 collection.updateOne({ _id: id }, { $inc: { count: 1 } })。但问题不在 $inc 本身——它确实是原子的;真正冲突源是业务逻辑层对“是否允许更新”的判断依赖了非原子读取。比如先 findOne 拿当前值,再 if-else 判断是否超限,最后才 $inc,中间窗口期就被其他请求钻空了。
计数器字段必须和业务规则耦合建模
单纯一个 count 字段撑不住真实业务。比如库存扣减,不能只看数字是否 >0,还要考虑:是否已下单未支付、是否被冻结、是否跨分片一致性。解决方案不是加锁,而是把约束条件全塞进 update 条件里:
- 用
$expr做运行时校验,例如{ $expr: { $gt: [ "$stock", 0 ] } },让 MongoDB 在引擎层拒绝非法更新 - 把状态字段(如
status: "available")、时间戳(如freezeUntil: { $lt: new Date() })一起作为 match 条件,避免应用层竞态 - 如果涉及多字段联动(如“扣库存同时改订单状态”),必须用
findOneAndUpdate+returnDocument: 'after',否则拿不到最新快照继续下一轮逻辑
高频计数器要拆分写入路径
单文档扛不住每秒上千次 $inc?别硬扛。把“热字段”从主文档里拎出来,单独建集合,用文档级原子性保底:
- 建
t_counter_shard集合,每条文档形如{ _id: "article_123_views", shard: 0, value: 12345 },shard 取 0~9 做哈希分散 - 写入时随机选一个
shard执行updateOne({ _id: key, shard: rand }, { $inc: { value: 1 } }, { upsert: true }) - 读总数时用
aggregate+$group汇总,或定期合并到主文档(异步 job,不卡主线程)
这个模式牺牲一点读一致性,换来写吞吐翻倍,且完全规避 WiredTiger X 锁排队 —— 因为每个 shard 文档互不干扰。
Driver 4.x+ 的 returnDocument 默认值是陷阱
Node.js 驱动 4.x+ 默认 returnDocument: 'before',意味着你调 findOneAndUpdate 拿到的是旧值。如果后续逻辑依赖新值(比如扣完库存立刻发 MQ),就会出错。必须显式写:
db.collection.findOneAndUpdate(
{ _id: id, stock: { $gt: 0 } },
{ $inc: { stock: -1 } },
{ returnDocument: 'after', projection: { stock: 1 } }
)
漏掉这句,重试逻辑就永远在拿过期数据撞墙,线上可能表现为“扣了一半库存就卡住”。











