雪花算法
格式(64bit)
- 1bit 不用:因为二进制中最高位是符号位,1 表示负数,0 表示正数,生成的 id 一般都是用整数,所以最高位固定为 0
- 41bit 时间戳:这里采用的就是当前系统的具体时间,单位为毫秒
- 10bit 工作机器 ID(workerId):每台机器分配一个 id,这样可以标示不同的机器,但是上限为 1024,标示一个集群某个业务最多部署的机器个数上限
- 12bit 序列号(自增域):表示在某一毫秒下,这个自增域最大可以分配的 bit 个数,在当前这种配置下,每一毫秒可以分配 2^12 = 4096 个数据
特点
- 全局唯一性:雪花算法可以保证集群系统的 ID 全局唯一
- 趋势递增:由于强依赖时间戳,所以整体趋势会随着时间递增
- 单调递增(×):不满足单调递增,在不考虑时间回拨的情况下,虽然在单机中可以保持单调递增,但在分布式集群中无法做到单调递增,只能保证总体趋势递增
- 信息安全指的是 ID 生成不规则,无法猜测下一个
时间回拨
简单说就是时间被调整回到了之前的时间,由于雪花算法重度依赖机器的当前时间,所以一旦发生时间回拨,将有可能导致生成的 ID 可能与此前已经生成的某个 ID 重复(前提是刚好在同一毫秒生成 ID 时序列号也刚好一致)。
看上去不会是一个很严重的问题,毕竟美团 Leaf 解决方案也无非是小于 5ms 就等一会,大于就直接报错。
基于时钟序列解决时间回拨的方案
如上图,将原本 10 位的机器码拆分成 3 位时钟序列及 7 位机器码。发生时间回拨的时候,时间已经发生了变化,那么这时将时钟序列新增 1 位,重新定义整个雪花 Id。为了避免实例重启引起时间序列丢失,因此时钟序列最好通过 DB 存储起来。
这当然会导致分布式实例规模由 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 10 ( 1024 ) 2^{10}(1024) </math>210(1024) 降至 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 7 ( 128 ) 2^7(128) </math>27(128),同时每个分布式实例支持最多 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 3 ( 8 ) 2^3(8) </math>23(8) 次时间回拨。
UidGenerator
UidGenerator 是 Java 实现的,基于 Snowflake 算法的唯一 ID 生成器。在实现上,UidGenerator 通过借用未来时间来解决 sequence 天然存在的并发限制;采用 RingBuffer 来缓存已生成的 UID, 并行化 UID 的生产和消费,同时对 CacheLine 补齐,避免了由 RingBuffer 带来的硬件级「伪共享」问题。
一言以蔽之就是整了两个预生成队列,然后一个线程不依赖系统时间、专门管理 delta seconds 这个递增变量往队列里生产 id。
RingBuffer 是无锁队列的典型实现(为什么会有两个 Ringbuffer 呢,笔者也不知道,按理说无锁队列有一个 Ringbuffer 就够了,也许两个能快一点?),而伪共享也是单纯的为了解决 id 生成效率问题。
所以 UidGenerator 本质上就是自己定义了一套随时间趋势递增的 id 自增规则然后预生成,不要 RingBuffer 和解决伪共享,这套方案无非也就慢点。
UidGenerator 行为可以概括如下:
第一种情况:实例关闭,时间回拨,重启实例。
这种情况下,实例每次重启都分配一个新的 workId,保证了重启后生成的 id 与之前产生的 id 不会重。
第二种情况:实例一直运行,时间回拨。
DefautlUidGenerator 使用了 System.currentTimeMillis() 获取时间与上一次时间比较,可能会有 currentTime<lastTime 情况发生,抛错错误。
而 CachedUidGenerator 使用 AtomicLong 的 incrementAndGet() 来获取下一次时间,脱离了时间,所以可以正常使用。
美团 Leaf
美团 Leaf 也是相当老(2017 年的技术博客)的解决方案了,实际上压根没解决时钟回拨的问题,估计主要是在早年提供了一个企业级实现,说我们自己这么用反正没问题,时钟回拨发生了就发生了,还不是解决过来了。流程如下:
从上图可以看到 workerID 是跟 ip:port 绑定的,这个也是企业级实现嘛,笔者也有想过通过一个 hash 函数结合服务的 mac 地址或者这样那样的唯一标识符生成 workerID,但是美团直接绑 ip:port 也是相当简单粗暴了。
Leaf 对于时钟回拨现象只是做了一些预防措施,比如启动的时候检查,先跟 zookeeper 确定本机时间再跟所有服务器均值确定本机时间,不符合校验就直接启动失败。这解决时钟回拨了吗,当然没有,启动失败报警完全可以看作另一种延时启动的方案。反正主旨就是一个等字。
总结
雪花算法强依赖本机时间,这带来的优点就是方便扩展,毕竟任何机器都不需要额外依赖都肯定有获取本机时间的函数。但这也带来了时钟回拨问题,解决这个问题可以舍弃一点 workerID 的位数(基于时钟序列解决时间回拨的方案)也可以舍弃高可用性直接等(美团 Leaf)。
注意到基于时钟序列解决时间回拨的方案无论如何也要加入 DB 持久化时钟序列,这引入了额外依赖,不是很妙。美团 Leaf 更是在 zookeeper 上面多存了一个周期性的本机时间,不过考虑到美团 Leaf 本身 workerID 也是存在 zookeeper 上的,也只能说是个妥协的办法。
百度 UidGenerator 则是直接使用 Atomiclong 变量管理递增的时间戳,这有什么问题?最大的问题当然是性能肯定不如获取本机时间戳,不然雪花算法干嘛不自己管理,谁都想得到这个时间戳只要跟现实时间无关当然就不存在时钟回拨问题。因此百度 UidGenerator 整了无锁队列以及优化伪共享,都是为了增加运行效率。即便如此,这一套东西还是要加入一个 DB 持久化自己管理的时间戳嘛。
结合上文,雪花算法的可拓展性高完全在于获取本机时间这个函数,而时钟回拨与可拓展性是一体两面的,无论如何也无法在不增加额外依赖下解决。