共计 1409 个字符,预计需要花费 4 分钟才能阅读完成。
1、为什么要实现发号器
很多地方我们都需要一个全局唯一的编号,也就是 uuid。举一个常见的场景,电商系统产生订单的时候,需要有一个对应的订单编号。在 composer 上我们也可以看到有很多可以产生 uuid 的优秀组件。那么,为什么我们还要自己实现发号器,来产生 uuid 呢?想了一下,主要有两个原因吧:
1、我希望 uuid 是可反解的,通过反解 uuid 可以得出和我业务相关的数据。而我看到的 composer 关于 uuid 的相关组件,生成的都是一串指定格式的字符串,我很难将它同具体的业务关联起来。
2、我希望通过 uuid 是可以随着并放量进行调整的。比如说原有支持 1 秒钟可以产生 1000 个 uuid,但随着业务规模增长,我希望变成可以支持 1 秒钟产生一万个。而且,最好改下配置就可以了。
出于以上两个原因,我们需要自己的发号器来产生 uuid。那么,下一个问题是,我们应该如何实现发号器,实现发号器的原理又是什么呢?
2、snowFlake 算法
关于发号器的实现原理,可能大家都听过鼎鼎大名的 snowflake 算法 — 雪花算法,Twitter 的分布式自增 Id 算法。国内的新浪微博也有自己实现的发号器算法,具体实现细节虽有不同,但是原理相通,明白其中一个即可。这里我们主要介绍 snowflake。
关于 snowflaw 的介绍,已经有很多文章进行介绍,而且写的也很不错,我没有必要在重写一遍,拿来粘贴即可,出于对作者的尊重,我会将原文链接添加到参考链接中。
推特的分布式自增 ID 算法,使用 long(8 × 8 = 64 byte)来保存 uuid。其中 1bit 留给固定符号位 0,41bit 留给毫秒时间戳,10bit 给 MachineID,也就是机器要预先配置,剩下 12 位留 Sequence(可支持 1 毫秒内 4096 个请求)。
也许有的人会问如果超过了 1 毫秒 4096 个请求怎么办?一般的做法是,让它等上 1 毫秒,促使 41bit 的时间戳变化。
这里我们将 MachineId 进行了拆分,5byte 留给机器(最多可以支持 32 机器),5byte 留给了业务号(最多可支持 32 种业务)
这里的时间戳保存的是当前时间与固定过去时间得一个差值,不是当前时间。这样的好处是能使用更长时间,而且不受年份限制,只取决于从什么时候开始用的,2^41 / 1000360024*365=69 年。
如果保存的是当前时间戳,最多只能使用到 2039 年。2^41=2199023255552=2039/9/7 23:47:35
理论上单机速度:2^12*1000 = 4 096 000/s
3、如何保证在单位时间内持续递增
通过对 snowflake 的初步了解,发现,其实发号器也是建立在时间戳基础之上的,因为时间是天然的唯一元素。但是,如何在单位时间内,比如说一秒钟或者一毫秒之内,保证 Sequence 持续递增才是发号器实现的关键。
这里我们实现的方式比较简单,直接使用 redis 的 incr 进行计数,对应的 key 就是毫秒时间戳。出于 redis 内存回收的考虑,我们需要将每一个 key 设置过期时间。如果 key 是秒级别的时间戳,那么过期时间就是 1 秒;如果 key 毫秒级别的时间戳,那么过期时间就是 1 毫秒。
与此同时,为了保证执行 incr,expire(pexpire)具有原子性,我们使用 lua 来进行实现。
好了,实现的思路大致如此。由于能力和水平有限,难免会有纰漏,希望及时指出。
4、参考文章
分布式 ID 生成器 PHP+Swoole 实现 (上) – 实现原理