雪花算法(snowflake)是由Twitter公布的分布式主键生成算法,它能够保证不同进程主键的不重复性,以及相同进程主键的有序性。
雪花算法是以二进制数为操作数的算法,最终生成64bit的长整型数据。所以本文开篇先普及一下二进制一些基础知识。
二进制是计算技术中广泛采用的一种数制。二进制数据是用0和1两个数码来表示的数。 它的基数为2,进位规则是“逢二进一”,借位规则是“借一当二”,由18世纪德国数理哲学大师莱布尼兹发现。 当前的计算机系统使用的基本上是二进制系统,数据在计算机中主要是以补码的形式存储的。 计算机中的二进制则是一个非常微小的开关,用“开”来表示1,“关”来表示0。
二进制的运算
算术运算 二进制的加法:0+0=0,0+1=1 ,1+0=1, 1+1=10(向高位进位);例:7=111;10=1010;3=11
二进制的减法:0-0=0,0-1=1(向高位借位) 1-0=1,1-1=0 (模二加运算或异或运算) ;
二进制的乘法:0 * 0 = 0; 0 * 1 = 0; 1 * 0 = 0; 1 * 1 = 1;
二进制的除法:0÷0 = 0,0÷1 = 0,1÷0 = 0 (无意义),1÷1 = 1 ;
逻辑运算二进制的或运算:遇1得1 二进制的与运算:遇0得0 二进制的非运算:各位取反。
正数 | 负数 | |
---|---|---|
原码 | 原码 | 原码 |
反码 | 原码 | 原码符号位外按位取反 |
补码 | 原码 | 反码+1 |
无符号数中,所有的位都用于直接表示该值的大小。
有符号数中最高位用于表示正负。
例: 8位2进制表示的: 无符号数的范围为0(00000000B) ~ 255 (11111111B);
有符号数的范围为-128(10000000B) ~ 127 (01111111B);
255 = 1*2^7 + 1*2^6 + 1*2^5 +1*2^4 +1*2^3 +1*2^2 +1*2^1 +1*2^0;
127 = 1*2^6 + 1*2^5 +1*2^4 +1*2^3 +1*2^2 +1*2^1 +1*2^0;
位运算符比一般的算术运算符速度要快,而且可以实现一些算术运算符不能实现的功能。如果要开发高效率程序,位运算符是必不可少的。位运算符用来对二进制位进行操作,包括:按位与(&)、按位或(|)、按位异或(^)、按位取反(~)、按位左移(<<)、按位右移(>>)。下面就给大家介绍位运算符的详细用法。
指定 A = 60(0011 1100); B = 13 (0000 1101)
按位与(&)
对两个数进行操作,然后返回一个新的数,这个数的每个位都需要两个输入数的同一位都为1时才为1,如下图:
(A & B) 结果为 12, 二进制为 0000 1100
按位或(|)
比较两个数,然后返回一个新的数,这个数的每一位设置1的条件是两个输入数的同一位都不为0(即任意一个为1,或都为1),如下图:
(A | B) 结果为 61, 二进制为 0011 1101
按位异或(^)
比较两个数,然后返回一个数,这个数的每个位设为1的条件是两个输入数的同一位不同,如果相同就设为0,如下图:
(A ^ B) 结果为 49, 二进制为 0011 0001
按位取反(~)
对一个操作数的每一位都取反,如下图:
(~A ) 结果为 -61, 二进制为 1100 0011
按位左移(<<)
将操作数的所有位向左移动指定的位数。
下图展示了11111111 << 1(11111111 左移一位)的结果。蓝色数字表示被移动位,灰色表示被丢弃位,空位用橙色的0填充。
(A << 2)结果为 240, 二进制为 1111 0000
按位右移(<<)
将操作数的所有位向又移动指定的位数。
下图展示了11111111 >> 1(11111111 右移一位)的结果。蓝色数字表示被移动位,灰色表示被丢弃位,空位用橙色的0填充。
A >> 2 结果为 15, 二进制为 0000 1111
在同一个进程中,它首先是通过时间位保证不重复,如果时间相同则是通过序列位保证。 同时由于时间位是单调递增的,且各个服务器如果大体做了时间同步,那么生成的主键在分布式环境可以认为是总体有序的,这就保证了对索引字段的插入的高效性。例如MySQL的Innodb存储引擎的主键。
使用雪花算法生成的主键,二进制表示形式包含4部分,从高位到低位分表为:1bit符号位、41bit时间戳位、10bit工作进程位以及12bit序列号位。
预留的符号位,恒为零。
41位的时间戳可以容纳的毫秒数是2的41次幂,一年所使用的毫秒数是:365 * 24 * 60 * 60 * 1000
。通过计算可知:
Math.pow(2, 41) / (365 * 24 * 60 * 60 * 1000L);
结果约等于69.73年。如果系统中的雪花算法的时间纪元从2016年11月1日零点开始,可以使用到2086年,能满足绝大部分系统的要求。
该标志在Java进程内是唯一的,如果是分布式应用部署应保证每个工作进程的id是不同的。该值默认为0,可通过属性设置。
该序列是用来在同一个毫秒内生成不同的ID。如果在这个毫秒内生成的数量超过4096(2的12次幂),那么生成器会等待到下个毫秒继续生成。
public class Snowflake {
//开始时间截 (2015-01-01),自己设定
private final long twepoch = 1489111610226L;
//机器ID所占位置
private final long workerIdBits = 5L;
//数据标识所占位数
private final long dataCenterIdBits = 5L;
//支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
//支持的最大数据标识id,结果是31
private final long maxDataCenterId = -1L ^ (-1L << dataCenterIdBits);
//序列在id中占的位数
private final long sequenceBits = 12L;
//生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095),也是12位能存储的最大正整数:4095
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
//机器ID向左移12位
private final long workerIdShift = sequenceBits;
//数据标识id向左移17位(12+5)
private final long dataCenterIdShift = sequenceBits + workerIdBits;
//时间截向左移22位(5+5+12)
private final long timestampLeftShift = sequenceBits + workerIdBits + dataCenterIdBits;
//工作机器ID(0~31)
private long workerId;
//数据中心ID(0~31)
private long dataCenterId;
//毫秒内序列(0~4095)
private long sequence = 0L;
//上次生成ID的时间截
private long lastTimestamp = -1L;
/**
* 带参构造函数
* 数据中心号和工作机器号根据实际部署情况手动设置
*
* @param workerId
* @param dataCenterId
*/
public Snowflake(long workerId, long dataCenterId) {
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;
}
/**
* 获得下一个ID (该方法是线程安全的)
*
* @return
*/
public synchronized long nextId() {
long timestamp = timeNow();
//如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if (timestamp < lastTimestamp) {
throw new RuntimeException(String.format(
"Clock moved backwards. Refusing to generate id for %d milliseconds",
lastTimestamp - timestamp));
}
//如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
//毫秒内序列溢出
if (sequence == 0) {
//阻塞到下一个毫秒,获得新的时间戳
timestamp = untilNextMillis(lastTimestamp);
}
} else {//时间戳改变,毫秒内序列重置
sequence = 0L;
}
//上次生成ID的时间截
lastTimestamp = timestamp;
//移位并通过或运算拼到一起组成64位的ID
return ((timestamp - twepoch) << timestampLeftShift)
| (dataCenterId << dataCenterIdShift)
| (workerId << workerIdShift)
| sequence;
}
private long untilNextMillis(long lastTimestamp) {
long timestamp = timeNow();
while (timestamp <= lastTimestamp) {
timestamp = timeNow();
}
return timestamp;
}
private long timeNow() {
return System.currentTimeMillis();
}
public static void main(String[] args) {
Snowflake idWorker = new Snowflake(0, 0);
for (int i = 0; i < 100; i++) {
long id = idWorker.nextId();
System.out.println(id);
}
}
}