objectid不是自增id,因其由时间戳、机器码、进程id和计数器拼接而成,仅保证唯一性和大致时间序,不满足严格递增、连续、可读、可范围查询等业务需求;需用counters集合配合findoneandupdate原子操作模拟。

MongoDB 本身不支持自增主键,所谓“优雅管理”,本质是绕过缺失功能、用原子操作+业务约定来模拟递增语义,同时规避并发冲突和性能瓶颈。
为什么不能直接用 ObjectId 当自增ID
ObjectId 是唯一且带时间戳的,但它不是全局递增的:同一秒内不同机器/进程生成的 ID 在字典序上可能乱序;跨秒时更无法保证数值连续。业务上要求“第1单、第2单、第3单”这种严格顺序时,ObjectId 的字符串值(如 "53102b43bf1044ed8b0ba36b")既不可读,也无法做范围查询或分页跳转。
常见错误现象:db.orders.find().sort({_id: 1}) 看起来有序,但实际是按时间戳+随机段排序,不是插入序;导出为 Excel 后用户发现“订单号”跳变、断号,误以为漏单。
使用场景限制:
- 仅适合内部追踪、日志类文档,不暴露给用户或下游系统
- 不能用于分页参数(如
?last_id=100),因为_id不是整数 - 无法做
WHERE id BETWEEN 100 AND 200这类 SQL 风格查询
用 counters 集合 + findAndModify 实现原子递增
这是最通用、兼容性最好的方案,适用于 MongoDB 3.2+ 所有部署模式(副本集、分片集群均可)。核心是靠 findAndModify(或现代等价的 findOneAndUpdate)的原子性,避免竞态条件。
实操要点:
- 先创建计数器集合:
db.createCollection("counters") - 初始化序列文档:
db.counters.insertOne({_id: "order_id", seq: 0})—— 注意字段名统一用seq,别混用sequence_value或next - 获取并递增 ID 的函数必须用
upsert: true和new: true:db.counters.findOneAndUpdate({_id: "order_id"}, {$inc: {seq: 1}}, {upsert: true, returnDocument: "after"}) - 返回值取
.seq字段,直接赋给新文档的_id或业务字段(如order_no)
容易踩的坑:
- 忘记设
upsert: true,首次调用会报no matching document - 在分片集群中,
counters集合必须以_id为分片键(即不分片),否则findOneAndUpdate跨片不保证原子性 - 高并发下频繁读写单个计数器文档会成为热点,可考虑按天/按业务线拆分(如
"order_id_202610")
Node.js 驱动中封装 getNextSequence 时的关键细节
不要在应用层做“先查再更新”,必须用驱动提供的原子方法。以官方 Node.js 驱动 v6+ 为例:
async function getNextSequence(db, name) {
const counters = db.collection('counters');
const result = await counters.findOneAndUpdate(
{ _id: name },
{ $inc: { seq: 1 } },
{ upsert: true, returnDocument: 'after' }
);
return result.value.seq;
}
为什么这么做:
-
returnDocument: 'after'确保拿到递增后的值,不是旧值 - 用
async/await避免回调地狱,也方便后续加重试逻辑 - 函数接收
db实例而非硬编码连接,利于测试和多租户隔离
性能影响:
- 单次调用约 2–5ms(局域网),比生成
ObjectId慢一个数量级 - 若每秒插入超 500 条,建议启用连接池复用,并监控
counters集合的queryExecutor指标 - 不推荐在事务中嵌套此操作(MongoDB 事务内不允许跨集合原子更新)
什么时候该放弃自增ID,回归 ObjectId
当业务对“连续”“可预测”没有强依赖时,强行上自增ID反而增加复杂度。典型信号包括:
- ID 只用于 API 内部关联(如
user_id关联order),不展示、不打印、不人工输入 - 需要水平扩展写入吞吐,且无法接受单点计数器瓶颈
- 已有系统用
ObjectId运行多年,迁移成本远高于收益
更务实的做法是:用 ObjectId 做主键保障唯一和分布式安全,另加一个可选的、非唯一的 display_id 字段存格式化编号(如 "ORD-2026-000123"),由应用层生成并确保幂等——这样既保留 MongoDB 优势,又满足前端友好需求。
真正难的不是实现递增,而是判断“是否真的需要它”。多数团队卡在第一步:没想清楚这个 ID 到底被谁用、怎么用、出错时谁担责。











