news 2026/7/12 17:04:48

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
分布式 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; /** * 时钟回拨容忍阈值(毫秒) * 为什么设 5ms:NTP 校时通常不会回退超过 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 生成的本质矛盾是:唯一性要求串行化,高性能要求并行化。三种方案各用不同的方式解这个"串行与并行"的矛盾——雪花算法用时间戳做天然分区、号段模式用批量分配减少串行化频率、数据库变体用步长交错实现伪并行。

没有银弹方案。选型需要权衡性能、一致性、可读性和运维复杂度四个维度。理解每种方案"牺牲了什么来换取什么",比记住它们的实现细节更重要。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/12 17:04:18

Kohya_ss:深度解析AMD GPU上的扩散模型训练平台架构与最佳实践

Kohya_ss&#xff1a;深度解析AMD GPU上的扩散模型训练平台架构与最佳实践 【免费下载链接】kohya_ss 项目地址: https://gitcode.com/GitHub_Trending/ko/kohya_ss Kohya_ss作为当前最先进的扩散模型训练工具&#xff0c;通过其完善的ROCm支持为AMD GPU用户提供了完整…

作者头像 李华
网站建设 2026/7/12 17:03:00

提示词工程:AI时代人人必学的核心技能

提示词工程&#xff1a;AI时代人人必学的核心技能这篇文章是《提示词工程教程》系列的开篇之作。我将带你从一个真实的场景出发&#xff0c;理解为什么提示词工程正在成为这个时代最重要的技能之一&#xff0c;以及你该如何迈出学习的第一步。一、开篇&#xff1a;一个正在重塑…

作者头像 李华
网站建设 2026/7/12 17:02:54

终极指南:如何为你的演示文稿选择最适合的AI模型提供商

终极指南&#xff1a;如何为你的演示文稿选择最适合的AI模型提供商 【免费下载链接】presenton Open-Source AI Presentation Generator and API (Gamma, Canva, Beautiful AI, Decktopus, Presentations AI Alternative) 项目地址: https://gitcode.com/GitHub_Trending/pr/…

作者头像 李华
网站建设 2026/7/12 17:00:45

VCS 2023.12 后仿环境配置:3种网表+SDF反标实战与10个关键选项解析

VCS 2023.12 后仿环境配置&#xff1a;3种网表SDF反标实战与10个关键选项解析1. 后仿验证环境搭建的核心逻辑门级仿真是芯片设计流程中的关键验证环节&#xff0c;其本质是通过SDF文件将物理实现的时序特性反标到网表中&#xff0c;模拟真实硅片行为。VCS 2023.12版本在时序反标…

作者头像 李华
网站建设 2026/7/12 16:59:00

群晖NAS硬盘兼容性终极解决方案:第三方硬盘完整支持指南

群晖NAS硬盘兼容性终极解决方案&#xff1a;第三方硬盘完整支持指南 【免费下载链接】Synology_HDD_db Add your HDD, SSD and NVMe drives to your Synologys compatible drive database and a lot more 项目地址: https://gitcode.com/GitHub_Trending/sy/Synology_HDD_db …

作者头像 李华
网站建设 2026/7/12 16:57:07

告别激活烦恼:KMS_VL_ALL_AIO一键解决Windows和Office激活难题

告别激活烦恼&#xff1a;KMS_VL_ALL_AIO一键解决Windows和Office激活难题 【免费下载链接】KMS_VL_ALL_AIO Smart Activation Script 项目地址: https://gitcode.com/gh_mirrors/km/KMS_VL_ALL_AIO 你是否曾因Windows系统频繁弹出激活提示而烦恼&#xff1f;是否在重要…

作者头像 李华