ObjectId 规则
| 时间戳 | 机器码 | PID | 计数器 |
|---|---|---|---|
| 0,1,2,3 | 4,5,6 | 7,8 | 9,10,11 |
- 前四位是时间戳,可以提供秒级别的唯一性。
- 接下来三位是所在主机的唯一标识符,通常是机器主机名的散列值。
- 接下来两位是产生 ObjectId 的 PID,确保同一台机器上并发产生的 ObjectId 是唯一的。
- 前九位保证了同一秒钟不同机器的不同进程产生的 ObjectId 时唯一的。
- 最后三位是自增计数器,确保相同进程同一秒钟产生的 ObjectId 是唯一的。
12 字节四段式结构讲清了”如何唯一”,但 ObjectId 的设计还有三个值得展开的层面:为什么这样排布(有序性设计)、新版规范的演进(机器码随机化与计数器扩展)、以及它在工程中的用法与边界。本文补全。
结构再确认(12 字节 = 24 hex 字符)
| 段 | 字节 | 作用 |
|---|---|---|
| 时间戳 | 4 | 秒级 Unix 时间,大端序(高位在前) |
| 机器标识 | 5(新版) | 原 3 字节机器哈希 + 2 字节 PID 合并演进为 5 字节随机值(进程级) |
| 计数器 | 3 | 进程内自增(随机初值),同秒内区分 |
新版(MongoDB 3.4+ 规范)把”机器码+PID”两段合并为 5 字节随机进程标识、计数器扩为 3 字节并随机初值——降低可预测性、提升单秒容量(每进程每秒 1677 万)。
排布的设计意图:有序性
时间戳放在最高位且大端,使 ObjectId 天然按生成时间近似有序:
- B-Tree 索引(MongoDB _id 默认索引)插入近似尾插,页分裂少——与雪花算法”时间戳高位”同一思想
- 可直接从 ID 提取生成时间(前 8 hex 转秒),运维排查免查 createdAt
- 近似而非严格有序:同秒内由随机进程标识与计数器决定次序,跨进程不保证全局时序
代价提醒:不要拿 ObjectId 当严格时序排序键(同秒乱序),业务排序应显式用时间字段+索引。
唯一性的概率边界
- 同进程同秒:计数器保证(3 字节回绕前 1677 万次/秒足够)
- 同秒跨进程:5 字节进程标识区分(随机碰撞概率按生日界估算,部署规模下可忽略;极端规模可自定义 machine identifier)
- 跨秒:时间戳区分
- 理论冲突存在但工程可忽略;MongoDB 驱动层生成,无中心发号器——这是它相对雪花/号段的架构优势(零依赖、客户端本地生成)
工程用法与边界
用法:
- Mongo 默认 _id:不指定即驱动生成;索引友好性已内建
- 关系库/日志中作业务 ID:24 字符字符串存储(或 12 字节 binary 省空间)
- 提取时间:
ObjectId.getTimestamp()(驱动 API)或前 8 hex 手动解析
边界与注意:
- 可预测性:时间戳+递增计数器部分可枚举(老版 PID 段更易猜);对外暴露的资源 ID 若需防枚举,应另加随机 slug 或权限校验,不能依赖 ID 不可猜
- 分片键选择:MongoDB 分片集群中 ObjectId 作 shard key 会导致写热点集中(时间序尾插)——分片键应选哈希分片(hashed shard key)或业务散列键
- 与雪花对比:雪花 64 bit 数值型(关系库友好、含毫秒),ObjectId 96 bit 字节型(零依赖、含秒);关系库主键选雪花系,Mongo 生态用 ObjectId,各归其位
设计思想提炼
ObjectId 与雪花、号段同属”结构化 ID“家族:把唯一性来源拆成”时间(粗粒度区分)+ 空间标识(机器/进程)+ 序列(细粒度区分)”三段,用排布顺序换索引友好性。理解这个三段式模板,看任何 ID 方案(雪花、ObjectId、ULID——ULID 即”字典序友好版 ObjectId+CUID 思想”)都是同一公式的变体。
ObjectId 的规则一句话:12 字节 = 秒级时间(高位保序)+ 进程标识(跨进程唯一)+ 计数器(同进程唯一),本地生成、零依赖、近似有序;记住排布动机与三个边界(非严格时序、可预测、分片热点),使用与选型就不会踩坑。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 非鱼小站!
评论
WalineDisqus











