跳到主要内容

设计分布式 ID 生成器

问题

如何在分布式系统中生成全局唯一、有序的 ID?

答案

一、为什么需要分布式 ID

单库时可以用数据库自增 ID。但在分库分表、微服务场景下:

  • 多个库的自增 ID 会重复
  • 需要在插入前就获得 ID(异步消息、前端预览等)
  • 自增 ID 暴露业务量(如订单量),有安全隐患

二、方案对比

方案有序性性能依赖长度
UUID❌ 无序⭐⭐⭐⭐⭐36 字符
雪花算法✅ 趋势递增⭐⭐⭐⭐⭐时钟64 bit
号段模式✅ 递增⭐⭐⭐⭐数据库可变
Redis INCR✅ 递增⭐⭐⭐⭐Redis可变
数据库自增✅ 递增⭐⭐数据库可变

三、UUID

import { randomUUID } from 'crypto';

const id = randomUUID();
// 'f47ac10b-58cc-4372-a567-0d02b2c3d479'

优点:无依赖、高性能、全球唯一。

缺点

  • 36 字符太长,索引效率低
  • 无序:B+ 树索引频繁分裂,写入性能差
  • 不可读,无法排序
UUID v7(推荐)

UUID v7 在前 48 bit 嵌入时间戳,保持趋势递增,解决了 B+ 树分裂问题:

// uuid v7(需要 uuid 库 v9+)
import { v7 as uuidv7 } from 'uuid';
const id = uuidv7();
// '018f3e4a-1b2c-7d4e-8a9b-1c2d3e4f5a6b'
// ^^^^^^^^ 时间戳部分,保证递增

四、雪花算法(Snowflake)

Twitter 提出的 64 bit ID 生成算法:

┌─ 1 bit ─┬──────── 41 bit ─────────┬── 10 bit ──┬── 12 bit ──┐
│ 符号位 0 │ 时间戳(毫秒级) │ 机器 ID │ 序列号 │
└──────────┴─────────────────────────┴────────────┴────────────┘

时间戳:41 bit 可用 69 年
机器 ID:10 bit 支持 1024 个节点(5 bit 数据中心 + 5 bit 机器)
序列号:12 bit 每毫秒 4096 个 ID
理论上限:409.6 万 ID/秒
class Snowflake {
private sequence = 0n;
private lastTimestamp = -1n;

// 起始时间(2024-01-01)
private readonly EPOCH = 1704067200000n;
private readonly MACHINE_ID_BITS = 10n;
private readonly SEQUENCE_BITS = 12n;

private readonly machineId: bigint;
private readonly maxSequence = (1n << this.SEQUENCE_BITS) - 1n;

constructor(machineId: number) {
this.machineId = BigInt(machineId);
}

nextId(): bigint {
let timestamp = BigInt(Date.now());

if (timestamp === this.lastTimestamp) {
// 同毫秒,序列号递增
this.sequence = (this.sequence + 1n) & this.maxSequence;
if (this.sequence === 0n) {
// 序列号溢出,等待下一毫秒
timestamp = this.waitNextMillis(this.lastTimestamp);
}
} else {
this.sequence = 0n;
}

this.lastTimestamp = timestamp;

return (
((timestamp - this.EPOCH) << (this.MACHINE_ID_BITS + this.SEQUENCE_BITS)) |
(this.machineId << this.SEQUENCE_BITS) |
this.sequence
);
}

private waitNextMillis(last: bigint): bigint {
let ts = BigInt(Date.now());
while (ts <= last) {
ts = BigInt(Date.now());
}
return ts;
}
}

const snowflake = new Snowflake(1);
const id = snowflake.nextId();
// 例如:7169537123456789012n
时钟回拨问题

服务器时钟回拨会导致 ID 重复。解决方案:

  1. 拒绝生成:检测到回拨直接抛异常
  2. 等待追上:回拨小于阈值(如 5ms)则等待
  3. 预留位:用 2~3 bit 做时钟序列号,回拨时递增

五、号段模式

-- 号段分配表
CREATE TABLE id_generator (
biz_tag VARCHAR(64) PRIMARY KEY, -- 业务标识
max_id BIGINT NOT NULL, -- 当前已分配的最大 ID
step INT NOT NULL DEFAULT 1000, -- 每次获取的号段长度
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 初始化
INSERT INTO id_generator (biz_tag, max_id, step) VALUES ('order', 0, 1000);

-- 获取号段(原子操作)
UPDATE id_generator SET max_id = max_id + step WHERE biz_tag = 'order';
-- 返回 [old_max_id + 1, old_max_id + step]
class SegmentIdGenerator {
private currentId: number;
private maxId: number;

async nextId(bizTag: string): Promise<number> {
if (this.currentId >= this.maxId) {
await this.loadSegment(bizTag); // 号段用完,重新申请
}
return ++this.currentId;
}

private async loadSegment(bizTag: string) {
// 双 Buffer:提前加载下一号段,避免阻塞
const result = await db.execute(
'UPDATE id_generator SET max_id = max_id + step WHERE biz_tag = ? RETURNING max_id, step',
[bizTag]
);
this.currentId = result.max_id - result.step;
this.maxId = result.max_id;
}
}

美团 Leaf:号段模式 + 双 Buffer 预加载 + 雪花算法模式可选。


常见面试问题

Q1: 雪花算法和号段模式如何选择?

答案

维度雪花算法号段模式
依赖无外部依赖依赖数据库
有序性趋势递增严格递增
性能极高(本地生成)高(批量获取)
可读性64 bit 长数字短序列号
风险时钟回拨数据库单点
  • 对有序性要求不严格 → 雪花算法
  • 需要短 ID、严格递增 → 号段模式

Q2: 为什么无序 UUID 不适合做 MySQL 主键?

答案

MySQL InnoDB 使用 B+ 树组织聚簇索引(主键索引)。无序 UUID 导致:

  1. 页分裂:新记录随机插入,导致 B+ 树频繁分裂
  2. 碎片化:页利用率低(通常只有 50%~70%)
  3. 写入慢:随机 IO 远慢于顺序 IO

有序 ID(自增、雪花、UUID v7)总是追加到 B+ 树尾部,避免分裂。

Q3: 如何保证分布式 ID 不重复?

答案

  • UUID:128 bit 随机数,碰撞概率极低(可忽略)
  • 雪花算法时间戳 + 机器 ID + 序列号,同一毫秒同一机器通过序列号区分
  • 号段模式:数据库 UPDATE 的原子性保证不同节点拿到不同号段

相关链接