ObjectId由客户端生成,服务端仅校验并补全;Snowflake需应用层实现,核心难点是跨节点sequence协调与workerId唯一性保障。

ObjectId 是客户端生成的,不是服务端分配的
很多人误以为 _id 是 MongoDB 服务端在插入时统一生成的,其实不然。从 2.2 版本起,所有官方 Driver(Java/Python/Node.js 等)默认在客户端生成 ObjectId,再随文档一起发给服务端;服务端只做校验:若文档不含 _id 字段,才自动生成一个——但这个“自动生成”仍是按 ObjectId 规则构造,不是中心化分配。
这意味着:你完全可以在插入前手动指定 _id,只要类型合法(ObjectId 或其它 BSON 类型),MongoDB 就接受。这也是自定义 ID 的前提。
- 不加干预时,
ObjectId天然满足分布式唯一、时间粗略有序(秒级)、12 字节紧凑 - 但它不支持毫秒级时间排序,机器 ID 和进程 ID 在容器或云环境里可能重复(比如多实例共享同一镜像+相同 PID)
- 计数器是每进程独立维护的,重启后重置,高并发下短时间密集写入仍可能撞上相同时间戳+相同计数器(概率极低但非零)
Snowflake ID 必须自己实现并控制序列号原子性
MongoDB 本身不提供 Snowflake 支持,所谓“MongoDB 中用雪花 ID”,本质是你在应用层生成一个 long 或 string 类型的 ID,然后赋给文档的 _id 字段。难点不在生成逻辑,而在**序列号(sequence)的跨进程/跨节点协调**。
常见错误是直接在内存里用 AtomicLong 或 JS 的 let seq = 0 ——这只能保证单进程内不重复,多实例部署时必然冲突。
- 正确做法是把 sequence 存在 MongoDB 自身(如
counters集合),用findAndModify或findOneAndUpdate原子递增 - 必须为每个 worker 分配唯一
workerId,不能靠随机或 PID——云环境 PID 经常重复,MAC 地址在容器里不可靠 - 时间戳部分要减去自定义 epoch(如
2020-01-01),否则 41 位不够存到 2106 年之后的时间 - 注意 JavaScript 的
Number是双精度浮点,无法精确表示大于2^53的整数,64 位 Snowflake ID 在 JS 里建议存为string或用BigInt
性能和索引影响:ObjectId 更轻量,Snowflake 需额外 IO
ObjectId 插入零额外开销:生成快、体积小、BSON 原生支持、索引结构友好(B-tree 上天然按时间局部聚集)。
Snowflake 模式下,每次生成新 ID 都要先查改一次 counters 集合(即使用了 upsert,也至少一次 round-trip)。在高吞吐写入场景(如每秒万级文档),这个瓶颈比想象中明显。
- 如果用
findOneAndUpdate+$inc,务必确保counters集合有{ _id: 1 }索引(默认就有) - 不要把所有 worker 共用一个
_id(如"snowflake"),否则变成单点锁;应按workerId分片,例如{ _id: "worker_1" } - 考虑缓存一批 sequence(比如预取 1000 个),减少 DB 调用频次——但宕机可能丢失未用完的部分,需权衡可靠性
什么时候该放弃 ObjectId 改用 Snowflake?
只有当业务明确要求以下至少一项时,才值得引入 Snowflake:
- 需要毫秒级时间顺序,且依赖 ID 直接排序(比如分页拉取最新 N 条,不用额外
created_at字段) - 下游系统(如 Kafka、ES、MySQL)要求主键是数字类型,而 ObjectId 的 12 字节二进制或 24 字符 hex 不被接受
- 已有 Snowflake ID 生态(如公司统一 ID 服务),MongoDB 只是其中一个存储节点,必须保持 ID 格式一致
其他情况,比如只是“觉得 ObjectId 不够直观”或“想和其他系统 ID 长度统一”,都不构成切换理由。ObjectId 的设计已足够健壮,强行替换反而增加故障面——尤其是序列号管理那几行代码,最容易出错。


















