# 分布式 ID 生成方案 ## 问题 1. 为什么需要分布式 ID?分布式 ID 有哪些要求? 2. 常见的分布式 ID 生成方案有哪些?各自的优缺点是什么? 3. Snowflake 算法的原理是什么?有什么坑? 4. 数据库自增 ID 如何实现分布式?如何优化性能? 5. Redis 如何生成分布式 ID?有哪些优缺点? 6. 在实际项目中,你是如何设计分布式 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 示例**: ```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:单机自增** ```sql 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 ``` **配置**: ```sql -- 实例 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(号段),缓存在本地。 **表结构**: ```sql 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 逻辑**: ```java @Service public class IdSegmentService { @Autowired private IdSegmentMapper segmentMapper; private final Map 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**: ```java @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。 **示例**: ```bash # 初始化 SET order:id 1 # 获取下一个 ID INCR order:id # 返回:2 # 批量获取(步长 1000) INCRBY order:id 1000 # 返回:1001 ``` --- #### **Java 实现** ```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 集群 - 宕机丢数据:未持久化会丢失 **持久化配置**: ```properties # 开启 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 实现** ```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); } } ``` **使用示例**: ```java // 初始化(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)← 冲突! ``` **解决方案**: 1. **拒绝服务**(简单): ```java if (timestamp < lastTimestamp) { throw new RuntimeException("时钟回拨,拒绝生成 ID"); } ``` 2. **等待时钟追上**(推荐): ```java 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"); } } ``` 3. **使用备用 workerId**(美团 Leaf 方案): ```java if (timestamp < lastTimestamp) { // 切换到备用 workerId workerId = backupWorkerId; } ``` --- ##### **问题 2:机器 ID 分配** **问题**:如何保证 workerId 全局唯一? **解决方案**: 1. **配置文件**(简单): ```yaml application.yml: snowflake: worker-id: 1 datacenter-id: 1 ``` 2. **数据库配置**: ```sql 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); ``` 3. **Zookeeper 顺序节点**(动态): ```java // 在 Zookeeper 中创建临时顺序节点 String path = zk.create("/snowflake/worker-", null, ZooDefs.Ids.EPHEMERAL_SEQUENTIAL); // 获取序号作为 workerId int workerId = Integer.parseInt(path.split("-")[1]); ``` 4. **Redis INCR**(动态): ```java Long workerId = redisTemplate.opsForValue().increment("snowflake:worker:id"); ``` --- ##### **问题 3:序列号溢出** **场景**:高并发下,1 毫秒内请求超过 4096 个。 **解决**: ```java // 序列号溢出,等待下一毫秒 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)** **优化点**: 1. **Zookeeper 生成 workerId**:动态分配,无需配置 2. **时钟回拨优化**: - 回拨 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 生成器的经验