跳到主要内容

分布式 ID 生成

问题

分布式系统中如何生成全局唯一 ID?有哪些方案?雪花算法的原理是什么?

答案

分布式 ID 的核心要求

要求说明
全局唯一不同节点不会生成重复 ID
趋势递增有利于数据库索引性能(B+ 树顺序插入)
高性能生成速度快,不成为瓶颈
高可用ID 生成服务不能单点故障
信息安全不暴露业务量等敏感信息(可选)

方案对比

方案唯一性有序性性能可用性适用场景
UUID无排序需求,如 traceId
数据库自增单库小规模
数据库号段中等规模
Redis INCR需要严格递增
雪花算法极高主流方案
Leaf(美团)大规模生产
Tinyid(滴滴)号段模式优化

雪花算法(Snowflake)

Twitter 开源的分布式 ID 生成算法,生成 64 位 long 型 ID

 0 | 0000000000 0000000000 0000000000 0000000000 0 | 00000 | 00000 | 000000000000
↑ |←————————————— 41 位时间戳 ——————————————→|←5位→|←5位→|←——— 12 位序列号 ———→|
符号位 毫秒级时间戳 数据中心 机器ID 同一毫秒内自增序列
部分位数说明
符号位1固定 0(正数)
时间戳41毫秒级,可用约 69 年
数据中心 ID50~31,共 32 个数据中心
机器 ID50~31,每个数据中心 32 台机器
序列号12同一毫秒内自增,0~4095

理论 QPS:单节点每毫秒可生成 4096 个 ID → 约 409.6 万/秒

雪花算法实现
public class SnowflakeIdGenerator {
private final long epoch = 1704067200000L; // 自定义起始时间 2024-01-01
private final long dataCenterIdBits = 5L;
private final long workerIdBits = 5L;
private final long sequenceBits = 12L;

private final long workerIdShift = sequenceBits; // 12
private final long dataCenterIdShift = sequenceBits + workerIdBits; // 17
private final long timestampShift = sequenceBits + workerIdBits + dataCenterIdBits; // 22
private final long sequenceMask = ~(-1L << sequenceBits); // 4095

private final long dataCenterId;
private final long workerId;
private long sequence = 0L;
private long lastTimestamp = -1L;

public SnowflakeIdGenerator(long dataCenterId, long workerId) {
this.dataCenterId = dataCenterId;
this.workerId = workerId;
}

public synchronized long nextId() {
long timestamp = System.currentTimeMillis();

// 时钟回拨检查
if (timestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨,拒绝生成ID: "
+ (lastTimestamp - timestamp) + "ms");
}

if (timestamp == lastTimestamp) {
// 同一毫秒,序列号自增
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
// 序列号溢出,等待下一毫秒
timestamp = waitNextMillis(lastTimestamp);
}
} else {
sequence = 0L; // 新毫秒,序列号重置
}

lastTimestamp = timestamp;

return ((timestamp - epoch) << timestampShift)
| (dataCenterId << dataCenterIdShift)
| (workerId << workerIdShift)
| sequence;
}

private long waitNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}
雪花算法的时钟回拨问题

服务器 NTP 时间同步可能导致时钟回拨,生成重复 ID。

解决方案:

  1. 拒绝生成:回拨时直接抛异常(简单粗暴)
  2. 等待追上:回拨幅度小时(< 5ms)自旋等待
  3. 备用机器位:预留扩展位,回拨时切换到备用 workerId
  4. 美团 Leaf:基于 ZooKeeper 管理 workerId,回拨时上报告警

号段模式(数据库 + 内存缓存)

号段表
CREATE TABLE id_alloc (
biz_tag VARCHAR(64) PRIMARY KEY, -- 业务标签
max_id BIGINT NOT NULL, -- 当前最大 ID
step INT NOT NULL, -- 每次获取的号段长度
update_time TIMESTAMP
);

-- 获取号段
UPDATE id_alloc SET max_id = max_id + step WHERE biz_tag = 'order';

美团 Leaf 的号段模式使用 双 Buffer 优化:当前号段消耗到一定比例时异步预加载下一号段,实现无阻塞 ID 分配。


常见面试问题

Q1: 雪花算法有什么问题?如何解决?

答案

问题解决方案
时钟回拨检测回拨后等待或切换 workerId
机器 ID 分配ZooKeeper 自动分配 / 配置中心管理
ID 不连续业务无关紧要;如需连续用号段模式
可反解ID 可解析出时间戳和机器信息(不安全可加密)

Q2: UUID 为什么不适合做数据库主键?

答案

  1. 无序:UUID 是随机的,插入 B+ 树索引时会导致页分裂,严重降低写入性能
  2. 长度大:UUID 128 位(String 36 字符),比 long(8 字节)占用更多存储和索引空间
  3. 无业务含义:无法从 ID 中推导出时间等信息
  4. 不可读:不方便调试和排查

UUID 适合做分布式唯一标识(如 traceId、sessionId),不适合做数据库主键。

Q3: 号段模式和雪花算法怎么选?

答案

对比雪花算法号段模式
ID 格式64位 long自增数字
依赖无(本地生成)数据库
趋势递增近似递增严格递增
性能极高(纯内存)高(号段缓存)
信息泄露可解析出时间和机器可推算出业务量
适用大多数场景需要严格递增

Q4: 分库分表后的 ID 怎么生成?

答案

分库分表后不能用单表自增 ID(不同表可能生成相同 ID),需要全局唯一 ID 方案。常见选择:

  1. 雪花算法:最常用,性能高,各节点独立生成
  2. 号段模式:如美团 Leaf,按业务 tag 分配号段
  3. ShardingSphere 内置:支持雪花算法和 UUID 两种策略

详见 分库分表

相关链接