分布式全局唯一ID生成方案详解:从UUID到雪花算法选型指南

发布时间:2026/10/21 21:21:14
分布式全局唯一ID生成方案详解:从UUID到雪花算法选型指南
1. 先搞清楚你需要的到底是“唯一”还是“有序且唯一”1.1 从单机自增到分布式问题变了先说一个很常见的场景单库单表的时代主键直接用自增就行了省心、可读、天然有序。可一旦系统拆成多节点、多库、多表自增就不够用了。两个节点同时插入各自从1开始肯定撞车。于是分布式环境下“全局唯一ID生成”成了绕不开的基础设施问题。很多人第一反应是“保证不重复就行”其实这只是最低要求。真正上线以后你会发现还要关心生成速度、是否有序、会不会引起数据库页分裂、能不能跨机房容灾。这些点一环扣一环方案选错了后期改造成本非常高。我在几个项目里踩过类似的坑所以想把这一整套思考过程整理出来。1.2 一个合格的全局ID至少要满足这四条按照我自己的项目经验评判一个方案好坏可以从四个维度看全局唯一性不管哪个节点、哪个时刻生成ID都不能重复。这是底线没啥好说的。趋势递增性不是严格递增而是“后面的ID整体比前面的大”。这样数据库索引写入友好不至于频繁造成页分裂。高性能与低延迟生成器要能扛住业务峰值延迟最好在个位数毫秒内否则会成为全链路瓶颈。高可用与可恢复ID生成器不能单点挂了要能切换重启后也不能产生重复。这也是为什么业界几乎没有用“UUID裸用”做主键也很少直接用“数据库自增”扛高并发。下面每个方案我都会对照这几个维度展开说。1.3 容易踩的误区全局唯一不等于全局有序注意“趋势递增”跟“严格递增”是两回事。趋势递增指的是ID的序列在宏观上随时间变大但不保证后生成的ID一定比先生成的大。比如同一毫秒内生成的ID顺序可能随机但只要时间维度放得足够宽整体是递增的。很多业务用不上严格递增比如订单号可以支持后创建的订单比先创建的订单小只是概率极低。如果业务强依赖“后者一定大于前者”方案会复杂很多比如要引入中心化分配器或加锁。选型时必须先跟产品确认好这个语义不然上线后会被诡异的“乱序”问题折磨。2. 最简单但经常被骂的方案UUID2.1 UUID是怎么生成的UUID通用唯一识别码严格来说不是分布式ID生成框架它是一个标准格式的128位标识符像一个“随机字符串”。最常见的UUID v4版本直接基于随机数生成字符形如550e8400-e29b-41d4-a716-446655440000。它的核心优势是“本地生成、完全不依赖网络”每个节点自己算自己的理论上重复概率低到可以忽略。看起来完美解决了“全局唯一”。但问题也恰恰出在这里它只解决了唯一性其他维度几乎全废。128位里没有时间维度或者只有粗粒度版本的时间戳整体上是随机排序的和业务时间完全无关。你可以简单理解成图书馆的进馆登记单每个人进来的顺序是1、2、3UUID版本相当于给每个人发一个随机乱序的会员号书架上一张张塞进去最后整个索引文件会乱成一团。2.2 为什么别拿UUID直接当数据库物理主键如果拿UUID直接作为关系数据库主键随机值会被写进聚簇索引。数据库的聚簇索引本质是一棵有序的B树写入的数据如果总是插入到树的中间位置就会频繁触发页分裂、页重排索引碎片增加写入性能会明显掉下去。做过压测的话会发现同样一批数据用自增ID和UUID当主键写入吞吐能差出20%到50%数据量越大越明显。是不是意味着UUID一无是处也不是。如果ID只在系统内部使用不存库、不做索引主键只作为消息追踪ID、日志关联ID那UUID就非常好用。因为它的长度是固定的不依赖中心节点联调排障时可以在日志里直接检索。我一般会把“数据库物理主键”和“业务追踪ID”分开前者用有序ID后者用UUID或类似随机串各取所长。2.3 改造一下把时间戳合并进UUID如果既想保留本地生成的去中心化特性又希望ID带一定的时间信息可以用一个折中做法把时间戳放到48位或更高位后面再拼随机数或节点标识。它的可读性和趋势性会比纯UUID好很多比如时间戳高40位 机器码 随机数生成时带上System.currentTimeMillis()。这样虽然没有标准UUID好看但作为业务ID足够用。这个思路本质上已经是雪花算法的雏形了所以别把UUID和雪花算法看成完全对立的它们是在同一个方向上走出了两个不同形态。3. 传统但耐用的方案数据库与Redis3.1 单库自增可用性不足性能有上限最早接触分布式ID时很多人想到的是“还是用数据库自增但搞一个专门的表来发号”。比如建一张id_generator表每来一个请求就UPDATE一个字段再SELECT回来。这个方案最大的问题是把数据库变成了中心化热点单机的QPS能撑到多少ID生成能力就是多少。为了扩性能有人用两库交替步长比如A库生成1,3,5B库生成2,4,6。这种思路能提升一点容量但步长和实例数要提前定死扩缩容时非常痛苦。我见过有团队用双库方案后来加一个节点时发现ID要重新规划线上改配置还出了一次重复ID事故。3.2 号段模式一次取一批ID避免频繁访问数据库号段模式是目前最成熟的数据库发号方案。核心逻辑是数据库里保存一个max_id和一个步长step每次请求到来业务服务从数据库取一段“号码区间”比如[1001, 1100]然后这段区间内的ID在本地内存中分配。当这段用完了再向数据库申请下一段[1101, 1200]。这个方案把数据库压力降低了几个量级因为数据库只在取号段时被访问而不是每次都访问。步长取值很关键。我见过默认取500或1000如果业务峰值是每秒1万想要撑过峰值的持续时长假设峰持续30秒需要一次性取的号段至少是30万。不过取太大也有副作用因为号段一旦取了没用完服务重启就会丢弃一部分造成ID空洞。所以步长不是越大越好而是刚好覆盖“从取号段到下一次取号段”的最大消耗再留20%缓冲。这一步需要压测和监控数据支撑不能拍脑袋。3.3 基于Redis的原子自增方案快但要处理好持久化与过期Redis的INCR命令可以在单线程模型下保证原子自增性能非常高实现也最简单直接对某个key执行INCR。它适合对ID趋势性要求不高的场景配合EXPIRE设置过期时间还能实现按天分key比如order_id_yyyyMMdd。每天从1开始既保持了可读性又避免了单key无限增大。但Redis方案有两个坑必须提前想清楚。第一如果Redis做了持久化要确认快照和AOF的同步机制是否可能拿到重复值。比如主从切换时从节点还没同步完最新INCR值就可能把已经发出去的ID又发一次。处理办法是在Redis上层加一个“发号服务”启动时从业务表当前最大值重新校准或者干脆接受少量空洞、只保证趋势递增。第二Redis本身也是一个中心节点如果ID生成服务直接怼到生产Redis一旦Redis抖动全部业务都会受影响。所以更稳妥的做法是把Redis作为发号器的存储端而不是让各业务系统直连。4. 高并发的主流选择雪花算法4.1 雪花算法的位段设计既然上面的方案要么依赖中心节点要么不好控制顺序那就需要一种“本地生成、带时间、有序、不依赖外部存储”的算法。雪花算法是目前最主流的选择。经典的雪花算法把64位long型ID分成三段第1位固定为0表示正数接下来41位存时间戳毫秒级可以表示约69年的时间跨度中间10位存机器标识再细分为数据中心ID和机器ID各5位支持1024台机器最后12位存同一个毫秒内的序列号支持每台机器每毫秒生成4096个ID。这样算下来雪花算法单机单毫秒最多生成4096个ID单机每秒理论值约409.6万。实际上业务很少真达到这个量级但即使打折到几十万QPS也已经明显高于数据库和纯Redis方案。它最大的优势是不依赖外部系统每台机器按照自己的时钟和机器码算ID天然去中心化性能极高。我在实际中会写一个简单的生成代码核心逻辑大致是取当前时间如果和上次时间相同序列号加一如果序列号满了就等到下一毫秒如果当前时间比上次时间还小就认为发生了时钟回拨需要特殊处理。这个判断过程看似简单但边界条件很多后续会详细展开。4.2 机器ID与序列号的分配策略机器ID分配看似是小问题其实最容易翻车。如果两台机器用了同一个机器ID序列号又不凑巧就可能会生成重复ID。最稳妥的办法是依靠注册中心分发比如服务启动时向注册中心申请一段机器ID编号申请成功后才能对外提供发号能力。没有注册中心的小团队最简单的做法是用IP后几位加进程号做组合虽然不能100%保证全局不重复但可以把冲突概率降到很低。如果追求更严谨就要做启动时自检比如向Redis或数据库登记并加锁拿到唯一标识后才启动。序列号的作用是在同一毫秒内区分不同请求。实现时可以每次递增也可以每来一个请求取一个随机数。随机序列号的好处是让同一毫秒内的ID顺序不可预测避免安全风险但代价是可能浪费一部分容量。业务如果对ID有可读性要求也可以用“序列号业务类型码”的组合设计把后12位拆开一部分表示业务线一部分表示序号不过这会降低同毫秒容量需要根据业务量判断。4.3 时钟回拨是雪花算法最大的坑雪花算法强依赖系统时间一旦服务器时间向后调整就可能生成比之前更小的ID甚至重复。这是生产环境里最头疼的问题。常见的处理策略有三类第一种快速失败。发现当前时间小于上次生成时间直接抛异常或拒绝服务。适合并发量可控、回拨极少发生的场景宁可不可用也不能出错。第二种等待追赶。记录回拨的时间差最多等待几毫秒或几秒等系统时钟追上来再继续。这个方案赌的是回拨幅度不大但要注意等待过程中必须阻塞线程否则仍有重复风险。第三种把时间戳换成逻辑时钟。在雪花算法的时间位上方再抽象一层维护一个单调递增的虚拟时间如果物理时钟回拨虚拟时间就加一而不是退回。这种方案可以完全避免回拨但实现复杂度高需要额外存储上次的虚拟时间。我个人建议的方案是“策略二 上限保护”正常情况下本地生成发现时钟回拨时结合秒级或毫秒级的时间补偿并加上一个“最大可容忍回拨值”超过该值时再快速失败同时报警。这样既能防住误操作导致的大回拨又能容忍云服务器偶尔的时间同步校时。4.4 变体可定制雪花算法与跨机房扩展标准雪花算法里机器ID是10位但如果公司只有几十台机器可以用更少的位数表示机器把空出来的位让给序列号提高单机容量。反过来如果跨机房部署想保证不同机房的ID在趋势上错开可以把数据中心ID独立出来比如用3位表示机房5位表示机器4位表示序列号。这个没有标准答案需要根据自身规模和增长预期来拆位。还要留意一个需求ID中是否要带业务信息。有时产品希望从订单号里直接看出地区、业务线甚至年份。可以在64位之外拼接业务前缀或者用128位的自定义ID。但这样会牺牲UUID式的随机性或者可计算的排序性。我的原则是ID尽量保持简短业务信息放数据库独立字段不要在ID本身堆太多含义。5. 选型参考结合业务规模选最合适的轮子5.1 从QPS、节点数、成本三个维度做对照以我见过的项目为例不同规模适合的方案差异很大。如果业务QPS只有几百、机器只有几台直接用数据库号段方案就足够了稳定、可控、易排查如果QPS到几千且需要跨机房号段Redis或雪花算法都行主要看团队是否已经运维了相应组件如果QPS到几万甚至更高基本就是雪花算法或基于其思想的自研发号器。做一个粗略选型表方案唯一性保障趋势递增抗并发运维复杂度适用场景UUID v4概率唯一否极高低日志追踪、临时ID数据库自增强是低低低并发的准入门ID数据库号段强是中中中小规模业务主键Redis自增强需注意恢复是较高中对趋势递增要求不高的数据雪花算法强前提时钟不回拨是很高中高高并发、海量数据这个表只是起步参考不要据此一刀切。真正选型时还要看团队对基础设施的掌控能力比如有没有注册中心、有没有成熟的配置中心如果没有雪花算法反而会比较难落地。5.2 性能压测与容量规划怎么做无论选哪种方案上线前都要做一轮发号服务的压测不能只在联调环境里跑通就完事。压测时重点看三组数据TPS峰值、TP99延迟、CPU和内存占用。比如本地雪花算法压测理想状态是可以达到数十万到百万级QPS但在真实容器里受CPU抢占、垃圾回收、网络调度影响实际值会缩水不少。建议压测时把业务可能的峰值流量放大1.5到2倍跑至少10分钟观察序列号是否够用、机器时钟是否稳定。容量规划方面以雪花算法为例如果一台机器分配了某个机器ID且同一毫秒内有超过4096个请求要么等待下一毫秒要么扩展机器ID或拆多个发号节点。更常见的做法是对业务做分片比如订单和支付各用一个发号器互不争抢序列号。我见过一个项目所有业务共用一个发号器高峰时单机单毫秒4000出头导致线程全部阻塞。后来按业务拆了三个发号器问题瞬间消失。5.3 从运维角度看看这些方案数据库号段方案的最大优点是可观测性强你随时能查的到当前发到哪个号段了出问题时好人工介入修复。它的缺点是每次扩容或调整步长需要重启且数据库依旧是最终状态来源。Redis方案的优缺点前面提过稳定性非常依赖持久化配置。雪花算法的运维难点反而在机器ID管理上机器ID不能随便重用销毁一台机器后最好有回收状态记录否则隔了好几个月新机器启动可能碰上旧ID。这一块我吃过亏。某次扩容时我图省事直接复制了老机器的配置忽略了机器ID字段结果两台机器同时对外发号线上出现了重复ID。排查到问题后对机器ID配置做了规范强制走注册中心申请才彻底解决。这个教训让我明白ID生成方案不仅要看代码实现还要把配置管理、发布流程一起约束住。6. 写在最后我踩过的几个坑和一点建议这里不做系统总结单纯分享几条实操经验。第一先定回拨容忍度再选算法。我在一个项目里用标准雪花算法没处理回拨某次压测环境系统时间被回调了几十秒ID直接重复数据写坏后修了一整天才恢复。从那以后任何用雪花算法的项目上线前必须明确回拨容忍时间和降级策略。第二发号逻辑尽早独立成服务。哪怕刚开始业务量很小也不要让各个服务自己实现一套ID生成。把发号能力收敛成一个独立服务或独立类库后续换方案只需改一处。我见过很多团队各自复制粘贴代码改造时像打地鼠漏一个就是线上事故。第三把ID生成纳入监控体系。无论哪种方案都应该记录生成失败率、回拨次数、号段剩余量、Redis连接异常等指标。一个看起来不起眼的“小模块”一旦出问题影响的就是全链路没有监控很难定位。最后再分享一个小技巧。如果使用雪花算法ID设计时可以预留几位“版本号”或“类型位”不一定要全部用完。后续如果需要区分新老发号器、不同业务线只要这些位还在就能平滑升级不用重构ID结构。类似这种细节往往比选型本身更能决定系统能否长期稳定运行。希望这个拆解能对你有所帮助。如果你正准备做分布式ID这个模块我的建议是从业务真实量级出发不要盲目追新方案把“唯一、有序、高性能、可恢复”这四个点逐一验证清楚基本能避免大部分天坑。