如何设计一个分布式ID生成器?

2025年 阅读约 15 分钟 面试指南 · 系统设计

深入解析分布式ID生成器设计:UUID(优缺点)、Snowflake雪花算法(时钟回拨解决方案)、号段模式(美团Leaf)、Redis自增,附面试模拟问答。

一句话总结

分布式ID要求:全局唯一、趋势递增(利于索引)、高性能、高可用。方案对比:UUID(简单但无序,索引性能差)→ Snowflake(64位:1+41时间戳+10机器+12序列号,趋势递增,最常用)→ 号段模式(美团Leaf,预分配号段,性能最高)→ Redis自增(简单但依赖Redis)。Snowflake核心挑战是时钟回拨,解决方案:等待、抛异常、或使用号段模式。

初级理解

方案对比

方案优点缺点适用场景
UUID简单、本地生成无序(索引性能差)、太长小规模、非主键
Snowflake趋势递增、高性能依赖时钟、需分配机器ID大多数场景(推荐)
号段模式性能最高、无时钟依赖实现复杂、ID不连续超高并发
Redis自增简单、严格递增依赖Redis、单点故障小规模
一句话总结:UUID 简单但无序;Snowflake 最常用;号段模式性能最高。

中级深入

Snowflake 雪花算法

# Snowflake 64位结构 # 1 位:符号位(始终为 0) # 41 位:时间戳(毫秒级,可用 69 年) # 10 位:工作机器 ID(5 位数据中心 + 5 位机器 ID,最多 1024 台) # 12 位:序列号(同一毫秒内最多 4096 个) public class SnowflakeIdGenerator { private final long workerId; private final long datacenterId; private long sequence = 0L; private long lastTimestamp = -1L; private static final long EPOCH = 1700000000000L; # 起始时间戳 public synchronized long nextId() { long timestamp = System.currentTimeMillis(); # 时钟回拨处理 if (timestamp < lastTimestamp) { throw new RuntimeException("Clock moved backwards!"); } if (timestamp == lastTimestamp) { sequence = (sequence + 1) & 0xFFF; # 4095 if (sequence == 0) { timestamp = waitNextMillis(lastTimestamp); } } else { sequence = 0; } lastTimestamp = timestamp; return ((timestamp - EPOCH) << 22) | (datacenterId << 17) | (workerId << 12) | sequence; } }

时钟回拨解决方案

# 时钟回拨:NTP 时间同步导致系统时间倒退 # 解决方案: # 1. 等待(简单) # 如果回拨时间短(如 < 5ms),等待追上 private long waitNextMillis(long lastTimestamp) { long timestamp = System.currentTimeMillis(); while (timestamp <= lastTimestamp) { timestamp = System.currentTimeMillis(); } return timestamp; } # 2. 抛异常(严格) # 回拨就拒绝服务,等时钟恢复 # 3. 备用序列号(推荐) # 记录上次时间戳,回拨时使用备用 workerId # 或使用扩展位(借用未来时间戳的位) # 4. 号段模式(根本解决) # 不依赖时钟,从 DB 预分配号段
中级要点:Snowflake 64位 = 1+41+10+12;时钟回拨可等待、抛异常或备用序列号。

高级拓展

号段模式(美团 Leaf)

# 号段模式原理 # 1. 从 DB 批量获取号段(如 1~1000) # 2. 本地内存中分配 ID # 3. 号段用完再取下一段 # 优点: # 1. 性能极高(本地分配,无网络开销) # 2. 无时钟依赖 # 3. DB 挂了也能撑一段时间 # 双 Buffer 优化 # buffer1: 当前号段(正在分配) # buffer2: 预加载号段(异步加载) # buffer1 用完 → 切换到 buffer2 → 异步加载新 buffer1 # DB 表设计 CREATE TABLE id_segment ( biz_tag VARCHAR(50) PRIMARY KEY, max_id BIGINT NOT NULL, step INT NOT NULL DEFAULT 1000 ); # 获取号段 UPDATE id_segment SET max_id = max_id + step WHERE biz_tag = 'order'; SELECT max_id FROM id_segment WHERE biz_tag = 'order';

实战场景

场景:订单ID生成

# 订单ID要求 # 1. 全局唯一 # 2. 趋势递增(利于 MySQL 索引) # 3. 包含业务信息(可选) # 方案:Snowflake + 业务前缀 # 订单ID = 业务前缀 + Snowflake ID # 如:ORD1700000000001 # 使用 Hutool 工具类 long id = IdUtil.getSnowflake(1, 1).nextId(); String orderId = "ORD" + id;

面试模拟

面试官:分布式ID有哪些方案?

你:1)UUID:简单但无序,索引性能差;2)Snowflake:64位,1+41+10+12,趋势递增,最常用;3)号段模式(美团Leaf):预分配号段,性能最高,无时钟依赖;4)Redis自增:简单但依赖Redis。推荐Snowflake或号段模式。

面试官:Snowflake 时钟回拨怎么解决?

你:1)回拨时间短(<5ms):等待追上;2)回拨时间长:抛异常或使用备用workerId;3)根本解决:用号段模式(不依赖时钟)。生产环境建议用号段模式或做好NTP配置防止大幅回拨。