雪花算法
英文名为 SnowFlake
,是一个64个big位组成的 long 类型的数字,由 Twitter 开源的分布式 ID 算法生成。主要应用于分布式环境下生成全局唯一ID。
雪花算法的意思是生成的ID如雪花一般独一无二。
如果我们单单只想解决全局唯一ID这个问题有很多的解决方法:比如 UUID
、系统时间戳
、Redis原子递增
、数据库全局表自增ID
等。但在实际应用中,我们需要的ID除了唯一性之外,还需要满足以下特征:
1)单调递增:保证下一个ID号一定大于上一个。
2)保证安全:ID号需要无规则性,不能让别人根据ID号猜出我们的信息和业务数据量,增加恶意用户扒取数据的难度。
3)含时间戳:ID需要记录系统时间戳。
4)高可用:获取分布式ID的请求,服务器至少要保证 99.999% 的情况下给创建一个全局唯一的分布式ID。
5)低延迟:获取分布式ID的请求,要快,急速。
6)高QPS:服务器要顶得住并且成功创建10w个分布式ID。
雪花算法就是一个比较符合这类特征的全局唯一算法。很多大厂的全局ID组件中,都有用到,比如百度的 UidGenerator、美团的 Leaf 算法等。
由于雪花算法严重依赖时间,所以当发生服务器时钟回拨时可能会产生重复ID。
雪花算法主要由4个部分组成:
1位标识:由于 long 基本类型在 Java 中是带符号的,最高位是符号位,正数是0,负数是1。由于 id 一般是正数,所以第一位都是0。
接下来41位存储毫秒级时间戳,41位可以表示 2^41-1 毫秒。
转化成年则是:(2^41-1)/(1000*60*60*24*356)=69 年。也就是说这个时间戳大概可以使用 69年 不重复。
注意: 这里的41位时间戳不是存储当前时间的时间戳,而是存储时间戳的差值 “当前时间戳 - 开始时间戳” 得到的值。这里的开始时间戳,一般是我们的 id 生成器开始使用的时间,由程序指定,设置好后就不要去变更了,切记!!!由此,雪花算法有了服务器时间回拨可能会生成重复id的缺点。
10位的数据机器位,包括 5 位 datacenterId
和 5 位 workerId
,最多可以部署 2^10=1024 台机器。
这里的 5 位可以表示的最大整数时 2^5-1=31,即可以用 0、1、2、3、…31 这 32 个数字,来表示不同的 datacenterId 或 workerId
用来记录同毫秒内产生的不同ID,12位的计数顺序支持每个节点每毫秒(同一机器,同一时间戳)产生 4096 个ID序号。
理论上雪花算法方案的 QPS 约为 409.6w/s,这种分配方式可以保证在任何一个 IDC 的任何一台机器在任意毫秒内生成的ID都是不同的。
关于具体实现网上有很多,这里就不赘述了,需要的话可以参考一下 hutool
中雪花算法的生成,依赖如下:
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>5.8.16version>
dependency>
使用示例如下:
import cn.hutool.core.util.IdUtil;
public static void main(String[] args) {
System.out.println(IdUtil.getSnowflakeNextId());
}
网上很多实现方法比较简单,针对机器数部分的处理不太够,在分布式场景下并发容易导致id重复。hutool
中的雪花算法是通过获取 MAC 地址来进行生成的,经过了大量的验证,还是比较严谨的,所以不推荐自己实现雪花算法。
了解了雪花算法的实现原理(1位符号位
+ 41位时间戳
+ 5位数据中心ID
+ 5位机器ID
+ 12位序列号
)之后,我们就可以根据已有的雪花算法ID来逆向解析出相应的信息。
解析代码实现如下:
AnalyzeSnowflake.java
import cn.hutool.core.util.IdUtil;
import java.text.SimpleDateFormat;
import java.util.Date;
public class AnalyzeSnowflake {
public static void main(String[] args) {
// 生成雪花算法ID
// timestamp - this.twepoch << 22 | this.dataCenterId << 17 | this.workerId << 12 | this.sequence
long snowflakeNextId = IdUtil.getSnowflakeNextId();
System.out.println("snowflake:" + snowflakeNextId);
// 补零
StringBuilder binaryString = new StringBuilder(Long.toBinaryString(1686769506909614080L)); // 将long类型的数值转换为二进制字符串
int addZeroCount = 64 - binaryString.length();
for (int i = 0; i < addZeroCount; i++) {
binaryString.insert(0, "0");
}
System.out.println("length:" + binaryString.length());
// 1位符号位 + 41位时间戳 + 5位数据中心ID + 5位机器ID + 12位序列号
System.out.println("part1-符号位:" + binaryString.charAt(0));
long millis = Long.parseLong(binaryString.substring(1, 42), 2);
long second = millis / 1000;
long minute = second / 60;
long hour = minute / 60;
long day = hour / 24;
long year = day / 365;
day = day % 365;
hour = hour % 24;
minute = minute % 60;
second = second % 60;
System.out.println("part2-从开始到现在运行了:" + String.format("%d年%d天%d小时%d分钟%d秒", year, day, hour, minute, second));
long startMillis = System.currentTimeMillis() - millis;
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("part2-开始时间大约在:" + simpleDateFormat.format(new Date(startMillis)));
System.out.println("part3-1-数据中心id:" + Long.parseLong(binaryString.substring(43, 47), 2));
System.out.println("part3-2-机器id:" + Long.parseLong(binaryString.substring(48, 52), 2));
System.out.println("part4-序列号:" + Long.parseLong(binaryString.substring(53, 64), 2));
}
}
执行结果:
在 hutool 的 Snowflake.java
源码中我们可以看到起始时间默认为:2010-11-04 01:42:54
,与我们推算的时间基本一致。
往下拉,在 Snowflake.java
可以看到雪花算法的核心实现:
timestamp - this.twepoch << 22 | this.dataCenterId << 17 | this.workerId << 12 | this.sequence
整理完毕,完结撒花~
参考地址:
1.雪花算法详解(原理优缺点及代码实现),https://www.cnblogs.com/mikechenshare/p/16787023.html
2.雪花算法原理及实现,https://blog.csdn.net/qq_41573860/article/details/124119358
3.通俗聊透雪花算法的实现原理,https://baijiahao.baidu.com/s?id=1750456904521809034&wfr=spider&for=pc