1. 原理

img

分布式场景里,为了保证主键的唯一性,我们可以将 key 设置为时间戳,value 为 12 位序列号,应用调用 Redis String 类型的 incr 自增命令 ,自增命令是原子操作,因为 41 位时间戳 + 12位序列号的组合不可能重复,所以理论来讲,Redis + 雪花算法生成分布式 ID不会重复。

2. 流程

下图展示了订单 ID 使用雪花算法的生成过程,生成的编号会携带企业用户 ID 信息。

img

1、查看并发队列是否有可用的序列号

img

使用并发队列 ConcurrentLinkedQueue ,是为了减少对 Redis 的网络 IO ,同时加一层缓冲,相对提升系统的可用性。

2、调用 redis incr 自增命令,每次自增一个步长,默认是15

img

Redis ID 生成器第一次执行后, 当前时间戳对应的序列号如下图:

img

3、将每一个步长的序号加入到并发队列

img

img

4、使用雪花算法组装编号

img

3. 时钟回拨

雪花算法对时间非常敏感,当发生时间回拨时,有一定几率生成重复的 ID。

时钟回拨是指:时间被调整回到了之前的时间,由于雪花算法重度依赖机器的当前时间,所以一旦发生时间回拨,将有可能导致生成的 ID 可能与此前已经生成的某个 ID 重复。

引起时钟回拨一般有三个原因:

3.1 网络时间自动校准

导致时钟回拨是机器本地时钟因为各种原因发生不准导致,其实网络中有NTP(NTP为Network Time Protocol的缩写,即网络时间协议)服务来提供时间校准。

通过时间校准让客户端和服务器之间进行时钟同步,提供高精准度的时间校正,NTP服务器从权威时钟源(例如原子钟、GPS)接收精确的协调世界时UTC,因为NTP Pool是绝大多数主流 Linux 发行版和许多网络设备的默认“时间服务器”。

img

3.2 人工设置时间

假如服务器的时间不相同,运维人员或者开发者直接修改服务器的时间。

3.3 闰秒

闰秒,是指为保持协调世界时接近于世界时时刻,由国际计量局统一规定在年底或年中(也可能在季末)对协调世界时增加或减少1秒的调整。

由于地球自转的不均匀性和长期变慢性(主要由潮汐摩擦引起的),会使世界时(民用时)和原子时之间相差超过到±0.9秒时,就把协调世界时向前拨1秒(负闰秒,最后一分钟为59秒)或向后拨1秒(正闰秒,最后一分钟为61秒);

当遇到时钟回拨的问题,RedisIdGeneratorService 能否避免这个问题呢 ?我们来模拟一下。

1、时间戳对应的 Redis ID key 存在

假设时钟回拨到时间戳 1710593308053 ,该时间戳对应的 Redis ID key 没有过期。

img

图中,通过 Redis 的 incr 命令自增了两个步长,此时依然有非常多的序列号可供使用,理论来讲,时钟回拨并不会有太多影响。

但假如应用过多,同一时间戳 incr 命令自增步长次数过多,导致没有可用的序列号了,此时生成 ID 会报异常,当然同一时间戳触发这种场景的概率相对较低。

img

2、时间戳对应的 Redis ID key 不存在

时钟回拨到时间戳 1710593308053 ,该时间戳对应的 Redis ID key 并不存在(可能是因为过期了),那么可使用的序列号从 1 开始了,可能[1710593308053,1] 这个组合已经被使用过了,那么有大的概率会生成一个重复的 ID 。

综上,我们可以尽量延迟时间戳对应的 Redis ID key 的过期时间尽量规避时钟回拨带来的风险。

4. 思维升级

写到了这里,我们思考下 RedisIdGeneratorService 还有哪些优化的空间 , 笔者想到了两点:

1、细粒度的分层 Redis ID Key

我们可以将每张表映射一个 Redis ID Key ,拼接格式如下图:

img

2、参考美团Leaf 自研一个服务端的 ID 生成器

我们在时钟回拨这一节提到:

假如应用过多,同一时间戳 incr 命令自增步长次数过多,导致没有可用的序列号了,此时生成 ID 会报异常,当然同一时间戳触发这种场景的概率相对较低。

同时,RedisIdGeneratorService 是客户端的模式,假如多团队需要接入,随着接入应用的增加,那么同一时间戳触发这种场景的概率就会越来越高。

因此,我们可以参考美团Leaf 自研一个服务端的 ID 生成器,就能够规避上面的风险,同时对接大规模的应用。


本站由 卡卡龙 使用 Stellar 1.29.1主题创建

本站访问量 次. 本文阅读量 次.