资讯动态

分布式ID生成算法全解析:九种方案与选型指南

发布时间:2026/10/15 17:30:23 来源:尧图企业网站定制
做后端这几年几乎每个项目都会碰到ID生成这件事。单机时代一个自增主键就能打天下但服务一旦拆成多个实例数据库分库分表之后事情瞬间就变复杂了。分布式ID生成算法这个词听起来偏理论实际上它是每次接口联调、每次订单落库都可能踩到的门槛。这篇文章我会把九种主流方案从头到尾捋一遍包括它们的基本原理、优缺点、适用场景以及我在真实项目里的取舍思路给正在做技术选型的你一个参考。1. 分布式ID的评判维度五个指标决定选型1.1 硬指标全局唯一、趋势递增、高性能、高可用先看第一个问题为什么单机自增在分布式环境下会失效当你把一张订单表拆到多个数据库实例每个库各自维护自增主键时会出现不同库生成相同ID的情况数据合并后必然冲突。所以分布式ID的第一条硬指标就是全局唯一无论多少节点并发生成都不能重复。光唯一还不够还要考虑数据库索引的物理存储特性。MySQL的InnoDB聚簇索引在插入数据时如果ID是趋势递增的新记录几乎总是追加到索引末尾附近页分裂的概率很小写入性能稳定。如果ID是完全随机的每一次插入都可能落在B树的中间位置导致频繁页分裂、随机IO和碎片化写入性能会明显下滑。所以趋势递增往往关系到系统在数据量增长后的写入表现。然后是高性能和高可用。ID生成通常处在核心业务链路的必经路径上生成一次ID延迟几十毫秒肯定是不能忍的。高可用指的是某个节点挂了或某个组件抖动时系统依然能正常发号而不是让整个业务跟着停摆。这两条指标直接决定了你不能把“发号”做成一个脆弱环节。1.2 隐藏指标安全性、可读性与可解析性除了上面四条实践中还有两个隐藏指标需要提前想清楚。第一个是安全性。如果你生成的ID是连续递增的比如订单号从1001到1002再到1003竞争对手或用户就能推断出你的订单量、日活、增长速度这属于业务信息泄露。很多电商场景宁愿用一个看起来没有规律、甚至包含随机位的ID也不愿意暴露真实规模。第二个是可解析性。雪花算法生成的ID内部其实包含时间戳和机器编号拿到一个ID就能反推出大约生成时间和由哪台机器生成。这在排查问题时非常有用但也会带来隐私风险。反过来纯随机的UUID虽然完全无法反推信息但排序、分页、范围查询会很麻烦。也就是说真正选型时你往往是在“趋势递增、信息安全、可解析性、复杂度”这四者之间做权衡。不存在一个在所有维度都碾压其他方案的算法只有最匹配你业务约束的方案。2. 九种主流算法原理与适用场景详解2.1 UUID最“懒”的本地生成方案UUID严格来说不算分布式组件它完全在本地生成不需要请求任何外部服务所以一上来就满足高可用和高性能。UUID的标准形式是8-4-4-4-12的36位字符串128bit。常用的是UUID v4内部由122bit随机数组成理论上重复概率极低UUID v1则包含时间戳和机器MAC地址可解析性稍强一点。它的优点是简单到不需要思考任何语言都有现成实现一行代码就能拿到一个全局唯一ID。缺点也非常直接它是一个非常长的字符串而且不具备趋势递增性。假如你用UUID作为MySQL主键前面说过完全无序的插入会让聚簇索引产生大量页分裂写入放大明显。同时36字符的字符串做主键每个二级索引都会冗余一份主键存储和内存开销比8字节的bigint大好几倍。因此我的习惯是UUID更适合用在链路追踪、日志traceId、临时文件名、会话token这类不参与数据库主键索引、不需要排序的场景。如果数据库主键你想用类似思路建议优先考虑后面会提到的雪花或号段方案。2.2 数据库自增与多主步长最简单但有硬伤单库自增ID是最原始的方式就是MySQL的AUTO_INCREMENT。它满足唯一、趋势递增、简单可读。问题在于单库写入能力和可用性都有限数据库一旦主从切换或压力过大发号也跟着出问题。为了扩展有人会考虑多主步长比如三台数据库分别设置auto_increment_offset为1、2、3auto_increment_increment固定为3这样各自生成的ID在取模意义上不会冲突。这套方案虽然解决了单点写瓶颈但扩展性极差。假如业务量增长你需要从3个节点扩到5个步长改成5之后旧ID规律全部作废已经生成的ID和未来新生成的ID可能跨区间冲突改造风险不小。ID的规律性也强几乎等于把业务吞吐量写在脸上。这类方案适合极低并发、对ID规律不敏感的后台管理系统或者临时内部工具。真正一到核心高并发链路数据库自增或多主步长几乎不会出现在长远的架构方案里。2.3 号段模式Segment数据库自增的高并发升级版号段模式的出现是为了解决数据库自增方案性能和扩展性不足的问题。它的核心思路是让数据库不再逐条生成ID而是每次分配一批号段给应用节点。比如应用启动时执行一次更新把某业务key的max_id从1000改成2000那么应用就独占1001到2000这一千个ID在本地内存中按顺序发放用完再找数据库要下一批。这里用一张简单的表就能实现CREATE TABLE id_generator ( biz_tag varchar(32) NOT NULL COMMENT 业务标识, max_id bigint NOT NULL COMMENT 当前已发放的最大ID, step int NOT NULL COMMENT 每次取号步长, version int NOT NULL COMMENT 版本号用于乐观锁, update_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (biz_tag) ); UPDATE id_generator SET max_id max_id step, version version 1 WHERE biz_tag ? AND version ?;UPDATE语句执行后用SELECT把新的max_id取回来应用就得到了一个区间。因为一次更新能支撑上千次内存发号数据库的访问压力被大大稀释。乐观锁的version字段可以避免并发取号时相互覆盖防止多个节点拿到重叠区间。更进一步生产级实现往往会做“双Buffer”优化维护两个号段第一个号段使用到一定阈值时后台异步去数据库加载下一个号段。这样即使取号耗时波动本地也不会出现线程阻塞发号吞吐能保持稳定。它的优势很明显ID趋势递增、性能远高于数据库自增、某个节点宕机只损失内存中一小段未发放的ID不会影响全局。缺点是ID连续、可预测数据库本身仍然是单点一旦数据库不可用新号段就取不回来。所以它特别适合订单号、用户ID、优惠券编号这类需要在数据库里整体规划、同时并发量又比较高的业务。2.4 Redis INCR原子自增的快速通道Redis的INCR命令可以简单地实现发号器SET一个业务key之后每次调用INCR就返回一个递增的long值。因为Redis是单线程事件模型INCR天然原子多个应用节点同时调也不会重复。相比数据库自增它把发号延迟降到极低单机Redis每秒可以支撑十万级别的INCR操作。但这里藏着几个坑。Redis如果使用RDB快照持久化快照之间的数据丢失可能导致ID回退主从切换时从库可能落后于主库新主库的counter值比旧主库小重新发号就有重复风险。我自己就见过一个真实事故发号key设置了24小时过期第二天key被Redis淘汰重建ID从1重新开始直接把业务表里已有的主键撞了线上报了一大堆DuplicateKey。所以用Redis发号持久化策略要选AOF并且绝对不能对发号key设置过期时间。用量大的业务更推荐用INCRBY一次取一批号比如每次取1000然后本地分配。这样既能减少网络RTT也能降低Redis故障时对业务的冲击。总体来说Redis发号适合短链接、流水号等对趋势递增有要求、但能接受Redis依赖和一定丢失窗口的业务。如果ID要求严格不重复且长时间不丢建议还是用号段或雪花方案。2.5 经典雪花算法分布式ID的事实标准Snowflake算法最早是知名社交平台内部设计的ID生成方案后来业界广泛采用几乎成了分布式ID的代名词。它生成的ID是一个64位的long正好对应数据库的bigint。标准位分配是1bit符号位固定为041bit毫秒时间戳10bit工作机器ID通常拆成5bit数据中心ID加5bit机器ID12bit同一毫秒内的自增序列号。41bit的时间戳如果从某个固定纪元开始算大约可以使用69年而不溢出。12bit序列号意味着同一台机器在同一毫秒内最多生成4096个ID单机每秒的理论生成量是409.6万。这个吞吐量对绝大多数业务绰绰有余。一个简化版Java实现大概长这样public class SnowflakeIdGenerator { private final long epoch 1609459200000L; // 自定义起始时间戳 private final long workerIdBits 10L; private final long sequenceBits 12L; private final long workerId; private long lastTimestamp -1L; private long sequence 0L; public SnowflakeIdGenerator(long workerId) { this.workerId workerId; } public synchronized long nextId() { long timestamp System.currentTimeMillis(); if (timestamp lastTimestamp) { throw new IllegalStateException(Clock moved backwards); } if (timestamp lastTimestamp) { sequence (sequence 1) 4095; if (sequence 0) { timestamp waitNextMillis(lastTimestamp); } } else { sequence 0L; } lastTimestamp timestamp; return ((timestamp - epoch) 22) | (workerId 12) | sequence; } private long waitNextMillis(long lastTimestamp) { long timestamp System.currentTimeMillis(); while (timestamp lastTimestamp) { timestamp System.currentTimeMillis(); } return timestamp; } }这段代码有两个关键点时间戳、workerId、序列号通过位运算拼到一个long里时间戳放在高位所以从整体趋势上看生成的ID是递增的。序列号在毫秒内耗尽时自旋等待下一毫秒即可。雪花算法的优点是几乎所有能力都在本地完成不依赖数据库、不依赖Redis性能极高ID趋势递增排序友好还能从ID中解析出生成时间对排查问题时非常有帮助。缺点也很集中——它依赖系统时钟。如果机器的系统时间发生回拨比如NTP校正导致时间往回跳在重复的时间戳下就可能生成重复ID且代码里通常只能抛异常或阻塞等待搞不好就拖垮业务。workerId也需要额外管理不能重复否则会破坏唯一性。即便有这些限制雪花算法仍然是目前订单、交易、账户等核心链路里最主流的方案。后续很多方案都可以看作在雪花算法基础上对位分配或协调方式做了优化。2.6 增强型雪花变体位分配可定制的UidGenerator思路经典雪花算法的41bit时间戳加10bit workerId加12bit序列号是一种默认配置但并不是唯一配置。不同业务场景对位分配的需求不同比如有的业务并发极高每秒千万级12bit序列号在毫秒内很快耗尽有的业务希望ID支持更长周期希望时间戳单位从毫秒改成秒。于是业界出现了一批增强型雪花实现核心思路就是把64bit按业务特征重新切分。举一个常见的增强方案1bit符号位 28bit序列号 22bit秒级时间戳 13bit工作节点ID。这样配置的好处是序列号位宽大幅提升单节点每个时间单位内可生成的ID数量比经典雪花多得多时间戳使用秒级相同bit数覆盖的年数更长workerId位宽也更大能支持更多节点。但这会牺牲ID的即时可解析性你不能一眼知道ID的精确生成时间只能定位到秒级。另一种增强方向是在应用层用RingBuffer数组预生成一批序列号线程在并发获取时通过无锁的原子指针消费。这样是为了降低并发竞争从CAS角度提升吞吐。很多高并发日志系统、消息系统在用这类变体因为它们不在乎ID是毫秒还是秒级排序只在乎单节点每秒能爆出多少不重复的long。这类方案适合并发写入压力极大、对时间精度要求不高、愿意自己做位配置和workerId管理的团队。代价是实现复杂度明显上升如果并发量还没到那个量级经典雪花反而更容易运维。2.7 一体化发号器号段与雪花结合的中间件思路在真实的大型业务系统中纯雪花要处理时间回拨问题纯号段又绕不开数据库单点。于是业界出现了一类组合方案常见的思路是用一个数据库表负责分配“节点标识”每个应用节点启动时从DB获取一个全局唯一的workerId或一段ID空间之后在本地用雪花算法或号段内存发放生成ID。这种组合的好处是把全局协调次数压到最低。节点启动后发号主路径完全在本地完成数据库只做低频的节点注册。相比DB号段它不要求每次用完一批就回来取号相比纯雪花workerId由DB统一分配不会出现两台机器配置了相同workerId的问题。另外一体化方案普遍内置了时间回拨处理策略比如小范围回拨时继续消耗序列号或切换到备用时间戳大范围回拨时快速摘除节点避免生成重复ID。这种方案通常包含管理后台、监控面板和心跳上报能力适用于订单、支付流水等需要高可靠、高可用、趋势递增的核心链路。但实话实说要自己从零实现一套这种组件工程量不小。选型时如果你所在团队已经有人维护这类发号服务优先复用如果没有很多人会先用经典雪花加一个workerId配置文件也能扛住前期业务量。2.8 ZooKeeper顺序节点强一致但重依赖ZooKeeper的顺序节点天然可以当作ID生成器每次创建一个持久顺序节点比如在/id目录下创建seq_节点ZK会自动在节点名称后面追加一个全局递增的序列号。多个客户端并发创建时序号是严格递增且不重复的。这个方案的优势是强一致序号不会丢失不会回退纯粹的有序数字。但它的问题也很明显ZK的写性能有限单集群TPS能达到的量级远不如本地发号发一次ID等于一次网络IO加一次持久化在毫秒级高并发场景下完全顶不住另外ID形式是路径字符串直接用它当订单号还要再做转换。一个节点挂了或网络分区客户端要处理Session过期发号请求就会失败。所以ZK顺序节点更适合低频但要求严格有序的场景比如分布式任务调度的任务ID、审批流实例编号、全局配置的版本号。核心高并发链路不要把它放在发号主路径上。2.9 MongoDB ObjectId另一种组合思路MongoDB的ObjectId常被忽略但它也是分布式ID中很有代表性的一种由12字节组成其中4字节是Unix秒级时间戳5字节是机器相关的随机值3字节是自增计数器。客户端驱动在本地就能生成不需要数据库参与。它和雪花一样都是“时间 机器 计数”的组合思路区别在于ObjectId的时间戳在最高位且以秒为单位因此多个客户端生成的ID整体趋势上有序但同一秒内不同客户端由于随机值不同ID之间没有严格的先后顺序。相比UUID它要短不少但还是12字节转成字符串算是24个十六进制字符。ObjectId适合文档类NoSQL、日志存储这类以对象存储为核心的场景。如果团队主要在关系型数据库上做大批量事务用它做业务主键的优势并不明显更建议直接用雪花后的long型ID。3. 九种算法横向对比与选型建议3.1 一张表看清九种方案算法唯一性来源趋势递增典型性能外部依赖推荐场景UUID本地随机/时间MAC否极高无链路追踪、日志traceId、tokenDB自增数据库自增是低数据库低并发后台、内部系统DB号段数据库分配号段是中高数据库订单号、优惠券、用户IDRedis INCRRedis原子计数是高Redis短链接、流水号、验证码编号经典雪花时间戳机器ID序列是极高本地时钟订单、交易、核心业务ID增强雪花定制位分配无锁序列是极高本地时钟worker管理超大并发日志、消息ID一体化发号器DB分配节点本地发号是极高数据库本地支付流水、高可靠核心链路ZK顺序节点ZK顺序节点是严格低ZooKeeper任务ID、低频严格有序ObjectId时间戳随机计数大致趋势高无文档型数据库、日志存储3.2 按场景选型的三个示例第一个场景是订单中心。订单量增长快又需要给用户展示订单号同时数据库是分库分表架构。这里我建议选经典雪花或一体化发号器原因有三个趋势递增能保证落库顺序好long型存储紧凑还可以从订单号反解出生成时间方便排查。如果不想暴露订单量可以在对外展示时对ID做一次可逆混淆编码。第二个场景是内容社区的评论ID或私信ID。这类数据量很大对排序要求不严格就算ID中间存在空洞也无所谓。用DB号段方案就能获得很好的性能而且ID趋势递增分页查询很容易。数据库压力也不大一个取号段操作能支撑上万次发号。第三个场景是分布式任务调度。任务ID需要全局严格有序以便判断执行顺序但生成频率很低远达不到每秒几千。这种情况下ZK顺序节点反而最合适强一致、绝对有序还顺便能借助ZK做分布式协调。4. 实战中的坑与排查技巧4.1 时间回拨雪花算法最大的敌人我真正在生产环境踩到过时间回拨的坑。某次一台应用服务器NTP校时后系统时间往回跳了几十毫秒雪花算法的代码立刻抛出异常。因为发号接口被核心链路调用异常导致一批请求失败。这类问题最典型的两种表象要么日志中出现大量时间回拨异常要么数据库出现主键冲突。处理时间回拨业界有几种常见策略。简单粗暴的做法是发现回拨就抛异常或持续阻塞到时间追平适合回拨频率极低的场景。更稳妥的做法是维护lastTimestamp当检测到回拨时先判断回拨幅度是否小于某个阈值比如500ms以内就继续在当前时间戳内递增序列号而不是退回到旧时间戳如果回拨幅度过大就从备用号段或Redis里申请临时workerId切换节点服务。最后一种是在架构层面做多节点冗余单个节点触发时间回拨时直接把自身摘除让其他节点接管发号用户无感知。4.2 号段模式与Redis模式的重复ID问题号段模式一个非常隐蔽的坑在于应用节点内存中还有未使用的ID时发生了宕机随后重启会从数据库再取一个新号段。如果取号逻辑写得不严谨比如max_id在UPDATE和SELECT之间存在并发窗口两个节点可能拿到重叠区间。解决方式是用乐观锁保证max_id的更新是原子的UPDATE成功后再回读新值。注意UPDATE和SELECT必须在同一个事务里或者由同一连接完成才能避免读到旧值。Redis发号重复的坑前面提过。要特别提醒的是AOF的appendfsync如果配成everysec极端情况下会丢最后一秒数据恢复后counter可能回退。你可以接受你业务里出现“少量ID空洞”但绝不能接受“ID重复”。所以如果系统对ID唯一性有强要求Redis方案一定要额外做启动时的补偿检查比如持久化一个已生成最大ID的快照恢复时主动把counter拨回到快照值以上。4.3 压测验证唯一性与稳定性选型完成后一定要做并发压测来验证方案。我自己常用的做法是写一个多线程测试用200个线程同时调用发号接口每个线程生成50万ID最终收拢到Set里去重检查是否有碰撞。同时会在压测过程中人为做一次NTP时间回拨注入观察系统能否自动恢复。测试中要特别留意几个指标生成一定数量ID的耗时曲线是否平缓数据库或Redis相关线程数量是否被打满节点重启后是否出现ID回退或重复。只测单机吞吐没有意义长时间高负载下的稳定性才是关键。靠运气不如在测试阶段把异常场景全部模拟一遍。4.4 不要忽略ID泄露问题最后说一个经常被忽略的事雪花算法生成的ID理论上可以把时间戳部分解出来从而推断系统创建时间和业务量级。如果你的ID会暴露到外部比如充当订单号、优惠码或对外接口的资源ID就需要额外考虑混淆。有两个常见手段。一个是对外展示时用可逆编码方案内部保留long型ID出口处转成短字符串入口再解析回来优点是对已有系统改动小。另一个是在ID里混入随机位破坏可预测性但会让ID失去严格趋势递增的属性。如果业务对趋势递增和排序有硬要求一般还是优先考虑在展示层做处理而不是改底层ID结构。分布式ID算法解决的是“全局唯一、趋势递增、高性能”这些基本盘不要把安全混淆也交给它一把抓。九种算法没有银弹“标准答案”永远是结合团队运维能力和业务特点做匹配。我这几年用过纯自增、踩过UUID主键的坑也维护过雪花和号段结合的发号链路。回头看比较稳妥的做法是先把方案复杂度压到最低能用一个简单方案撑住半年业务增长就先上等真的出现性能瓶颈或跨地域容灾需求时再逐步演进到一体化发号器。发号看起来是个小点但它趴在所有核心链路的底座上值得你多花几天时间把方案选扎实。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑