图 1:本文封面,12 字节三段布局与三条性质。
先说明属性:这是一篇经典题解析,题目出自本地历史面试资料整理,与任何公司的当前招聘无关。「ObjectID 由哪些部分组成」表面是背结构,实际考的是你能否从 12 个字节的分工里推出它的两条核心性质:为什么分布式环境下能保证唯一,又为什么天然大致有序。这两条性质决定了它在真实业务里能干什么、不能干什么。
一句话结论:12 字节 = 4 字节秒级时间戳 + 5 字节随机值 + 3 字节自增计数器。
按 MongoDB 官方文档的定义,ObjectId 共 12 字节(96 位),由三段拼接而成。第一段是 4 字节时间戳,记录文档创建时刻距 Unix 纪元的秒数。第二段是 5 字节随机值,每个客户端进程在启动时随机生成一次,用于区分不同的机器与进程;进程重启或主节点切换时会重新生成。第三段是 3 字节自增计数器,进程内每生成一个 ID 就加 1,初始值也是随机的。还有一个容易被忽略的细节:时间戳和计数器按大端序存储(高位字节在前),这与 BSON 里其他数值类型的小端序相反——这个设计正是为了让字典序与时间序一致。
三段各解决一个问题,拼出两条性质。
时间戳打头解决「有序」:ID 的字典序约等于创建时间序,按 _id 排序就近似按时间排序,对索引来说新文档永远追加在末尾,写入友好。随机值解决「分布式」:不同机器、不同进程各自持有不同的随机段,生成 ID 完全在本地完成,不需要任何中心发号器或跨节点协调。计数器解决「同秒并发」:同一进程同一秒内生成的 ID 靠计数器拉开,3 字节即 24 位,上限 2²⁴ = 16,777,216,约每秒 1677 万个——单进程很难打满。三段合起来,就是「零协调成本换分布式唯一 + 大致有序」的经典取舍。
用代码把一个字节串拆开验证(Node.js 22.15.0,node objectid.js):
function parseObjectId(hex) {
const ts = parseInt(hex.slice(0, 8), 16); // 前 4 字节:Unix 秒(大端)
const rand = hex.slice(8, 18); // 中 5 字节:随机值
const cnt = parseInt(hex.slice(18, 24), 16); // 后 3 字节:计数器
return { unixSeconds: ts, random: rand, counter: cnt };
}
console.log(parseObjectId('507f1f77bcf86cd799439011'));
console.log('计数器单秒上限 =', (2 ** 24).toLocaleString('en-US'));
const t1 = 1800000000; // 任一 Unix 秒
const id1 = t1.toString(16).padStart(8, '0') + '8cd4e507b3' + '9a3b21';
const id2 = (t1 + 60).toString(16).padStart(8, '0') + '8cd4e507b3' + '9a3b21';
console.log('id1 =', id1, '\nid2 =', id2);
console.log('晚 60 秒生成的 ID 字典序更大:', id1 < id2);
实测输出:
{ unixSeconds: 1350508407, random: 'bcf86cd799', counter: 4427793 }
计数器单秒上限 = 16,777,216
id1 = 6b49d2008cd4e507b39a3b21
id2 = 6b49d23c8cd4e507b39a3b21
晚 60 秒生成的 ID 字典序更大: true
随手拿来一个十六进制串,前 8 位翻成十进制就是创建时刻的 Unix 秒数、中间 10 位是随机段、最后 6 位是计数器,结构一目了然;两个相差 60 秒的 ID 直接比较字典序就分出了先后。
图 2:位布局拆解与「唯一 + 有序」性质的推导链。
用对边界,避开三个坑。
第一,时间戳是秒级精度,同一秒内的大量文档只能靠计数器和随机段区分,排序在秒内有细微不确定性,别把 _id 当精确时间戳用,需要精确时间就该单独存字段。第二,「大致有序」依赖各节点时钟大致同步,时钟严重漂移的机器会造出乱序的 ID。第三,计数器上限是单进程每秒约 1677 万,真的触及这个量级的生成并发时,就该考虑雪花 ID 等更宽的方案——这属于主键选型的横向话题,本文不展开。AI 应用里这条知识很日常:会话记录、日志文档的默认主键就是 ObjectID,理解了有序性,才能放心用 _id 做按时间范围查询和「取上一页最后一条的 _id 向下翻页」这种游标分页。
面试现场的答题结构。
先报 4+5+3 的三段布局,再逐段讲职责:时间戳定顺序、随机值保分布、计数器兜同秒并发。然后主动推性质:本地生成、无中心协调,所以分布式唯一;时间戳打头加大端序,所以大致有序、索引追加友好。最后用一个十六进制示例现场拆段收尾,再补一句边界——秒级精度和每秒千万级的计数器上限。答到「这是用计数器上限换零协调实现的取舍」这一层,就把背结构升成了讲设计。你在业务里用 ObjectID 做过范围查询或分页吗?有没有踩过秒级精度的坑?欢迎聊聊。