百万并发下的ID生成架构演进
第一部分:开场——高并发下的“如履薄冰”

做高并发系统,最可怕的感觉是什么? 不是技术难,不是架构复杂,是“如履薄冰”。 你以为高并发场景下只要把Redis缓存做好,把MQ削峰做好,系统就稳了? 错。 往往那个最不起眼、最基础的组件,会成为压垮骆驼的最后一根稻草。
比如,分布式ID生成。 在很多人的概念里,这就是一个非常简单,根本不用去思考的事儿。UUID、序列自增、雪花算法,一百种方案随便选。如果你这么想,在百万并发的冲击下,你的系统已经死了一半了。 数据库写锁冲突、分库分表数据倾斜、主键回拨导致数据覆盖... 每一个坑,都能让你的年终奖瞬间归零。
这次,我就带大家看看在百万QPS的极限场景下,“ID生成”这个看似简单的功能,要怎么一步步优化到极致。这中间的每一步演进,都是很多程序员用无数心血换来的经验。
这期内容非常硬核,保证是你在其他地方很难看到的。记得点赞、收藏。开始这段高并发之旅。
第二部分:两大流派的本质与瓶颈
在分布式ID的江湖里,主要分两大流派。我们先得把它们的底裤看穿,才能谈优化。

第一派:号段模式(Segment)。 简单说,就是“批发”。 应用服务觉得每次找数据库要一个号太慢,就一次性申请1000个。 数据库里只需要一张极简的表:
biz_tag(业务名):区分你是订单还是用户。max_id(当前最大值):记录发到哪了。step(步长):一次发多少。 本质:用数据库的强一致性,换取应用的批量性能。
第二派:雪花算法(Snowflake)。 简单说,就是“自产自销”。 完全不依赖数据库。在本地把时间戳、机器ID、序列号拼成一个64位的长整数。 优点:极致的快,理论上性能无上限。
但是! 在极致的高并发下,这两种方案如果不做魔改,全是漏洞。 Segment模式会有网络毛刺,Snowflake模式会有机器ID分配难题。 接下来,我们一个一个解决。
第三部分:Segment的极致——打破双Buffer的极限

先看号段模式。 最经典的问题:TP999毛刺。 当你手里这1000个号用到最后一个时,必须停下来去数据库拿新的。 这中间的几十毫秒网络IO,会让后续的请求全部阻塞。
常规解法:双Buffer(双缓存)。 这也就是美团Leaf的思路。 准备两个桶,每个桶存一个号段。第一个桶用到10%的时候,异步线程赶紧去把第二个桶装满。 切换时丝滑无感。 这已经是很多大厂的标准答案了。
但这还不够。 如果你遇到超高并发的秒杀场景,瞬间流量可能把两个桶都一起打穿。这怎么办? 或者数据库稍微抖动了一下,异步线程没来得及拉回来怎么办?
能不能不要限制只有两个桶,而是设计一个动态伸缩的链表,把多个桶连起来呢?当然可以。还可以设计算法根据你当前的消费速度,剩余的桶数量,智能决定预加载多少个号段。 你吃得越快,我预加载的链条就越长。 甚至在内存里囤积未来几分钟的ID量。 这样,哪怕数据库宕机了,靠内存里的存货,系统还能硬抗好几分钟。 这就把“高可用”做到了极致。这就是终极解法:Segment Chain(链式模式)。
第三部分:Snowflake的三座大山
再来看雪花算法。 实际上,这个简单高效的算法,在高并发场景下,也是问题不断。

一是时钟回拨问题。就是系统时间戳回到了某一个之前的时间点。这就会产生ID重复的问题。产生时钟回拨问题的原因很多,例如系统默认通过NTP进行时钟同步,那就有可能因为网络延迟等原因产生小范围时钟回退。这个问题比较好解决。保存上一次生成ID的时间,与当前时间进行比对。如果发现了时钟回拨,可以直接抛异常或者休眠等待。
另一个是分片倾斜问题。雪花算法最后一个序列位默认保存当前时间戳下重复出现的ID序号。如果在低并发场景下,几乎没有重复ID。那么这个序号就可能都是0。这会导致在典型的取模分片的场景下,大部分数据都会被集中分配到同一个分片下,造成严重的数据倾斜。这会严重影响数据分片的性能。通常解法是要做序列号震荡。让序列号不要在每个时间戳都归0,在一个比较小的范围内循环递增。
这两个问题虽然都比较隐蔽,但早就有了比较固定的解决方法。像ShardingSphere的雪花算法实现中都已经解决了这两个问题。
第四部分:攻克机器ID分配的难题

但接下来这个真正的大BOSS就没那么好解决了。这就是机器ID(Worker ID)怎么分配? 雪花算法中间有10位设计的是机器ID,原本是要给不同机器设置不同的ID,防止分布式场景下,不同机器在同一时刻产生重复的ID。
这个机器ID,通常都只能由应用自行指定,例如ShardingSphere默认的雪花算法实现中,这个机器ID就需要应用自行配置。但本身大规模分布式场景下,要单独给每个应用实例设置ID就比较困难。再加上K8s容器化环境下,Pod随时重启,IP随时变。 我们怎么保证每个实例拿到的机器ID是唯一的?
你当然会想到用类似于号段模式的方式,自动给集群中每个应用实例分配连续的机器ID。这样才能真正防止分布式场景下的ID重复问题。
但是在高并发场景下,雪花ID的生成速度是非常快的。而这个机器ID不管在多大的集群中,也赶不上雪花ID的生成需求。另外,机器ID就只有这么几个比特位,每一个数字都非常珍贵。如何让每一个机器ID都物尽其用呢?这就需要一套严密的机制了。
这里我们可以设计一套严密的“三步走”分配机制。 这套机制的核心思想,是利用数据库(JDBC)作为协调者,管理无状态的Snowflake节点。
第一步:本地认领(Local & Stable)。 应用启动时,先检查本地缓存(文件)。 如果我上次用过“88号”,而且这个号被标记为“Stable(稳定)”的。 那我就优先尝试去数据库里续签这个88号。 这就像老员工复职,工号照旧。
第二步:捡漏回收(Revert)。 如果是新实例,或者老号续签失败。 我们就去数据库里找那些“尸体”。 也就是那些很久没有更新时间戳的、或者显式标记为“已下线”的机器ID。 比如“66号”机器上次心跳是三天前,那我就把它捡漏过来。 这叫资源复用,防止机器ID被耗尽。
第三步:扩容申请(Remote Distribute)。 如果既没有老号,也没有死号。 那就只能扩容了。 执行MAX(machine_id) + 1,申请一个新的号码。 比如现在最大是100号,那我就领101号,并把我的信息插入数据库。
通过这三步:认领 -> 捡漏 -> 扩容。 我们实现了一个自动化、可回收、高可用的机器ID分配体系。 就算集群需要频繁的扩缩容,也可以保证机器ID能够及时正确的分配出去。
把雪花算法的每一个位置都用到极致,这才是工业级的雪花算法。
第五部分:存储抽象——万物皆可为ID源

架构设计到这一步,我们还需要最后一块拼图:存储适配。 刚才我们讲的Segment模式和机器ID分配,都是基于数据库的。但数据库本身性能就不高,能不能把这些分布式ID需要持久化的数据转移出去呢?能不能把同样的逻辑转移到Redis,Zookeeper或者是MongoDB呢?
真正的高手,会设计一层“ID Provider”抽象层。 把Chain模式/Snowflake算法的“发号逻辑”和“持久化逻辑”彻底解耦。
- 用Redis的
INCR做原子递增; - 用Zookeeper的顺序节点做分配;
- 用MongoDB的
ObjectId或统计功能。
对于上层业务来说,我只要ID,并不关系这些数据存哪里。而且分布式ID需要的数据本身也并不大,项目里用到那个组件就切换到这个组件。这种可插拔的设计,才是一个中间件能活得长久的根本。
第六部分:集大成者——CosID与ShardingSphere

最后,把我们刚才讲的所有这些: 无限预加载的Segment Chain、 防倾斜的序列号震荡、 三步走的机器ID自动化分配、 全场景存储适配...
把这些零散的设计整合在一起,就是我今天要介绍的终极方案——CosID。
它旨在提供通用、灵活、高性能的分布式 ID 生成器。它是Apache ShardingSphere内置集成的分布式ID生成框架。官网资料显示,在基准测试中,它的Segment模式单机TPS每秒高达1.2亿,是UUID的三倍。而SnowflakeID单机TPS也高达409w/s。它把分布式ID生成这个问题,在高性能、高可用、容器原生这些领域,推到了极致。它也证明了,我们刚才推导的那一套复杂的架构设计,不仅仅是理论,而是已经经过了工业级验证的最佳实践。
第七部分:人生的高并发

回到开篇的问题:做高并发系统,最可怕的感觉是什么? 是“如履薄冰”。
设计一个ID生成器,看似简单。 但为了那0.01%的极端情况,程序员们引入了双Buffer,引入了Chain,引入了复杂的机器号回收机制。 最终付出的所有复杂性,都是为了呈现给CRUD们最简单的稳定性。
做技术是这样,做人何尝不是如此? 在人生的“高并发”时刻, 多准备几个技能储备作为Buffer, 管理好自己的ID这个核心竞争力, 哪怕遇到时钟回拨这样的逆境,也能换个身份,继续向前。
我是楼兰,关注我,IT路上我们一起进步。接下来,评论区见。