MongoDB中通过$inc操作符在counters集合中原子更新计数器文档(如{"_id":"order_id","seq":1000})是最稳的自增ID实现方式,必须配合upsert:true和returnDocument:"after"确保首次创建与并发安全,并按业务维度拆分计数器避免热点。

用 $inc 更新计数器集合是最稳的自增方式
不要在应用层读+算+写,那是并发冲突的根源。MongoDB 本身不提供“全局自增ID”原语,但可以用一个专用计数器文档 + $inc 原子操作安全实现。关键不是“怎么加”,而是“加在哪、怎么查、怎么索引”。
典型做法是建一个 counters 集合,每条文档代表一个业务序列(如 {"_id": "order_id", "seq": 1000}),然后用:
db.counters.updateOne(
{ _id: "order_id" },
{ $inc: { seq: 1 } },
{ upsert: true }
)注意三点:
- 必须设 upsert: true,否则首次调用会失败
- _id 字段必须是唯一业务标识(不能用模糊条件)
- 该集合要对 _id 建唯一索引(默认就有,但别删)
为什么不能直接用 findAndModify 或 findOneAndUpdate 返回旧值再加?
不是语法错,而是逻辑陷阱。比如你写 db.counters.findOneAndUpdate({ _id: "user_id" }, { $inc: { seq: 1 } }, { returnDocument: "before" }),本意是取旧值当ID用,但返回的是更新前的 seq,而你真正需要的是更新后的结果——它只在响应的 value.seq 字段里。更糟的是,如果网络延迟或重试,你可能拿到重复的旧值。
正确姿势是始终信任 $inc 的原子性,然后用 returnDocument: "after"(或驱动里的 new: true)拿新值:
db.counters.findOneAndUpdate(
{ _id: "user_id" },
{ $inc: { seq: 1 } },
{ upsert: true, returnDocument: "after" }
)返回文档里的 seq 就是你该用的 ID。
并发高时,counters 集合本身会成瓶颈吗?
会,但比你想象中轻量得多。单个计数器文档在高并发下确实可能成为热点,因为所有请求都争抢同一行。这不是 MongoDB 锁粒度问题(文档级锁已足够细),而是设计问题。
缓解方法只有两个:
- 按业务维度拆分计数器:不要只用一个
"global",而是"order_202607"、"user_vip"这类带时间或角色前缀的_id - 避免在核心链路强依赖自增ID:比如订单号不必严格连续,可用
时间戳 + 随机后缀 + 计数器混合生成,把压力分散
千万别说“加 Redis 缓存计数器”——那又引入跨系统一致性难题,得不偿失。
ObjectId 能当自增ID用吗?
不能。ObjectId 是时间+随机+计数器的组合,天生不连续、不可预测、不能排序体现插入顺序(秒级精度,同秒内顺序无保证)。有人截取时间戳部分做“伪自增”,但漏掉的正是那 5 字节随机值和 3 字节计数器——它们才是防冲突的关键,也是你无法控制的部分。
如果你真需要“可读、有序、自增”的 ID,老实用计数器方案;如果只是要唯一主键,ObjectId 完全够用,且零配置、零风险。混用两者(比如用 ObjectId 当主键、另存一个自增字段)反而增加存储和索引开销。
最后提醒一句:所有自增逻辑必须收口到数据库层原子操作,任何在应用内存里维护计数、靠本地变量递增的做法,在多实例部署下第一天就崩。


















