分布式系统中,传统数据库自增 ID 无法满足分库分表后的全局唯一需求。本文对比分析主流分布式 ID 生成方案。
1. 方案对比总览
| 方案 | 趋势 | 唯一性 | 性能 | 长度 | 可读性 | 依赖 |
|---|---|---|---|---|---|---|
| UUID v4 | 无 | 概率唯一 | 极高 | 36 字符 | 差 | 无 |
| Snowflake | 时间递增 | 绝对唯一 | 极高 | 64bit | 中 | 时钟 |
| 号段模式 | 趋势递增 | 绝对唯一 | 高 | 64bit | 好 | 数据库 |
| Leaf | 趋势递增 | 绝对唯一 | 极高 | 64bit | 中 | 数据库/ZK |
| Ulid | 时间递增 | 概率唯一 | 极高 | 26 字符 | 较好 | 无 |
2. 雪花算法 (Snowflake)
Twitter 开源,64bit 长整型,结构如下:
0 | 0000000000 0000000000 0000000000 0000000000 0 | 00000 | 00000 | 000000000000
1位 41位时间戳(毫秒,约69年) 10位机器ID 12位序列号
符号 5位DC+5位Worker
2.1 Java 实现
public class SnowflakeIdWorker {
private final long workerId;
private final long datacenterId;
private long sequence = 0L;
private long lastTimestamp = -1L;
private final long twepoch = 1288834974657L; // 起始时间戳
private final long workerIdBits = 5L;
private final long datacenterIdBits = 5L;
private final long maxWorkerId = ~(-1L << workerIdBits); // 31
private final long maxDatacenterId = ~(-1L << datacenterIdBits); // 31
private final long sequenceBits = 12L;
private final long workerIdShift = sequenceBits; // 12
private final long datacenterIdShift = sequenceBits + workerIdBits; // 17
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; // 22
private final long sequenceMask = ~(-1L << sequenceBits); // 4095
public synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backwards");
}
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - twepoch) << timestampLeftShift)
| (datacenterId << datacenterIdShift)
| (workerId << workerIdShift)
| sequence;
}
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
private long timeGen() {
return System.currentTimeMillis();
}
}
2.2 时钟回拨问题
| 策略 | 实现 | 适用 |
|---|---|---|
| 等待 | Thread.sleep() 直到追上 | 回拨短 |
| 异常 | 抛出异常,由上层处理 | 严格场景 |
| 备用位 | 扩展时间戳位数 | 长期方案 |
| 序列号扩展 | 借用 workerId 位 | 临时方案 |
推荐使用 NTP 配合 -x 选项(逐渐调整,而非跳变)。
3. 美团的 Leaf
Leaf 提供两种模式:号段模式(Leaf-segment)和 Snowflake 模式(Leaf-snowflake)。
3.1 Leaf-segment(号段模式)
CREATE TABLE leaf_alloc (
biz_tag VARCHAR(128) PRIMARY KEY,
max_id BIGINT NOT NULL DEFAULT 1,
step INT NOT NULL,
description VARCHAR(256),
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 初始数据
INSERT INTO leaf_alloc (biz_tag, max_id, step, description)
VALUES ('order', 0, 1000, '订单号');
// 双 buffer 优化,号段用完前异步加载下一个号段
public class SegmentBuffer {
private Segment current; // 当前使用
private Segment next; // 预加载
private volatile boolean nextReady; // 是否就绪
public long getNextId() {
long value = current.getAndIncrement();
if (value < current.getMax()) {
return value;
}
// 当前号段用完,切换到 next
lock.lock();
try {
if (!nextReady) {
waitForNextReady(); // 等待异步加载完成
}
current = next;
nextReady = false;
return current.getAndIncrement();
} finally {
lock.unlock();
}
}
}
优点:趋势递增,利于 B+Tree 索引;数据库压力大为降低(步长 1000 时减少 1000 倍)。
3.2 Leaf-snowflake
基于 ZooKeeper 分配 workerId,解决 Snowflake 的 workerId 分配问题。
注册到 ZooKeeper: /leaf-snowflake/{serviceName}/{ip:port}-{sequence}
workerId = sequence (顺序分配)
4. 其他方案
4.1 Ulid(Universally Unique Lexicographically Sortable Identifier)
01ARZ3NDEKTSV4RRFFQ69G5FAV
├── 48bit 时间戳(毫秒)
└── 80bit 随机数
特点:字典序可排序、大小写不敏感、URL 安全、无特殊字符。
import ulid
u = ulid.new()
print(u) # 01ARZ3NDEKTSV4RRFFQ69G5FAV
print(u.timestamp) # Unix 时间戳
print(u.randomness) # 随机部分
4.2 数据库多主模式
-- 设置不同自增步长和起始值
-- 节点1: auto_increment_increment=3, auto_increment_offset=1
-- 节点2: auto_increment_increment=3, auto_increment_offset=2
-- 节点3: auto_increment_increment=3, auto_increment_offset=3
缺点:扩展性差,新增节点需重配。
5. 选型建议
| 场景 | 推荐方案 |
|---|---|
| 高并发、趋势递增 | Snowflake / Leaf |
| 无依赖、简单场景 | Ulid |
| 不追求趋势、简单 | UUID v4 |
| 已有 MySQL、低并发 | 号段模式 |
| 需要排队号/可读性 | 号段模式 |
线上建议:使用 Leaf 或改造版 Snowflake(集成 workerId 自动分配 + 时钟回拨处理)。
6. 分布式 ID 的隐藏陷阱与排查
6.1 时钟回拨的线上应急
Snowflake 依赖系统时钟,当 NTP 同步导致时钟回拨时,可能产生重复 ID。线上排查步骤:
- 监控告警:部署
ntpd或chronyd时钟偏移监控,超过 5ms 即告警 - 快速止血:临时切换为 Leaf-segment 号段模式,规避时钟依赖
- 根因修复:检查 NTP 配置,启用
-x选项(slewing 模式,逐步调整而非跳变) - 数据补偿:扫描回拨时间段内生成的 ID,通过业务时间戳字段去重
6.2 ID 冲突的排查思路
当出现疑似 ID 冲突时:
- 提取冲突 ID 的二进制表示,检查时间戳部分是否异常(如全 0 或未来时间)
- 分析 workerId 分布,确认是否存在重复分配(ZK ephemeral 节点未及时清理)
- 检查序列号是否溢出(同一毫秒内超过 4096 个请求)
6.3 高并发下的性能瓶颈
单机 Snowflake 的 QPS 上限约为 400万/秒(4096 序列号 × 1000 毫秒)。当业务峰值超过此上限时:
- 水平扩展:增加机器节点(workerId),每个节点独立生成
- 批量预取:客户端一次申请 1000 个 ID,减少 RPC 调用
- 本地缓存:将号段缓存在本地内存,仅耗尽时请求服务端
6.4 不同语言的实现参考
| 语言 | 库/项目 | 特点 |
|---|---|---|
| Java | Twitter Snowflake / 美团 Leaf | 工业级,文档完善 |
| Go | bwmarrin/snowflake | 轻量,支持自定义 epoch |
| Python | pysnowflake | 简洁,适合脚本场景 |
| Rust | snowflake-rs | 零分配,高性能 |
| Node.js | node-snowflake | 异步友好 |
选型原则:Java 生态优先选 Leaf,Go 服务可选 bwmarrin/snowflake,Python 脚本可用 ulid-py。无论选哪种方案,务必在压测环境验证时钟回拨处理逻辑,并建立 ID 唯一性监控。
延伸阅读
- 数据库读写分离 — 分库分表与分布式 ID 的配合
- Redis 主从复制 — 缓存层 ID 生成方案
- ZooKeeper 协调服务 — Leaf 的 workerId 分配依赖
- Kafka 消息队列 — 分布式日志中的 ID 策略
- 系统架构设计 — 全局唯一性的通用解法
分布式 ID 与数据库主键策略的配合
| 数据库 | 自增 ID | UUID | 雪花算法 | 建议 |
|---|---|---|---|---|
| MySQL | ✅ 简单 | ❌ 页分裂 | ✅ 需自定义 | bigint / 雪花 |
| PostgreSQL | ✅ 简单 | ⚠️ 性能差 | ✅ 需自定义 | bigserial / 雪花 |
| MongoDB | ✅ ObjectId | — | ✅ 可选 | ObjectId 内置 |
| TiDB | ❌ 不连续 | ✅ 支持 | ✅ 推荐 | 避免自增热点 |
MySQL 自增 ID 分库分表问题:
分表后多表的自增 ID 会冲突。解决方案:
- 设置不同起始值和步长:表 A 从 1 开始步长 10,表 B 从 2 开始步长 10
- 或完全放弃自增,改用雪花算法
TiDB 的自增 ID 陷阱:
TiDB 的自增 ID 不保证连续(分布式事务预留范围可能未用完),且在高并发写入时可能成为热点。TiDB 推荐使用 AUTO_RANDOM 替代 AUTO_INCREMENT。
ID 可读性与安全性权衡
自增 ID: 100001 → 100002 → 100003 (可预测,暴露业务规模)
雪花 ID: 1426756382912348160 (无规律,但冗长)
UUID: 550e8400-e29b-41d4-a716-446655440000 (更长,URL 不友好)
ULID: 01ARZ3NDEKTSV4RRFFQ69G5FAV (26字符,按时间排序)
对外暴露的 ID 建议额外做混淆(如 Hashids 编码),避免被遍历攻击。内部系统使用原生雪花 ID 即可。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。