让你设计一个分布式 ID 发号器,怎么设计?
让你设计一个分布式 ID 发号器,怎么设计?
一般在分库分表场景,就会有分布式 ID 的需求,因为需要有一个唯一标识来标记一个订单或者其他类似的数据等。
全局唯一 ID 有很多种实现,例如 UUID ,优势就是本地生成,很简单。但它是无序的,如果需要将唯一 ID 作为主键,则 UUID 不合适,首先是太长了,其次无序插入会导致数据页频繁分裂,性能不好。
在回答这个面试题的时候可以先提下 UUID,简单说下优缺点(UUID 的缺点其实侧面能体现出你对 MySQL 的了解,这都是小心机呀)。
常见的分布式 ID 实现有两种:
1)雪花算法
2)基于数据库
雪花算法
雪花算法用有 64bit,实际就用到了 63bit,最前面有一个 1bit 没用,图中就没画了。
其中 41bit 是时间戳,10bit 是机器 ID(可以表示 1024 台机器。如果有机房的划分,可以把 5bit 分给机房号,剩下 5bit 分给机器)。最后 12bit 是自增序列号,12bit 可以表示 2 的 12 次方个 ID。

可以看到,以时间戳打头可以保证趋势是有序性,其中分配了机器号避免了多机器之间重复 ID 的情况,后面的自增序号使得理论上每台机器每毫秒都可以产生 2 的 12 次方个 ID。
简述优点:
1)分布式 ID 从整体来看有序的。
2)简单、灵活配置,无序依赖外部三方组件。
缺点:
1)依赖时钟,如果发生时钟回拨,可能会导致重复 ID。
常见的 hutool 就有提供了雪花算法工具类。
面试官可能会问雪花算法的机器 ID 如何动态配置,因为现在机器都是动态扩容部署,机器数都是不固定的,如果机器 ID 没配置好,容易导致冲突。
可以借助 Redis 或者 zookeeper 来实现机器 ID 的分配。
redis 的执行是单线程的,机器启动时候调用 redis incr 即可得到自增 id ,可将这个 id 作为机器 ID。
zookeeper 的使用也很简单,机器启动时候可以利用 zookeeper 持久顺序节点特性,将注册成功的顺序号作为机器 ID。
基于数据库
对 MySQL 来说,直接利用自增 id 即可实现。
1 | REPLACE INTO table(bizTag) VALUES ('order'); |
将 bizTag 设为唯一索引,可以填写业务值(也可以不同业务多张表),REPLACE INTO 执行后自增 ID 会 + 1,通过 last_insert_id 即可获得自增的 ID 。
优点:简单、利用数据库就能实现,且 ID 有序。
缺点:性能不足。
可以利用 auto_increment_increment 和 auto_increment_offset 实现横向扩展。
比如现在有两台数据库,auto_increment_increment 都设置为 2,即步长是 2。第一台数据库表 auto_increment_offset 设置为 1,第二台数据库表 auto_increment_offset 设置为 2。
这样一来,第一台的 ID 增长值就是 1、3、5、7、9….,第二台的 ID 增加值就是 2、4、6、8、10….
这样也能保证全局唯一性,多加几台机器弥补性能问题,只要指定好每个表的步长和初始值即可。
不过单调递增特性没了,且加机器的成本不低,动态扩容很不方便。
这里我们可以思考下,每次操作数据库就拿一个 ID ,我们如果一次性拿 1000 个,那不就大大减少操作数据库的次数了吗?性能不就上去了吗?
重新设计下表,主要字段如下:
bizTag: 业务标识
maxId: 目前已经分配的最大 ID
step: 步长,可以设置为 1000 那么每次就是拿 1000 ,设置成 1w 就是拿 1w 个
每次来获取 ID 的 SQL 如下:
1 | UPDATE table SET maxId = max_id + step WHERE bizTag = xxx |
这其实就是批量思想,大大提升了 ID 获取的性能。
到这里可能面试官会追问,假设业务并发量很高,此时业务方一批 ID 刚好用完后,来获取下一批 ID ,因为当前数据库压力大,很可能就会产生性能抖动,即卡了一下才拿到 ID,从监控上看就是产生毛刺。
这样怎么处理?
其实这就是考察你是否有预处理思想,如果你看过很多开源组件就会发现预处理的场景很多,例如 RocketMQ commitlog 文件的分配就是预处理,即当前 commitlog 文件用完之前,就会有后台线程预先创建后面要用的文件,就是为了防止创建的那一刻的性能抖动。
同理,这个场景我们也可以使用预处理思想。
发号器服务可以本地缓存两个 buffer,业务方请求 ID 每次从其中一个 buffer 里取,如果这个 buffer 发现 ID 已经用了 20 %(或者另外的数量),则可以起一个后台线程,调用上面的 SQL 语句,先把下一批的 ID 放置到另一个 buffer 中。
当前面那个 buffer ID 都用完了,则使用另一个 buffer 取号,如此循环使用即可,这样就能避免毛刺问题。
将 ID 放在本地缓存性能好,即使服务重启了也没事,无法就是中间空了一点点 ID 罢了,整体还是有序的。
一些关键点
雪花算法面试官可能会追问如果时钟回拨了怎么办,理论上可以存储上一次发号的时间,如果当前发号的时间小于之前的发号时间,则说明时钟回拨,此时拒绝发号,可以报警或者重试(重试几次时间可能就回来了)。
数据库方案如果面试官说数据库故障怎么办?理论上数据库都故障了其实很多业务都无法执行下去。。这属于不可抗拒因素,但是我们有本地缓存,可以将 step 设置大一些,例如 qps 最高时候的 600 倍,这样至少有 10分钟的缓冲时间,可能数据库就恢复了。其次数据库可以做主从,但是主从会有复制延迟,导致 maxId 重复,这里可以采取和雪花算法对抗时钟回拨一样的套路,服务记录上次 maxId,如果发现 maxId 变小了,则再执行一次 update。
还有一点,数据库实现的 ID 是完全连续的,如果用在订单场景,竞对早上下一单,晚上下一单,两单一减就知道你今天卖了多少单,所以很不安全,因此这种 ID 不适合用在这种场景(再比如文章的 id 容易被爬虫简单按序一次性爬完)。
这一套如果跟面试官说下来,我相信这场面试你肯定稳了,整体设计没问题、还有很多性能方面的考虑并且还想到了安全性问题。