分布式 ID 生成方案对比:雪花算法、号段模式与数据库自增

📅 2026/7/12 17:04:48
分布式 ID 生成方案对比:雪花算法、号段模式与数据库自增
分布式 ID 生成方案对比雪花算法、号段模式与数据库自增一、分库分表后你的主键还是自增的在一个单体应用的数据库里AUTO_INCREMENT是最自然的主键生成方式。插入一条记录数据库帮你在上一行 ID 的基础上加 1简单可靠。但当系统分库分表之后这个方案立即失效——不同的分表会产生相同的 ID全局唯一性被打破。在分布式系统中全局唯一的 ID 生成是一个看似简单、实则充满工程细节的问题。它不仅要保证唯一还要考虑趋势递增利于索引、高性能不成为瓶颈、以及高可用单点故障不影响 ID 生成。当前业界有三个主流方案雪花算法、号段模式、数据库自增的特殊应用。这篇文章从原理到实现完整对比这三种方案的优劣。flowchart TB A[分布式 ID 生成需求] -- B{选择方案} B -- C[雪花算法 Snowflake] B -- D[号段模式 Segment] B -- E[数据库自增变体] C -- C1[41bit 时间戳 10bit 机器ID 12bit 序列号] C -- C2[优点: 高性能, 无外部依赖] C -- C3[缺点: 时钟回拨问题] D -- D1[每次从数据库取一段号] D -- D2[优点: 简单可控, 可定制业务ID] D -- D3[缺点: 依赖数据库, 号段耗尽需处理] E -- E1[多主模式, 步长不同] E -- E2[优点: 绝对递增, 无空洞] E -- E3[缺点: 扩展困难, 写入瓶颈]二、雪花算法高性能的解耦之美雪花算法Snowflake是 Twitter 开源的一种分布式 ID 生成方案。它在单机内生成 64 位长整型完全无需外部依赖是性能最高的方案。ID 结构64 位| 1bit 符号位 | 41bit 时间戳毫秒 | 10bit 工作机器ID | 12bit 序列号 |41bit 时间戳从自定义纪元开始如 2020-01-01可覆盖约 69 年10bit 机器 ID支持 1024 个节点12bit 序列号同一毫秒内可生成 4096 个 ID每秒理论最大 QPS 4096 × 1000 4,096,000完全能满足绝大部分场景。雪花算法的致命缺陷时钟回拨。如果服务器时钟因为 NTP 校准向后调整了 1 毫秒雪花算法可能生成与前一毫秒重复的 ID。这是雪花算法在工程实践中最需要小心处理的问题。主流解决方案包括等待时钟追上阻塞直到时间恢复、使用备用 Worker ID检测回拨后切换到备用机器号、或记录最大时间戳保存上次生成 ID 的时间回拨时拒绝生成。三、三种方案的完整工程实现/** * 雪花算法实现——带时钟回拨保护 * * 为什么需要时钟回拨保护NTP 校时可能导致服务器时间倒退 * 如果不处理会产生重复 ID破坏全局唯一性。 */ public class SnowflakeIdGenerator { // 起始时间戳2024-01-01 00:00:00 // 为什么用固定纪元让 41bit 时间戳可用更久 private static final long EPOCH 1704067200000L; // 各部分位数 private static final long WORKER_ID_BITS 10L; private static final long SEQUENCE_BITS 12L; // 最大值 private static final long MAX_WORKER_ID ~(-1L WORKER_ID_BITS); private static final long MAX_SEQUENCE ~(-1L SEQUENCE_BITS); // 偏移量 private static final long TIMESTAMP_SHIFT WORKER_ID_BITS SEQUENCE_BITS; private static final long WORKER_ID_SHIFT SEQUENCE_BITS; private final long workerId; private long sequence 0L; private long lastTimestamp -1L; /** * 时钟回拨容忍阈值毫秒 * 为什么设 5msNTP 校时通常不会回退超过 5ms * 超过此值说明时钟严重异常应拒绝生成。 */ private static final long MAX_BACKWARD_MS 5L; public SnowflakeIdGenerator(long workerId) { if (workerId MAX_WORKER_ID || workerId 0) { throw new IllegalArgumentException( String.format(Worker ID 必须在 0-%d 之间, MAX_WORKER_ID)); } this.workerId workerId; } public synchronized long nextId() { long currentTimestamp System.currentTimeMillis(); // 时钟回拨检测与处理 if (currentTimestamp lastTimestamp) { long backwardMs lastTimestamp - currentTimestamp; // 回拨在容忍范围内等待时钟追上 if (backwardMs MAX_BACKWARD_MS) { try { // 为什么等待而不是直接拒绝短暂回拨大概率是 NTP 微调 // 等待恢复比拒绝更符合业务需求 Thread.sleep(backwardMs 1); currentTimestamp System.currentTimeMillis(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(时钟回拨等待被中断, e); } } else { // 回拨超过容忍上限拒绝生成 // 为什么必须拒绝大量回拨说明系统时钟严重异常 // 生成的 ID 有极高重复风险 throw new RuntimeException( String.format(时钟回拨 %d ms超过容忍上限 %d ms拒绝生成 ID, backwardMs, MAX_BACKWARD_MS)); } } // 同一毫秒内序列号递增 if (currentTimestamp lastTimestamp) { sequence (sequence 1) MAX_SEQUENCE; // 序列号用完同一毫秒内生成了 4096 个 ID if (sequence 0) { // 忙等待下一毫秒 // 为什么用忙等待而非 sleep毫秒级等待用 sleep 不够精确 // 可能等的时间过长 while (currentTimestamp lastTimestamp) { currentTimestamp System.currentTimeMillis(); } } } else { sequence 0L; // 新的毫秒序列号归零 } lastTimestamp currentTimestamp; // 组装 ID时间戳左移 | 机器ID左移 | 序列号 return ((currentTimestamp - EPOCH) TIMESTAMP_SHIFT) | (workerId WORKER_ID_SHIFT) | sequence; } } /** * 号段模式——基于数据库的批量 ID 生成 * * 为什么需要号段模式雪花算法生成的 ID 不是严格递增的 * 对于需要连续 ID 的业务场景如订单号 * 号段模式更可控。 */ Service public class SegmentIdGenerator { Autowired private JdbcTemplate jdbcTemplate; // 内存中的当前号段 private final AtomicLong currentId new AtomicLong(0); private volatile long maxId 0; // 每次从数据库获取的号段大小 // 为什么设 1000每获取一次号段就够 1000 次请求使用 // 数据库压力降到 1/1000 private static final int SEGMENT_SIZE 1000; /** * 获取下一个 ID * 为什么用 double-check 锁高频调用时 * 大部分情况只需 AtomicLong 自增不需要进锁。 */ public long nextId(String bizTag) { long id currentId.incrementAndGet(); // 号段用完或首次使用从数据库获取新号段 if (id maxId) { synchronized (this) { // double-check进入锁后再次判断 // 防止多个线程同时等待锁后重复获取 if (currentId.get() maxId) { fetchSegment(bizTag); } } id currentId.incrementAndGet(); } return id; } /** * 从数据库获取新号段 * 使用事务保证原子性UPDATE 在同一事务中完成读取和递增 */ private void fetchSegment(String bizTag) { // UPDATE segment SET max_id max_id SEGMENT_SIZE // WHERE biz_tag ? AND version ? // 返回更新后的 max_id String sql UPDATE id_segment SET max_id max_id ? WHERE biz_tag ? RETURNING max_id; Long newMaxId jdbcTemplate.queryForObject( sql, Long.class, SEGMENT_SIZE, bizTag); if (newMaxId null) { throw new RuntimeException(获取号段失败bizTag bizTag); } // 号段范围(newMaxId - SEGMENT_SIZE, newMaxId] currentId.set(newMaxId - SEGMENT_SIZE); maxId newMaxId; } } /** * 数据库自增变体——多主模式 * * 适合小规模系统 10 个分库 * 核心思路不同数据库实例设置不同的自增步长 */ Configuration public class MultiMasterIdConfig { /** * 为什么设计成这样MySQL 支持 auto_increment_increment * 和 auto_increment_offset让多个主库生成的 ID 交错而不重复。 * * 实例 1步长 2偏移 1 → ID: 1, 3, 5, 7, ... * 实例 2步长 2偏移 2 → ID: 2, 4, 6, 8, ... * * 限制扩容时需要修改所有实例的步长复杂度随实例数增加。 */ // 实例 1 的 MySQL 配置 // SET GLOBAL auto_increment_increment 2; // SET GLOBAL auto_increment_offset 1; // 实例 2 的 MySQL 配置 // SET GLOBAL auto_increment_increment 2; // SET GLOBAL auto_increment_offset 2; }四、三种方案的对比与选型决策维度雪花算法号段模式数据库自增变体唯一性强需处理时钟回拨强依赖数据库强MySQL 内保证趋势递增是但不连续严格递增严格递增性能极高纯内存高批量取号中每次写 MySQL外部依赖无MySQLMySQL部署复杂度中需分配 Worker ID低低可读性差64位Long好可带业务前缀好纯数字选型建议雪花算法适合高并发、大规模分布式系统如订单系统但其趋势递增而非严格连续的特性不适合对连续 ID 有强需求的场景。号段模式适合需要业务可读 ID 的场景如订单号202407120001且数据库压力可控。数据库自增变体适合分库数量少 10、不想引入额外组件的系统。一个重要的架构权衡雪花算法虽然无外部依赖、性能最高但引入了对系统时钟的依赖——这在分布式系统中是一个新的故障点。号段模式依赖数据库但数据库本身就是系统的基础设施额外故障风险反而更低。五、总结分布式 ID 生成的本质矛盾是唯一性要求串行化高性能要求并行化。三种方案各用不同的方式解这个串行与并行的矛盾——雪花算法用时间戳做天然分区、号段模式用批量分配减少串行化频率、数据库变体用步长交错实现伪并行。没有银弹方案。选型需要权衡性能、一致性、可读性和运维复杂度四个维度。理解每种方案牺牲了什么来换取什么比记住它们的实现细节更重要。