
JCSprout 分布式 ID 生成器全解析从数据库自增到 Twitter 雪花算法【免费下载链接】JCSprout Java Core Sprout : basic, concurrent, algorithm项目地址: https://gitcode.com/gh_mirrors/jc/JCSprout在分布式系统中ID 是贯穿全局的关键业务属性——订单 ID、消息 ID、会话 ID 都需要一个可靠的生成机制。JCSprout 仓库中的 分布式 ID 生成器对应在线版 docs/distributed/ID-generator.md系统梳理了业界主流的四种生成方案及其利弊。本文以该文档为核心骨架结合仓库中分库分表相关的实践文档深入剖析每种方案的原理、优缺点与适用场景帮助你为业务场景选对 ID 生成策略并彻底理解 Twitter 雪花算法的位级设计。分布式 ID 的两个核心诉求一个唯一 ID 在分布式系统中是非常重要的业务属性其中包括订单 ID、消息 ID、会话 ID 等它们都有一些共有的特性全局唯一唯一标识某一次请求、某一个业务避免在多个节点之间产生冲突。趋势递增ID 随时间推移整体呈增长趋势这在数据库索引、排序、分页等场景中尤为关键。全局唯一很好理解——它保证了无论在哪个节点、哪个时刻生成的 ID在整个系统范围内都不会重复。而趋势递增则意味着新生成的 ID 通常会大于旧 ID这对依赖主键顺序的存储引擎如 MySQL InnoDB 的聚簇索引非常友好能减少页分裂、提升写入性能。围绕这两个诉求业界通常有以下几种方案基于数据库自增、本地 UUID 生成、采用本地时间以及Twitter 雪花算法。下面逐一展开。基于数据库auto_increment 自增 ID可以利用 MySQL 中的自增属性auto_increment来生成全局唯一 ID也能保证趋势递增CREATE TABLE id_generator ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, PRIMARY KEY (id) ); INSERT INTO id_generator () VALUES (); SELECT LAST_INSERT_ID();这种方案实现最简单数据库天然保证全局唯一与递增无需任何额外代码。但它的致命短板是强依赖 DB——如果数据库挂了ID 生成服务也就随之中断非常容易出问题同时在写入瓶颈明显时单库的吞吐能力会成为系统的天花板。水平扩展改进奇偶步长针对单库的吞吐瓶颈文档给出了水平扩展的改进思路将数据库水平拆分为两个库 A 库和 B 库通过设置不同的自增步长与起始值来错开 ID 区间库起始值步长生成的 ID 序列A 库020, 2, 4, 6, 8, ...B 库121, 3, 5, 7, 9, ...-- A 库 SET auto_increment_offset 0; SET auto_increment_increment 2; -- B 库 SET auto_increment_offset 1; SET auto_increment_increment 2;这样两台数据库实例可以并行生成 ID提高了系统可用性并且 ID 整体仍是趋势递增的。不过该方案也存在明显的局限扩容困难之前已经定义好了 A、B 库递增的步长新加入的数据库难以融入现有步进体系水平扩展非常困难。强依赖数据库仍然强依赖数据库本身且如果其中一台挂了ID 序列就不是绝对递增了会出现区间断层或跳号。本地 UUID 生成还可以采用UUID的方式生成唯一 ID。由于是在本地生成没有网络之类的消耗因此效率非常高。例如 Java 中直接调用String uuid UUID.randomUUID().toString(); System.out.println(uuid); // 例如550e8400-e29b-41d4-a716-446655440000但这种方式也有几个问题无序性生成的 ID 是随机的、无序的不能做到趋势递增无法利用索引的顺序性。不适合做主键UUID 是字符串且不递增作为主键时不仅占用空间大还会因为随机性导致索引频繁分裂所以不太适合用作数据库主键。综合来看UUID 适合对排序无要求、对唯一性要求极高的场景如生成消息去重键但不适合作为数据库主键。采用本地时间毫秒数 业务 ID这种做法非常简单利用本地的毫秒数加上一些业务 ID 来拼接生成唯一 IDlong id System.currentTimeMillis() * 1000 businessId % 1000;这样可以做到趋势递增并且是在本地生成效率也很高。但它有一个致命的缺点当并发量足够高的时候唯一性就不能保证了——同一毫秒内如果多个请求的业务 ID 取值相同就会碰撞产生重复 ID。时间戳的粒度限制了单位时间内可生成的 ID 数量高并发下极易撞车因此在生产环境不能单独依赖这种方案。Twitter 雪花算法Snowflake可以基于 Twitter 的Snowflake算法来实现分布式 ID。它主要是一种划分命名空间的算法将生成的 ID 按照机器、时间等维度来进行标志从而在无中心协调的情况下保证全局唯一与趋势递增。64 位二进制位布局雪花算法生成的 ID 是一个 64 位 long 型整数标准布局如下Snowflake 算法的公开设计本仓库文档将其概括为按机器、时间等划分命名空间位区间长度含义第 63 位最高位1 bit符号位固定为 0保证 ID 为正数第 62 ~ 22 位41 bit时间戳毫秒级可表示约 69 年第 21 ~ 12 位10 bit机器 ID可细分为机房 ID 机器 ID最多支持 1024 个节点第 11 ~ 0 位12 bit同一毫秒内的序列号可支持 4096 个不同 ID整体换算为公式即ID (时间戳 22) | (机器ID 12) | 序列号。其中41 位时间戳决定了 ID 的趋势递增性——同一时刻生成的 ID 必然大于更早时刻生成的 ID且从时间上可以直接反推生成时刻。10 位机器 ID为每台机器分配独立命名空间从根源上避免多节点冲突这正是文档所说按照机器、时间等来进行标志。12 位序列号保证同一毫秒内同一台机器上最多可以生成 4096 个不重复 ID当序列号溢出时会自动等待到下一毫秒再生成。优点与注意点雪花算法的优势非常明显本地生成、无网络开销、效率高ID 为 64 位长整型趋势递增非常适合作为数据库主键也是目前业界使用最广泛的分布式 ID 方案之一。但同时需要注意几个工程上的细节依赖机器时钟ID 的递增性建立在服务器时间单调递增的假设之上一旦发生时钟回拨如 NTP 校时就可能生成重复 ID实现时需要做相应的容忍或等待策略。需要分配机器 ID需要一套机制为每台机器分配唯一的工作节点 ID保证 10 bit 命名空间不冲突。69 年上限41 位毫秒时间戳从某个起始纪元开始计数只能覆盖约 69 年需要合理设定起始时间并预估业务生命周期。仓库实践佐证分库分表场景下的 ID 选型本仓库虽然没有内置雪花算法的 Java 实现代码但相关文档在分库分表场景中反复提及了分布式 ID 的必要性与选型可以作为本文的实战佐证。在 数据库水平垂直拆分 中作者指出分表之后的新增数据需要生成 ID再根据 ID 取模路由到正确的物理表比如新增一条数据的时候往往需要一张临时表来生成 ID然后根据生成的 ID 取模计算出需要写入的是哪张表也可以使用分布式 ID 生成器来生成 ID。而在 一次分表踩坑实践的探讨 中作者结合真实的分表实践给出了更具体的选型结论——分表后不能再依赖单表的字段自增可选方案包括时间戳 随机数可满足大部分业务。UUID生成简单但没法做排序。雪花算法统一生成主键 ID。同时该文还强调了一个与 ID 相关的关键细节分表数量需要取 2 的 N 次方这样在做sharding 字段 % 分表数量的取模路由时后续即使再扩容分表受影响的数据也能尽量小。这与本文讨论的 ID 生成方案共同构成了分库分表落地的完整拼图。方案对比与选型总结方案全局唯一趋势递增生成位置主要问题适用场景数据库自增✅✅DB 侧强依赖 DB单点瓶颈低并发、单机或可容忍故障的业务数据库水平扩展奇偶步长✅✅非绝对DB 侧扩容困难仍强依赖 DB中等并发、可接受跳号的场景本地 UUID✅❌本地无序、字符串、不适合主键消息去重、非排序场景本地时间 业务 ID高并发下❌✅本地高并发下唯一性无法保证低并发辅助手段Twitter 雪花算法✅✅本地依赖时钟、需分配机器 ID高并发分布式系统的主流选择从仓库文档的演进脉络看作者的推荐路径非常清晰优先考虑雪花算法——它兼具本地生成的高效率、全局唯一性与趋势递增特性是分布式高并发系统中最稳妥的默认选择只有当业务对排序没有要求时才考虑 UUID而单靠数据库自增或本地时间拼接则要警惕它们在可用性、扩容与高并发唯一性上的硬伤。理解了每种方案的位级设计与边界条件你就能在真实业务中做出有理有据的选型决策。【免费下载链接】JCSprout Java Core Sprout : basic, concurrent, algorithm项目地址: https://gitcode.com/gh_mirrors/jc/JCSprout创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考