【算法】雪花算法

相比于 UUID 或者自增主键,雪花算法可以既保证在分布式环境中的唯一性的同时又能趋势递增以增强写入效率。但也有时钟回拨可能导致重复的缺陷。

结构

总长度为 8 字节 64 位,刚好为 Java Long 或者 MySQL BIGINT 类型的长度。

缺省(1) 时间戳(41) 数据中心标识(5) 数据节点标识(5) 序列号(12)
0 0 00000000 00000000 00000000 00000000 00000000 00000 00000 0000 00000000

含义

  1. 最高位缺省(1)总是为 0 保证生成的是正整数。
  2. 时间戳(41)精确到毫秒。
  3. 在分布式环境下可以分为多个数据中心(5),每个数据中心可以分为多个数据节点(5)。
  4. 序列号(12)保证在相同毫秒时的顺序递增以保证整体的唯一性。

拓展

其实不难发现雪花算法的本质是通过时间戳(41)、固定标识(10)和序列号(12)三个部分来保证唯一性。在分布式环境下,部署应用时应当保证一个固定标识对应一个数据库,这样才能保证应用整体中不同数据库中的主键总是顺序递增的。在单机部署的情况下,可以将固定标识设置为机器 MAC 或者 IP。

算法

核心

  1. 计算时间戳
//  总长度 64
long totalBitsLength = 64L;
//  最高位长度 1
long topBitsLength = 1L;
//  时间戳长度 41,能容纳的时间范围:
//  1970-01-01 00:00:00
//  23                      41
//  00000000000000000000000 00000000000000000000000000000000000000000
//  2039-09-07 23:47:35
//  23                      41
//  00000000000000000000000 11111111111111111111111111111111111111111
long timeBitsLength = 41L;
//  初始掩码
//  23                      41
//  11111111111111111111111 11111111111111111111111111111111111111111
long defaultMask = -1L;
//  向左移 41 后的掩码
//  23                      41
//  11111111111111111111111 00000000000000000000000000000000000000000
long leftMoveMask = defaultMask << timeBitsLength;
//  取反后的掩码
//  23                      41
//  00000000000000000000000 11111111111111111111111111111111111111111
long reverseMask = ~leftMoveMask;
//  时间戳
//  23                      41
//  00000000000000000000000 11000111000111011011110100110101011100111
long time = 1710394862311L;
//  掩码后的时间戳
//  23                      41
//  00000000000000000000000 11000111000111011011110100110101011100111
long maskTime = time & reverseMask;
//  时间戳偏移量(22)=总长度(64)-最高位长度(1)-时间戳长度(41)
long timeOffset = totalBitsLength - topBitsLength - timeBitsLength;
//  时间戳部分
//  1   41                                          22
//  0   11000111000111011011110100110101011100111   0000000000000000000000
long timePart = maskTime << timeOffset;
  1. 计算固定标识(MAC)
//  总长度 64
long totalBitsLength = 64L;
//  最高位长度 1
long topBitsLength = 1L;
//  时间戳长度 41
long timeBitsLength = 41L;
//  MAC 长度 10
long macBitsLength = 10L;
//  初始掩码
//  54                                                      10
//  111111111111111111111111111111111111111111111111111111  1111111111
long defaultMask = -1L;
//  向左移 10 后的掩码
//  54                                                      10
//  111111111111111111111111111111111111111111111111111111  0000000000
long leftMoveMask = defaultMask << macBitsLength;
//  取反后的掩码
//  54                                                      10
//  000000000000000000000000000000000000000000000000000000  1111111111
long reverseMask = ~leftMoveMask;
//  MAC
//  54                                                      10
//  000000000000000000000000000000000011011000111010010010  1011110011
long mac = 755L;
//  掩码后的 MAC
//  54                                                      10
//  000000000000000000000000000000000000000000000000000000  1011110011
long maskMac = mac & reverseMask;
//  MAC 偏移量(12)=总长度(64)-最高位长度(1)-时间戳长度(41)-MAC 长度(10)
long macOffset = totalBitsLength - topBitsLength - timeBitsLength - macBitsLength;
//  MAC 部分
//  1   41                                          10          12
//  0   00000000000000000000000000000000000000000   1011110011  000000000000
long macPart = maskMac << macOffset;
  1. 计算序列号
//  总长度 64
long totalBitsLength = 64L;
//  最高位长度 1
long topBitsLength = 1L;
//  时间戳长度 41
long timeBitsLength = 41L;
//  MAC 长度 10
long macBitsLength = 10L;
//  序列号长度 10
long sequenceBitsLength = 12L;
//  初始掩码
//  52                                                      12
//  1111111111111111111111111111111111111111111111111111    111111111111
long defaultMask = -1L;
//  向左移 12 后的掩码
//  52                                                      12
//  1111111111111111111111111111111111111111111111111111    000000000000
long leftMoveMask = defaultMask << sequenceBitsLength;
//  取反后的掩码
//  52                                                      12
//  0000000000000000000000000000000000000000000000000000    111111111111
long reverseMask = ~leftMoveMask;
//  序列号
//  52                                                      12
//  0000000000000000000000000000000000000000000000000000    000000000000
long sequence = 0L;
//  掩码后的序列号
//  52                                                      12
//  0000000000000000000000000000000000000000000000000000    000000000000
long maskSequence = sequence & reverseMask;
//  序列号偏移量(0)=总长度(64)-最高位长度(1)-时间戳长度(41)-MAC 长(10)-序列号长度(12)
long sequenceOffset = totalBitsLength - topBitsLength - timeBitsLength -macBitsLength - sequenceBitsLength;
//  序列号部分
//  1   41                                          10          12
//  0   00000000000000000000000000000000000000000   0000000000  000000000000
long sequencePart = maskSequence << sequenceOffset;
  1. 合并各部分
//  时间戳部分
//  1   41                                          10          12
//  0   11000111000111011011110100110101011100111   0000000000  000000000000
long timePart = 7173916012570476544L
//  MAC 部分
//  1   41                                          10          12
//  0   00000000000000000000000000000000000000000   1011110011  000000000000
long macPart = 3092480L;
//  序列号部分
//  1   41                                          10          12
//  0   00000000000000000000000000000000000000000   0000000000  000000000000
long sequencePart = 0L;
//  主键
//  1   41                                          10          12
//  0   11000111000111011011110100110101011100111   1011110011  000000000000
long id = timePart | macPart | sequencePart;

以上就是核心算法,当然还需要切合业务来优化,比如由于时间戳按照目前的设计最多只到 2039 年就会溢出,而且序列号也没有递增。

优化

  1. 时间戳溢出

可以通过设定一个基准时间戳,然后使用当前时间戳减去基准时间戳得到一个相对时间戳,这个值显而易见很小,41 的长度能容得下。

  1. 序列号没有递增

序列号的生成依赖于时间戳的变化,在生成相对时间戳的时候判断有没有递增,如果有递增就归零,没有就顺序递增。

在分布式网络并发模型里,主键的生成需要加锁保证时间或序列号的顺序递增。

  1. 时钟回拨

当服务器的时间发生回退时,有可能导致生成时间戳重复的情况,可以通过保存上一次时间戳进行对比,选择抛出异常或者从固定标识中划出一部分当作时间序列,原理和生成序列号一样。

26 Likes

好好学习,天天向上!

又学到了,每天进步一点点 :saluting_face:

好好好,多发点,爱看!

又学到了。

又学到了

你这个实现应该没有考虑时间回滚的情况

1 Like

看不懂

23 Likes

学习,看coze-discord-proxy项目有用下面的库产生NextID,但只考虑时间和IP

1 Like

可以啊!!!

在线课堂

4 Likes

期待大佬分享更多算法 :pray:

学习了

学习了

每日一学

谢谢,已补充。

学习了

cy

佩服研究算法的佬

2 Likes

前阵子还踩过坑,使用的mybatis plus的内置雪花算法,deployment部署多pod,容易出现不同pod的worker-id 和 datacenter-id重复,导致请求同一毫秒打到不同pod时,出现自增id重复的问题