背景
雪花算法是分布式应用中应用比较多的 ID 生成算法,某项目中使用该算法生成ID,近期被反馈算法生成的 ID 存在重复的情况,排了一天,终于找到问题根源了。
本文将总结这个 Bug ,顺便温故一下雪花算法及改造雪花算法支持更大的工作中心和机器码的注意事项。
雪花算法概览
雪花算法 (SnowFlake ),是 Twitter 开源的分布式 id 生成算法。
为什么叫雪花算法呢?据相关研究表示,一般的雪花大约由10的19次方个水分子组成。在雪花形成过程中,会形成不同的结构分支,所以说大自然中不存在两片完全一样的雪花,每一片雪花都拥有自己独特的形状。
雪花算法,顾名思义,算法生成的ID如雪花一般独一无二。
其核心思想是:使用一个 64 bit 的 long 型的数字作为全局唯一 id。在分布式系统中的应用十分广泛,且ID 引入了时间戳,基本上保持自增的。
雪花算法的 64 位数据组成结构如下《原图来源》:
中间 10位=数据中心5位 + 机器码5位,最后12位存同一毫秒内的ID序号。
最后这三部分能表示的最大数值为:
数据中心个数:0-31
机器码个数:0-31
序号数:0-4095
算法可以保证一个工作中心的一台机器上,在同一毫秒内,生成一个唯一的 id。
同一毫秒内,由最后 12 位的序号累加生成不同 ID。
算法改造过程
项目对该算法做了改造,希望能配置更大的数据中心和机器码,所以把后三部分的长度调整为:
- 数据中心:9位,范围 0-511。
- 机器码:7位,范围 0-127。
- 相同毫秒内的序列号:10,范围 0-1024。
调整后,三部分总长就由算法推荐的 22 位调整为 26位,前面的时间戳部分长度就变成了 37 位。这导致了在不同毫秒、且相差几毫秒时生成的 ID 会重复。
打印某次测试时 ID 及当时的时间信息如下:
Generate result is=5695462951227492352,timestamp=1720565011504,seq=0
Generate result is=5695462951227492352,timestamp=1720565011505,seq=0
Generate result is=5695462951227492352,timestamp=1720565011508,seq=0
Generate result is=5695462951227492352,timestamp=1720565011509,seq=0
上面四次 ID 的时间戳相差1-5毫秒,但是按改造的算法却生成了4个相同的 ID ,判断根源在时间戳的存储长度不是算法推荐的 41 位导致的。
修正算法,保持时间戳长度为 41 位,其他三部分总长仍然是 22 位,生成的 ID 就不存在重复问题了,只是 数据中心+工作机器码占16 位后,序列号只有 6位,每毫秒只能生成 128 个 ID 了。
雪花算法实现类
雪花算法实现不过一百多行,还是比较简单的,到处都能搜到,《常见源码如下》:
public class IdWorker {
//因为二进制里第一个 bit 为如果是 1,那么都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0。
//机器ID 2进制5位 32位减掉1位 31个
private long workerId;
//机房ID 2进制5位 32位减掉1位 31个
private long datacenterId;
//代表一毫秒内生成的多个id的最新序号 12位 4096 -1 = 4095 个
private long sequence;
//设置一个时间初始值 2^41 - 1 差不多可以用69年
private long twepoch = 1585644268888L;
//5位的机器id
private long workerIdBits = 5L;
//5位的机房id
private long datacenterIdBits = 5L;
//每毫秒内产生的id数 2 的 12次方
private long sequenceBits = 12L;
// 这个是二进制运算,就是5 bit最多只能有31个数字,也就是说机器id最多只能是32以内
private long maxWorkerId = -1L ^ (-1L << workerIdBits);
// 这个是一个意思,就是5 bit最多只能有31个数字,机房id最多只能是32以内
private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
private long workerIdShift = sequenceBits;
private long datacenterIdShift = sequenceBits + workerIdBits;
private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
private long sequenceMask = -1L ^ (-1L << sequenceBits);
//记录产生时间毫秒数,判断是否是同1毫秒
private long lastTimestamp = -1L;
public long getWorkerId(){
return workerId;
}
public long getDatacenterId() {
return datacenterId;
}
public long getTimestamp() {
return System.currentTimeMillis();
}
public IdWorker(long workerId, long datacenterId, long sequence) {
// 检查机房id和机器id是否超过31 不能小于0
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(
String.format("worker Id can't be greater than %d or less than 0",maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(
String.format("datacenter Id can't be greater than %d or less than 0",maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
this.sequence = sequence;
}
// 这个是核心方法,通过调用nextId()方法,让当前这台机器上的snowflake算法程序生成一个全局唯一的id
public synchronized long nextId() {
// 这儿就是获取当前时间戳,单位是毫秒
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
System.err.printf(
"clock is moving backwards. Rejecting requests until %d.", lastTimestamp);
throw new RuntimeException(
String.format("Clock moved backwards. Refusing to generate id for %d milliseconds",
lastTimestamp - timestamp));
}
// 下面是说假设在同一个毫秒内,又发送了一个请求生成一个id
// 这个时候就得把seqence序号给递增1,最多就是4096
if (lastTimestamp == timestamp) {
// 这个意思是说一个毫秒内最多只能有4096个数字,无论你传递多少进来,
//这个位运算保证始终就是在4096这个范围内,避免你自己传递个sequence超过了4096这个范围
sequence = (sequence + 1) & sequenceMask;
//当某一毫秒的时间,产生的id数 超过4095,系统会进入等待,直到下一毫秒,系统继续产生ID
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0;
}
// 这儿记录一下最近一次生成id的时间戳,单位是毫秒
lastTimestamp = timestamp;
// 这儿就是最核心的二进制位运算操作,生成一个64bit的id
// 先将当前时间戳左移,放到41 bit那儿;将机房id左移放到5 bit那儿;将机器id左移放到5 bit那儿;将序号放最后12 bit
// 最后拼接起来成一个64 bit的二进制数字,转换成10进制就是个long型
return ((timestamp - twepoch) << timestampLeftShift) |
(datacenterId << datacenterIdShift) |
(workerId << workerIdShift) | sequence;
}
/**
* 当某一毫秒的时间,产生的id数 超过4095,系统会进入等待,直到下一毫秒,系统继续产生ID
* @param lastTimestamp
* @return
*/
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
//获取当前时间戳
private long timeGen(){
return System.currentTimeMillis();
}
}
启示录
也没啥启示,就是想记录一下这个不成功的改造而已。
思考一个问题:雪花算法 32个工作中心、32台机器的这种默认配置,能否满足分布式环境下的 ID 生成的需求呢?1024 台机器的集群规模、每毫秒 4096 个 ID ,够不够用呢?
为什么要这样改造雪花算法,就是一个单机应用,改造数据中心参数到 9 位完全没必要。在ID生成不密集的情况下,ID很少重复,但是在 1秒内,密集的生成 ID 、且有毫秒及的时间差时,就产生 ID 重复了。