MongoDB ObjectId

MongoDB ObjectId 完全解析(2025 最新版)

核心结论ObjectId 是 MongoDB 的 默认主键12 字节全局唯一自带时间戳,是高并发系统的基石。


一、ObjectId 结构(12 字节)

字节内容说明
0-3时间戳秒级 Unix 时间(从 1970 开始)
4-6机器标识主机唯一标识(MD5 前 3 字节)
7-8进程 ID进程唯一标识
9-11计数器自增计数器(随机初始值)
示例:507f1f77bcf86cd799439011
│        │        │        │
│        │        │        └─ 3 字节计数器(16777217)
│        │        └────────── 2 字节 PID
│        └─────────────────── 3 字节机器码
└─────────────────────────── 4 字节时间戳(2012-10-15 12:00:00)

二、为什么用 ObjectId?(5 大优势)

优势说明
1. 自带时间戳可排序、可提取创建时间
2. 全局唯一分布式系统无需协调
3. 轻量高效12 字节 vs UUID 16 字节
4. 自增趋势利于 B-tree 索引插入
5. 可解析无需查询即可提取信息

三、ObjectId 生成原理(防碰撞)

ObjectId = timestamp + machine + pid + counter
  • 时间戳:秒级,同一秒内可能重复
  • 机器 + PID:区分不同服务器/进程
  • 计数器:每秒最多 16,777,216 个(2^24)

结论每秒每台机器最多 1677 万 ID,远超实际需求


四、Node.js 中使用 ObjectId

import { ObjectId } from 'mongodb';

// 1. 创建
const id = new ObjectId();
console.log(id.toString()); // "507f1f77bcf86cd799439011"

// 2. 解析时间
console.log(id.getTimestamp()); // 2012-10-15T12:00:00.000Z

// 3. 字符串转 ObjectId
const objId = new ObjectId("507f1f77bcf86cd799439011");

// 4. 查询
db.users.find({ _id: new ObjectId("507f1f77bcf86cd799439011") })

五、Mongoose 中使用

const userSchema = new Schema({
  name: String,
  // _id 自动生成 ObjectId
});

const User = mongoose.model('User', userSchema);

// 插入
const user = await User.create({ name: 'Alice' });
console.log(user._id); // ObjectId("...")

// 查询
const found = await User.findById("507f1f77bcf86cd799439011");

六、提取 ObjectId 时间(实用工具)

// Node.js 提取创建时间
function getObjectIdTimestamp(id) {
  return new Date(id.getTimestamp());
}

// 或手动解析(字符串)
function parseObjectIdTime(hexStr) {
  const timestamp = parseInt(hexStr.substring(0, 8), 16);
  return new Date(timestamp * 1000);
}

console.log(parseObjectIdTime("507f1f77bcf86cd799439011"));
// 2012-10-15T12:00:00.000Z

七、ObjectId vs 其他主键对比

类型大小唯一性可排序插入性能推荐
ObjectId12BGlobalYesFast5 stars
UUID v416BGlobalNoSlow3 stars
自增 ID8BLocalYesFast2 stars(需协调)
雪花 ID8BGlobalYesFast4 stars(需实现)

八、常见问题与解决方案

1. 如何自定义 _id

// 插入时指定
db.users.insertOne({ _id: "user_001", name: "Alice" })

// Mongoose 关闭自动生成
const schema = new Schema({ name: String }, { _id: false });

2. 如何避免 _id 冲突?

// 使用 upsert + $setOnInsert
db.users.updateOne(
  { email: "a@x.com" },
  { $setOnInsert: { _id: new ObjectId(), createdAt: new Date() } },
  { upsert: true }
)

3. 如何批量生成 ObjectId

// 生成 100 个
const ids = Array.from({ length: 100 }, () => new ObjectId());

九、生产级最佳实践

场景推荐
默认主键使用 ObjectId
需要业务 ID额外字段 userId: "U123"
分片键_idhashed _id
时间范围查询_id 代替 createdAt
// 用 _id 查某天数据(无需额外字段)
const start = new ObjectId(Math.floor(Date.now() / 1000).toString(16) + "0000000000000000");
const end = new ObjectId(Math.floor(Date.now() / 1000 + 86400).toString(16) + "0000000000000000");

db.logs.find({
  _id: { $gte: start, $lt: end }
})

十、ObjectId 工具库

// 安装
npm install bson-objectid

// 使用
import ObjectId from 'bson-objectid';
const id = new ObjectId();

十一、终极案例:高并发日志系统

// 插入日志(_id 自带时间)
db.logs.insertOne({
  level: "info",
  message: "User login",
  user_id: "u123",
  ip: "1.2.3.4"
  // _id 自动生成,包含时间
})

// 查询今天日志
const today = new Date();
today.setHours(0, 0, 0, 0);
const startId = new ObjectId(Math.floor(today / 1000).toString(16) + "0000000000000000");

db.logs.find({ _id: { $gte: startId } })

总结:ObjectId 黄金三问

问题答案
1. 能不能不用?可以,但需自己保证唯一性
2. 能不能自定义?可以,但建议保留 _id
3. 能不能排序?天然支持(时间递增)

你现在可以:

// 一行生成带时间的唯一 ID
const id = new ObjectId();

告诉我你的业务,我帮你设计主键:

  1. 业务模型(用户、订单、日志?)
  2. 是否需要业务 ID
  3. 是否分片

回复 3 个关键词,我生成完整主键方案 + 查询优化!

文章已创建 2371

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部