算法–基础–12–SnowFlake
1、介绍
- 是 Twitter 开源的分布式 id 生成算法。
- 核心思想:使用一个 64 bit 的 long 型的数字作为全局唯一 id。
2、结构
0 - 0001000000 0000010000 0001000100 0000100000 0 - 10001 - 11001 - 000000000000
2.1、第1部分:1bit
- 1 个 bit
- 这个是无意义的
无意义原因
因为二进制里第一个 bit 为如果是 1,那么都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0。
2.2、第2部分:41 bit
- 41 个 bit
- 表示时间戳。
- 单位是毫秒。
- 可以使用69年
可以使用69年的原因
41 bit 可以表示的数字多达 2^41 - 1,也就是可以标识 2 ^ 41 - 1 个毫秒值,换算成年就是表示 69 年的时间。
2.3、第3部分:10 bit
-
记录工作的机器 id,代表的是这个雪花算法服务最多可以部署在 2^10 台机器上,也就是 1024 台机器。
-
10 bit 里 5 个 bit 代表机房 id,5 个 bit 代表机器 id。意思就是最多代表 2 ^ 5 个机房(32 个机房),每个机房里可以代表 2 ^ 5 个机器(32 台机器),也可以根据自己公司的实际情况确定。
2.3.1、第1部分:5bit
- 5 个 bit
- 表示机房 id,如上面10001
2.3.2、第2部分:5bit
- 5 个 bit
- 表示机器id,如上面11001
2.4、第4部分:12 个 bit
- 12 个 bit:
- 表示序列号
- 表示10001机房11001机器上这一毫秒内同时生成的id 的序号
12 个 bit 可以生产多少序列号
12 bit 可以代表的最大正整数是 2 ^ 12= 4096,也就是同一个毫秒内可以生成 4096 个不同的 id。也就是0到4095。
3、总结
3.1、生成唯一 id的过程
假设你的某个服务假设要生成一个全局唯一 id,那么就可以发送一个请求给部署了 SnowFlake 算法的系统,由这个 SnowFlake 算法系统来生成唯一 id。
这个 SnowFlake 算法系统首先肯定是知道自己所在的机房和机器的,比如机房 id = 17,机器 id = 25。
接着 SnowFlake 算法系统接收到这个请求之后,首先就会用二进制位运算的方式生成一个 64 bit 的 long 型id
步骤1: 1bit
给1bit 设置为0
步骤2: 41bit
用当前时间戳(单位到毫秒)生成41bit
步骤3: 5bit
机房 id = 17,生成5bit
步骤4: 5bit
机器 id = 25,生成5bit
步骤5: 12bit
最后再判断一下,当前这台机房的这台机器上这一毫秒内,这是第几个请求,给这次生成 id 的请求累加一个序号,作为最后的 12 个 bit。
最终一个 64 个 bit 的 id 就出来了
这个算法可以保证说,一个机房的一台机器上,在同一毫秒内,生成了一个唯一的 id。可能一个毫秒内会生成多个 id,但是有最后 12 个 bit 的序号来区分开来。
3.2、小结
用一个 64 bit 的数字中各个 bit 位来设置不同的标志位,区分每一个 id。
4、代码
package com.example.snowflake.demo1;
public class SnowFlake {
/**
* 起始的时间戳 2016-11-26 21:21:05
*/
private final static long START_STAMP = 1480166465631L;
/**
* 每一部分占用的位数
*/
private final static long SEQUENCE_BIT = 12; // 序列号占用的位数
private final static long MACHINE_BIT = 5; // 机器标识占用的位数
private final static long DATA_CENTER_BIT = 5;// 数据中心占用的位数
/**
* 每一部分的最大值
* MAX_DATA_CENTER_NUM的计算过程
* DATA_CENTER_BIT=5,对应的补码: 0000 0101
* -1L对应的补码: 1111 1111
* -1L <<5 后:1110 0000
* -1L ^ 1110 0000后 0001 1111
* 0001 1111 等于31
*/
// 机房的最大id,也就是机房的最大数量
// 这里是31
private final static long MAX_DATA_CENTER_NUM = -1L ^ (-1L << DATA_CENTER_BIT);
// 机器的最大id,也就是机器的最大数量
// 这里是31
private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
// 序列号的最大值
// 这里是 4095
private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);
/**
* 每一部分向左的位移
*/
// 机器的二进制数据左位移 12位
private final static long MACHINE_LEFT = SEQUENCE_BIT;
// 机房的二进制数据左位移 17位
private final static long DATA_CENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
// 时间戳的二进制数据左位移 22位
private final static long TIMESTAMP_LEFT = DATA_CENTER_LEFT + DATA_CENTER_BIT;
// 机房ID,5个bit,最大2^5=32,0到31,即(0000 0000-0001 1111)
private long dataCenterId;
// 机器ID,5个bit,最大2^5=32,0到31,即(0000 0000-0001 1111)
private long machineId;
// 序列号:12个bit,最大2^12=4096,0到4096,即(0000 0000 0000-1111 1111 1111)
// 代表一毫秒内生成多少个序号,可以生成4096个,因为第1个0不需要生成。所以真正可以生成4095个
private long sequence = 0L;
// 上一次时间戳
private long lastStamp = -1L;
public SnowFlake(long dataCenterId, long machineId) {
// 检查机房id是否超过31 不能小于0
if (dataCenterId > MAX_DATA_CENTER_NUM || dataCenterId < 0) {
throw new IllegalArgumentException("机房id的id不能超过最大值,也不能为0");
}
// 检查机器id是否超过31 不能小于0
if (machineId > MAX_MACHINE_NUM || machineId < 0) {
throw new IllegalArgumentException("机器id的id不能超过最大值,也不能为0");
}
// 设置当前机房id
this.dataCenterId = dataCenterId;
// 设置当前机器id
this.machineId = machineId;
}
/**
* 让当前这台机器上的snowflake算法程序生成一个全局唯一的id
* @return
*/
public synchronized long nextId() {
// 获得当前时间的毫秒数
long currStamp = getNewStamp();
if (currStamp < lastStamp) {
throw new RuntimeException("服务器的时钟向后移动,拒绝生成id");
}
// 假设在同一个毫秒内,又发送了一个请求生成一个id
// 这个时候就得把seqence序号给递增1,最多就是4095
if (currStamp == lastStamp) {
// 相同毫秒内,序列号自增,且保证取值范围最大为MAX_SEQUENCE
sequence = (sequence + 1) & MAX_SEQUENCE;
// 同一毫秒的序列数已经达到最大,也就是产生的id数 超过4095,系统会进入等待,直到下一毫秒,系统继续产生ID
if (sequence == 0L) { // 即:已经超出MAX_SEQUENCE 即:1 0000 0000 0000
currStamp = getNextMill();
}
} else {
// 不同毫秒内,序列号置为0
sequence = 0L;
}
// 这儿记录一下最近一次生成id的时间戳,单位是毫秒
lastStamp = currStamp;
// 这儿就生成一个64bit id的二进制位运算操作
System.out.println("时间戳:" + (currStamp - START_STAMP));
System.out.println("时间戳左移位数:" + (TIMESTAMP_LEFT));
System.out.println("机房id:" + (dataCenterId));
System.out.println("机房id左移位数:" + (DATA_CENTER_LEFT));
System.out.println("机器id:" + (machineId));
System.out.println("机器id左移位数:" + (MACHINE_LEFT));
System.out.println("序列号:" + (sequence));
long id = (currStamp - START_STAMP) << TIMESTAMP_LEFT // 时间戳部分
| dataCenterId << DATA_CENTER_LEFT // 数据中心部分
| machineId << MACHINE_LEFT // 机器标识部分
| sequence; // 序列号部分
System.out.println("id的二进制是:" + Long.toBinaryString(id));
return id;
}
/**
* 获得下一个毫秒数,比lastStamp大的下一个毫秒数
*
* @return
*/
private long getNextMill() {
long mill = getNewStamp();
while (mill <= lastStamp) {
mill = getNewStamp();
}
return mill;
}
/**
* 获得当前毫秒数
*
* @return
*/
private long getNewStamp() {
return System.currentTimeMillis();
}
public static void main(String[] args) {
SnowFlake snowFlake = new SnowFlake(3L, 10L);
System.out.println("snowFlake.nextId() = " + snowFlake.nextId());
}
}
5、sequence = (sequence + 1) & MAX_SEQUENCE的逻辑
5.1、 相同毫秒内,序列号自增 原理
&公式
1&1=1
1&0=0
0&1=0
0&0=0
原理
MAX_SEQUENCE是2^12-1,即 1111 1111 1111。
根据 &公式,我们可以得到,x & 1111 1111 1111,那么结果就是x二进制从右到左的12位,内容都不变,因为1&1=1,0&1=0。
5.2、 sequence 保证取值范围最大为MAX_SEQUENCE的原理
x二进制从右到左的13位开始,和1111 1111 1111 的& 运算都是都是0,所以最大就是MAX_SEQUENCE
测试地址
https://www.23bei.com/tool-531.html
6、(currStamp - START_STAMP) << TIMESTAMP_LEFT | dataCenterId << DATA_CENTER_LEFT | machineId << MACHINE_LEFT | sequence 原理
6.1、 为什么要currStamp - START_STAMP) << TIMESTAMP_LEFT
TIMESTAMP_LEFT=22
6.2、 原理
t=currStamp - START_STAMP
t最大是:2^41-1
假设t=1000
1000的二进制如下(41bit)
0 0000 0000 0000 0000 0000 0000 0000 0011 1110 1000
假设dataCenterId=3
3的二进制如下(5bit)
0 0011
假设machineId=10
10的二进制如下(5bit)
0 1010
假设sequence=10
10的二进制如下(12bit)
0000 0000 0000
因为第1位是0,所以拼接起来id的二进制就是
0000000000000000000000000000000011111010000001101010000000000000
我们通过代码调试结果
时间戳:1000
时间戳左移位数:22
机房id:3
机房id左移位数:17
机器id:10
机器id左移位数:12
序列号:0
id的二进制是:11111010000001101010000000000000
snowFlake.nextId() = 4194738176
7、优缺点
7.1、优点
- 按照时间自增排序,在多个分布式系统内不会产生id碰撞(数据中心+机器id区分)
- 毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
- 容量大:理论上QPS约为409.6w/s(1000*2^12)个自增ID。
- 高可用:生成时不依赖于数据库,完全在内存中生成。
- 灵活性高:可以根据自身业务情况调整分配bit位
7.2、缺点:
强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。
针对此,美团做出了改进:
https://github.com/Meituan-Dianping/Leaf
8、分布式部署
8.1、场景1
应用A,容器化部署,有10个POD,现在有个定时任务,每秒往T1表插入1000条数据。那么10个POD一起跑的时候,就有可能产生主键冲突
解决方案:
通过第3部分:10 bit
来解决,不同的pod,设置不同的机器 id
。
机器 id
可以通过 (podIP转数字)%32取模来获取,其中32是2^5,表示机器id的范围。