- design-seckill.md: 秒杀系统设计 - design-shorturl.md: 短链接系统设计 - design-lbs.md: LBS附近的人系统设计 - design-im.md: 即时通讯系统设计 - design-feed.md: 社交信息流系统设计 Each document includes: - Requirements analysis and data volume assessment - Technical challenges - System architecture design - Database design - Caching strategies - Scalability considerations - Practical project experience - Alibaba P7 level additional points Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
18 KiB
18 KiB
分布式 ID 生成方案
问题
- 为什么需要分布式 ID?分布式 ID 有哪些要求?
- 常见的分布式 ID 生成方案有哪些?各自的优缺点是什么?
- Snowflake 算法的原理是什么?有什么坑?
- 数据库自增 ID 如何实现分布式?如何优化性能?
- Redis 如何生成分布式 ID?有哪些优缺点?
- 在实际项目中,你是如何设计分布式 ID 的?
标准答案
1. 分布式 ID 的要求和特点
为什么需要分布式 ID?
在分布式系统中,需要保证 ID 的全局唯一性:
- 订单号、用户 ID、支付流水号
- 分库分表后的主键冲突问题
- 微服务间的数据关联
单机 ID 的问题:
单机数据库自增 ID:
- 实例 1:1, 2, 3, 4, 5
- 实例 2:1, 2, 3, 4, 5 ← 冲突!
分布式 ID 的核心要求
| 要求 | 说明 | 示例 |
|---|---|---|
| 全局唯一性 | 不能重复 | 订单号不能重复 |
| 有序性 | 趋势递增(可选) | 按时间排序的订单号 |
| 高性能 | 生成速度快 | 支持 10 万+/秒 |
| 高可用 | 服务不中断 | 宕机后仍可生成 |
| 信息安全 | 不暴露业务信息 | 不暴露订单总量 |
2. 常见分布式 ID 方案对比
| 方案 | 唯一性 | 有序性 | 性能 | 复杂度 | 适用场景 |
|---|---|---|---|---|---|
| UUID | ✅ | ❌ | ⭐⭐⭐⭐⭐ | 低 | 非主键、内部 ID |
| 数据库自增 | ✅ | ✅ | ⭐⭐ | 低 | 小规模、并发低 |
| Redis INCR | ✅ | ✅ | ⭐⭐⭐ | 低 | 中小规模 |
| Snowflake | ✅ | ✅ | ⭐⭐⭐⭐⭐ | 中 | 大规模、高并发 |
| 号段模式 | ✅ | ✅ | ⭐⭐⭐⭐ | 中 | 大规模、高性能 |
| 美团 Leaf | ✅ | ✅ | ⭐⭐⭐⭐⭐ | 高 | 金融级、高可用 |
3. UUID
原理
UUID(Universally Unique Identifier)是 128 位的唯一标识符。
格式:
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
550e8400-e29b-41d4-a716-446655440000
Java 示例:
import java.util.UUID;
String uuid = UUID.randomUUID().toString();
// 输出:550e8400-e29b-41d4-a716-446655440000
优缺点
优点:
- 性能高:本地生成,无网络开销
- 简单:JDK 自带,无需额外组件
缺点:
- 无序:无法按时间排序
- 过长:36 字符,存储空间大
- 不安全:暴露 MAC 地址(UUID v1)
- 索引性能差:无序 ID 导致 B+ 树频繁分裂
B+ 树分裂问题:
有序 ID:
1 → 100 → 1000 → 10000
└─ 顺序插入,B+ 树叶子节点顺序填充
无序 ID(UUID):
abc → xyz → 123 → 999
└─ 随机插入,B+ 树频繁分裂,性能差
适用场景
- ✅ 非数据库主键(如请求 ID)
- ✅ 临时标识、会话 ID
- ✅ 内部系统、不需要排序
- ❌ 订单号、用户 ID(需要有序)
4. 数据库自增 ID
方案 1:单机自增
CREATE TABLE orders (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
order_no VARCHAR(32),
created_at DATETIME
);
问题:单机性能瓶颈,无法扩展。
方案 2:多机步长模式(Flickr 方案)
原理:不同数据库实例设置不同的起始值和步长。
实例 1:起始 1,步长 2 → 1, 3, 5, 7, 9
实例 2:起始 2,步长 2 → 2, 4, 6, 8, 10
配置:
-- 实例 1
SET auto_increment_increment = 2;
SET auto_increment_offset = 1;
-- 实例 2
SET auto_increment_increment = 2;
SET auto_increment_offset = 2;
优点:
- 实现简单
- ID 有序
缺点:
- 扩容困难:需要重新计算步长
- 性能瓶颈:数据库写入限制
方案 3:号段模式(批量获取)
原理:一次从数据库获取一批 ID(号段),缓存在本地。
表结构:
CREATE TABLE id_segment (
biz_type VARCHAR(32) PRIMARY KEY, -- 业务类型
max_id BIGINT, -- 当前最大 ID
step INT, -- 步长(批量大小)
version INT, -- 版本号(乐观锁)
updated_at DATETIME
);
-- 初始化数据
INSERT INTO id_segment (biz_type, max_id, step, version)
VALUES ('order', 0, 1000, 1);
获取 ID 逻辑:
@Service
public class IdSegmentService {
@Autowired
private IdSegmentMapper segmentMapper;
private final Map<String, IdSegment> localCache = new ConcurrentHashMap<>();
@Transactional
public synchronized Long nextId(String bizType) {
IdSegment segment = localCache.get(bizType);
// 本地号段用完,从数据库获取
if (segment == null || segment.getCurrentId() >= segment.getMaxId()) {
segment = fetchSegmentFromDb(bizType);
}
// 返回下一个 ID
return segment.getNextId();
}
private IdSegment fetchSegmentFromDb(String bizType) {
// 使用 CAS 更新数据库
IdSegment dbSegment = segmentMapper.selectByType(bizType);
// 更新 max_id = max_id + step
int updated = segmentMapper.updateMaxId(
bizType,
dbSegment.getMaxId() + dbSegment.getStep(),
dbSegment.getVersion()
);
if (updated == 0) {
throw new RuntimeException("并发冲突,请重试");
}
// 缓存到本地
IdSegment newSegment = new IdSegment();
newSegment.setMaxId(dbSegment.getMaxId() + dbSegment.getStep());
newSegment.setCurrentId(dbSegment.getMaxId());
localCache.put(bizType, newSegment);
return newSegment;
}
}
Mapper:
@Update("UPDATE id_segment SET max_id = #{maxId}, version = version + 1 " +
"WHERE biz_type = #{bizType} AND version = #{version}")
int updateMaxId(@Param("bizType") String bizType,
@Param("maxId") Long maxId,
@Param("version") Integer version);
优点:
- 性能高:本地缓存,减少数据库访问
- 有序:ID 趋势递增
缺点:
- 宕机丢 ID:本地缓存的 ID 未使用完就丢失
- 实现复杂
优化:使用双缓冲(Double Buffer)机制预加载号段。
5. Redis INCR
原理
使用 Redis 的 INCR 和 INCRBY 命令生成全局唯一 ID。
示例:
# 初始化
SET order:id 1
# 获取下一个 ID
INCR order:id
# 返回:2
# 批量获取(步长 1000)
INCRBY order:id 1000
# 返回:1001
Java 实现
@Service
public class RedisIdGenerator {
@Autowired
private StringRedisTemplate redisTemplate;
public Long nextId(String key) {
// 使用 redisTemplate 的 opsForValue
Long id = redisTemplate.opsForValue().increment(key);
if (id == null) {
throw new RuntimeException("生成 ID 失败");
}
return id;
}
// 批量获取(优化性能)
public Long[] batchNextId(String key, int count) {
Long startId = redisTemplate.opsForValue().increment(key, count);
Long[] ids = new Long[count];
for (int i = 0; i < count; i++) {
ids[i] = startId - count + i + 1;
}
return ids;
}
}
优缺点
优点:
- 性能高:Redis 内存操作
- 有序:ID 趋势递增
- 实现简单
缺点:
- 依赖 Redis:需要维护 Redis 集群
- 宕机丢数据:未持久化会丢失
持久化配置:
# 开启 AOF 持久化
appendonly yes
appendfsync everysec
适用场景
- 中小规模、性能要求高
- 已有 Redis 集群
- 可容忍短期 ID 丢失
6. Snowflake 算法
原理
Snowflake 是 Twitter 开源的分布式 ID 算法,生成 64 位的 Long 型 ID。
结构(64 位):
0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
↑ ↑ ↑ ↑ ↑
│ │ │ │ │
│ └─ 41 位时间戳(毫秒) │ │ │
│ │ │ │
│ └─ 5 位数据中心 ID │
│ └─ 12 位序列号
1 位符号位(始终为 0)
组成部分:
- 1 位符号位:始终为 0(正数)
- 41 位时间戳:毫秒级,可用 69 年(
2^41 / 1000 / 60 / 60 / 24 / 365 ≈ 69) - 5 位数据中心 ID:支持 32 个数据中心
- 5 位机器 ID:每个数据中心 32 台机器
- 12 位序列号:每毫秒可生成 4096 个 ID
理论 QPS:
单机:4096 / 毫秒 = 409.6 万/秒
Java 实现
public class SnowflakeIdGenerator {
// 起始时间戳(2024-01-01 00:00:00)
private final long twepoch = 1704067200000L;
// 各部分位数
private final long workerIdBits = 5L;
private final long datacenterIdBits = 5L;
private final long sequenceBits = 12L;
// 最大值
private final long maxWorkerId = -1L ^ (-1L << workerIdBits); // 31
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); // 31
private final long maxSequence = -1L ^ (-1L << sequenceBits); // 4095
// 位移
private final long workerIdShift = sequenceBits; // 12
private final long datacenterIdShift = sequenceBits + workerIdBits; // 17
private final long timestampShift = sequenceBits + workerIdBits + datacenterIdBits; // 22
private final long workerId;
private final long datacenterId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdGenerator(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException("workerId 无效");
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException("datacenterId 无效");
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
public synchronized long nextId() {
long timestamp = System.currentTimeMillis();
// 时钟回拨检查
if (timestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨,拒绝生成 ID");
}
// 同一毫秒内,序列号自增
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & maxSequence;
// 序列号溢出,等待下一毫秒
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
// 新毫秒,序列号重置
sequence = 0L;
}
lastTimestamp = timestamp;
// 组装 ID
return ((timestamp - twepoch) << timestampShift)
| (datacenterId << datacenterIdShift)
| (workerId << workerIdShift)
| sequence;
}
// 等待下一毫秒
private long tilNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
// 解析 ID(用于调试)
public static void parseId(long id) {
long timestamp = (id >> 22) + 1704067200000L;
long datacenterId = (id >> 17) & 0x1F;
long workerId = (id >> 12) & 0x1F;
long sequence = id & 0xFFF;
System.out.println("ID: " + id);
System.out.println("时间戳: " + new Date(timestamp));
System.out.println("数据中心 ID: " + datacenterId);
System.out.println("机器 ID: " + workerId);
System.out.println("序列号: " + sequence);
}
}
使用示例:
// 初始化(workerId=1, datacenterId=1)
SnowflakeIdGenerator idGenerator = new SnowflakeIdGenerator(1, 1);
// 生成 ID
long id = idGenerator.nextId();
System.out.println("生成的 ID: " + id);
// 解析 ID
SnowflakeIdGenerator.parseId(id);
Snowflake 的坑
问题 1:时钟回拨
原因:
- 系统时钟不准确(NTP 同步)
- 人工修改系统时间
后果:
时间:10:00:00.100,生成 ID(时间戳=T1)
时钟回拨:时间 → 09:59:59.900
时间:09:59:59.950,生成 ID(时间戳=T2 < T1)← 冲突!
解决方案:
- 拒绝服务(简单):
if (timestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨,拒绝生成 ID");
}
- 等待时钟追上(推荐):
if (timestamp < lastTimestamp) {
long offset = lastTimestamp - timestamp;
if (offset <= 5) {
// 等待 5 毫秒
try {
Thread.sleep(offset << 1);
timestamp = System.currentTimeMillis();
} catch (InterruptedException e) {
throw new RuntimeException("时钟回拨,等待失败");
}
} else {
throw new RuntimeException("时钟回拨过多,拒绝生成 ID");
}
}
- 使用备用 workerId(美团 Leaf 方案):
if (timestamp < lastTimestamp) {
// 切换到备用 workerId
workerId = backupWorkerId;
}
问题 2:机器 ID 分配
问题:如何保证 workerId 全局唯一?
解决方案:
- 配置文件(简单):
application.yml:
snowflake:
worker-id: 1
datacenter-id: 1
- 数据库配置:
CREATE TABLE worker_config (
id INT PRIMARY KEY,
worker_id INT,
datacenter_id INT,
ip VARCHAR(32),
used BOOLEAN
);
-- 启动时申请 workerId
INSERT INTO worker_config (worker_id, datacenter_id, ip, used)
VALUES (1, 1, '192.168.1.10', TRUE);
- Zookeeper 顺序节点(动态):
// 在 Zookeeper 中创建临时顺序节点
String path = zk.create("/snowflake/worker-", null, ZooDefs.Ids.EPHEMERAL_SEQUENTIAL);
// 获取序号作为 workerId
int workerId = Integer.parseInt(path.split("-")[1]);
- Redis INCR(动态):
Long workerId = redisTemplate.opsForValue().increment("snowflake:worker:id");
问题 3:序列号溢出
场景:高并发下,1 毫秒内请求超过 4096 个。
解决:
// 序列号溢出,等待下一毫秒
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
优化:使用 13 位序列号(每毫秒 8192 个 ID)。
优缺点
优点:
- 性能极高:409 万 QPS/单机
- 有序:趋势递增(按时间排序)
- 不依赖数据库、Redis
缺点:
- 时钟回拨问题
- 机器 ID 分配复杂
- ID 较长(18 位数字)
适用场景
- 大规模、高并发场景
- 需要有序 ID
- 可容忍时钟问题(或已有解决方案)
实际应用:
- 百度 UidGenerator
- 美团 Leaf(Snowflake 模式)
- Etsy)
7. 美团 Leaf
Leaf-segment(号段模式)
原理:优化版号段模式,使用双缓冲机制。
架构:
Leaf Server
├─ Buffer 1:当前使用号段 [1000, 2000)
└─ Buffer 2:预加载号段 [2000, 3000)(后台异步加载)
当 Buffer 1 用完:
└─ 切换到 Buffer 2
└─ 异步加载 Buffer 1 的下一个号段
优点:
- 无停顿:双缓冲无缝切换
- 高性能:本地缓存
缺点:
- 宕机丢 ID:未使用的号段丢失
Leaf-snowflake(优化版 Snowflake)
优化点:
- Zookeeper 生成 workerId:动态分配,无需配置
- 时钟回拨优化:
- 回拨 5ms 内:等待时钟追上
- 回拨 5ms 外:告警并拒绝服务
架构:
Leaf Server 1(workerId=1)
Leaf Server 2(workerId=2)
Leaf Server 3(workerId=3)
↓
Zookeeper(协调)
GitHub:https://github.com/Meituan-Dianping/Leaf
8. 百度 UidGenerator
特点:
- 基于 Snowflake 优化
- 使用 22 位序列号(每秒 400 万 ID)
- 支持跨毫秒分配序列号
GitHub:https://github.com/baidu/uid-generator
9. 实际项目选型建议
决策树
是否需要有序?
├─ 否 → UUID(最简单)
└─ 是 → 继续判断
│
├─ QPS < 1000?
│ ├─ 是 → Redis INCR(简单)
│ └─ 否 → 继续判断
│
├─ 已有 Redis?
│ ├─ 是 → 号段模式(高性能)
│ └─ 否 → 继续判断
│
├─ 可容忍时钟回拨问题?
│ ├─ 是 → Snowflake(性能最高)
│ └─ 否 → 美团 Leaf-snowflake
│
└─ 金融级可靠性?
└─ 美团 Leaf-segment + 监控
性能对比
| 方案 | 单机 QPS | 延迟 | 依赖 |
|---|---|---|---|
| UUID | 1000 万+ | 0.001ms | 无 |
| 数据库自增 | 1000 | 10ms | 数据库 |
| Redis INCR | 10 万 | 1ms | Redis |
| 号段模式 | 100 万 | 0.1ms | 数据库 |
| Snowflake | 400 万 | 0.01ms | 无 |
10. 阿里 P7 加分项
深度理解:
- 理解 Snowflake 的时间戳回拨问题的根本原因
- 理解号段模式的双缓冲机制和 CAS 原理
实战经验:
- 有处理 Snowflake 时钟回拨的线上故障经验
- 有号段模式宕机丢 ID 的解决方案
- 有分布式 ID 迁移经验(如从数据库自增迁移到 Snowflake)
架构能力:
- 能设计支持多业务类型的分布式 ID 系统
- 能设计分布式 ID 的监控和告警体系
- 有分布式 ID 容灾方案(多机房容灾)
技术选型:
- 能根据业务特点选择合适的方案
- 了解美团 Leaf、百度 UidGenerator 等开源方案
- 有自研分布式 ID 生成器的经验