bigywater.tech
MongoDB · 12

ObjectId:12 个字节各是什么,是否严格递增

ObjectId 的 12 个字节各是什么?是不是严格递增?这篇把它拆成时间戳、随机值、计数器三段,在多个进程里实测它的顺序,并确认 _id 缺省时到底是驱动还是服务端生成的。

4 + 5 + 3
ObjectId 的 12 个字节:时间戳、进程内随机值、计数器
100,000
一个进程在不到 1 秒内生成的 ObjectId 数,靠计数器区分
3394 / 7044
3 个进程并发生成时,按创建时间排序后相邻两个 _id 反而变小的对数

三段结构

实测 · 本机执行输出
  十六进制                      时间戳(4)    随机(5)       计数器(3)
  6acc348d25e7f1e741141771  6acc348d  25e7f1e741  141771  = 1316721
  6acc348d25e7f1e741141772  6acc348d  25e7f1e741  141772  = 1316722
  6acc348d25e7f1e741141773  6acc348d  25e7f1e741  141773  = 1316723
  6acc348d25e7f1e741141774  6acc348d  25e7f1e741  141774  = 1316724
  时间戳 1791767693 秒 = 2026-10-12 01:14:53 UTC(pymongo 的 generation_time)

前 4 字节是秒级时间戳(大端),中间 5 字节是进程启动时生成的随机值,后 3 字节是计数器(起点随机,每次 +1)。所以同一进程内一连串 ObjectId 只有计数器在变。

秒精度

实测 · 本机执行输出
  连续生成 100,000 个:覆盖的秒数偏移 [0](相对生成前的整秒);其中第 1 个和最后 1 个的计数器 1316725 → 1416724
  整批在同一个进程里 5 字节随机部分的取值个数: 1
  ObjectId.from_datetime(2026-01-01) = 6955b9000000000000000000(后面 8 字节全 0,常用来做按时间范围的 _id 查询边界)

同一秒生成的 ObjectId 时间部分相同,靠计数器区分;ObjectId.from_datetime 把后 8 字节置 0,可以当按时间范围查询 _id 的边界。

不同进程

实测 · 本机执行输出
  进程 1: 6acc348d 20109c624f 725910  6acc348d 20109c624f 725911
  进程 2: 6acc348d 20dfcae807 96f286  6acc348d 20dfcae807 96f287
  进程 3: 6acc348d 1e3839d63a c66a7e  6acc348d 1e3839d63a c66a7f

每个进程的随机 5 字节和计数器起点都不同,所以多个进程同一秒内生成的 ObjectId 互不冲突,但谁先谁后由随机值而不是时间决定。

计数器回绕

实测 · 本机执行输出
  6acc348d25e7f1e741fffffd  计数器 fffffd
  6acc348d25e7f1e741fffffe  计数器 fffffe
  6acc348d25e7f1e741ffffff  计数器 ffffff
  6acc348d25e7f1e741000000  计数器 000000
  6acc348d25e7f1e741000001  计数器 000001

3 字节计数器到 ffffff 后回绕到 0,这时同一秒内生成的 ObjectId 会比前一个小。

是否严格递增

实测 · 本机执行输出
  3 个进程各生成约 2348 个,共 7044 个 ObjectId;按创建时间(纳秒时钟)排好之后,相邻两个中 _id 反而变小的有 3394 对
  按 _id 排序是否等于按创建时间排序: False
  进程 1 内部是否严格递增: True
  进程 2 内部是否严格递增: True
  进程 3 内部是否严格递增: True

单个进程内严格递增(不回绕时);多个进程并发时,按 _id 排序和按创建时间排序是两回事。不要把 _id 当严格的创建顺序。

谁生成

实测 · 本机执行输出
  test> oid = ObjectId("65a1b2c3d4e5f60718293a4b"); [oid.getTimestamp(), oid.toString(), oid.id.toString("hex").length]
  [ ISODate('2024-01-12T21:44:35.000Z'), '65a1b2c3d4e5f60718293a4b', 24 ]
  pymongo: insert_one 之前 doc = {'y': 2};
  insert_one 之后同一个字典变成 {"y": 2, "_id": ObjectId(...)}: ['_id', 'y'],_id 的类型 ObjectId,与 inserted_id 相同: True
  用原始 insert 命令发送一个没有 _id 的文档 {"z": 1},读回来: ['_id', 'z'],_id 类型 ObjectId
  test> db.c3.aggregate([{$project: {t: {$toDate: "$_id"}, idtype: {$type: "$_id"}}}])
  [
    {
      _id: ObjectId('6acc349052406f65c30f4027'),
      t: ISODate('2026-10-12T01:14:56.000Z'),
      idtype: 'objectId'
    }
  ]

驱动在发送前给文档补上 _id(pymongo 会修改你传入的字典);如果文档到了服务端还没有 _id,服务端也会生成一个 ObjectId。

分步演示

ObjectId 的 12 个字节
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / MongoDB 9.0.2(Homebrew 的 mongod、mongos)/ mongosh 2.12.0 / pymongo 4.18.3;每个实验在临时目录里起全新的 mongod(独立 dbpath,端口 9900 以上,只监听 127.0.0.1,WiredTiger 缓存 0.25 GB),需要副本集或分片集群时在本机起多个进程;命令输出来自真实的 mongosh,批量写入、计时与多线程用 pymongo;计时用 time.perf_counter,数字每次运行会有小幅抖动,ObjectId、时间戳等每次运行都不同。多进程部分是 3 个独立的 Python 进程并行生成并带上纳秒时钟;计数器与随机值每次运行都不同,文中的具体取值来自最后一次运行
    • 没有验证:其它驱动(Java、Go、Node)的 ObjectId 实现细节;跨机器时钟不同步对顺序的影响;fork 之后子进程是否重新生成随机值;2106 年之后 32 位时间戳溢出;_id 用 ObjectId 对索引插入位置(是否追加在 B 树末尾)的影响
    京ICP备2026067502号-1京公网安备11010502063487号