$inc 是 MongoDB 原子增减字段值的核心操作符,直接在服务端完成读-改-写,避免竞态;要求字段为数字类型,支持自动初始化、负数递减,但需规避浮点精度、类型不一致及分片路由等问题。

用 $inc 实现字段值的原子增减
MongoDB 原生支持原子级计数器,核心就是 $inc 操作符。它直接在服务端完成读-改-写,无需应用层加锁或重试,天然避免竞态。只要目标字段是数字类型(int、long、double),就能安全使用。
常见错误是先 find 再 update:这会引入竞态窗口,多个并发请求可能读到同一旧值,导致最终结果少加。必须跳过“读取再计算”这一步。
-
$inc的值可以是正数、负数或零;传负数等价于“减” - 如果字段不存在,
$inc会自动初始化为该增量值(比如对空文档执行{$inc: {count: 1}},结果是{count: 1}) - 不建议对浮点字段用
$inc做精确计数,因 IEEE 754 精度问题可能导致意外舍入
确保计数器字段存在且类型一致
一旦字段被写成字符串(比如误用 $set 赋了 "1"),后续 $inc 会报错:Cannot increment with non-number。MongoDB 不做隐式类型转换。
所以建模阶段就得明确约定字段类型。上线后若发现类型混乱,得用聚合管道配合 $convert 或写脚本批量修正——但这是补救,不是方案。
- 插入新文档时,可显式初始化计数器字段为数字:例如
{_id: "user_123", login_count: 0} - 用
updateOne+upsert: true可兼顾“存在则增、不存在则建”:db.users.updateOne( { _id: "user_123" }, { $inc: { login_count: 1 } }, { upsert: true } ) - 避免在同一个更新中混用
$inc和$set修改同一字段(虽语法允许,但逻辑易混淆)
高并发下仍需注意的边界情况
$inc 是原子的,但“原子”只保证单次操作不被中断,并不解决业务层面的逻辑冲突。比如库存扣减,不能只靠 $inc: {stock: -1} 就完事——得同时检查扣减后是否 stock >= 0。
这时要用 updateOne 的过滤条件做前置校验:
db.inventory.updateOne(
{ _id: "item_A", stock: { $gt: 0 } },
{ $inc: { stock: -1 } }
)
返回的 result.matchedCount 为 0 就说明库存不足,应用层据此处理失败分支。
- 不要依赖
getLastError或旧版writeConcern来判断计数是否成功;看matchedCount和modifiedCount更可靠 - 分片集合上
$inc仍原子,但要求查询条件能路由到单一分片(即含分片键),否则会报错 - 如果计数器要支持超大数值(如 > 2^53),考虑用
Long类型(Java/C# 驱动)或Int64(Python PyMongo),避免 JS number 精度丢失
为什么不用 findAndModify 或事务?
findAndModify(或现代的 findOneAndUpdate)确实能返回更新前/后的文档,但它比纯 $inc 多一次数据传输和序列化开销,纯计数场景没必要。
事务更重——仅当你要把计数器更新和其他写操作(比如日志插入、状态变更)绑成一个原子单元时才需要。单独计数,事务是过度设计,还带来性能和复杂度代价。
-
findOneAndUpdate适合你需要立刻拿到新值的场景(比如生成带序号的订单号),但多数后台计数只需“确保加了”,不关心返回值 - 开启事务意味着要选主节点、维护会话、处理可能的
TransactionTooLarge错误,而$inc单操作无此负担 - 如果你正在用副本集,确认
writeConcern设置合理(比如{w: "majority"}),否则网络分区时可能丢计数
实际用的时候,最常漏掉的是分片键约束和类型初始化。写完更新语句,顺手跑个 db.collection.findOne({ _id: xxx }) 看一眼字段类型,比事后查日志快得多。











