分布式系统中,传统数据库自增 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 自动分配 + 时钟回拨处理)。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。