Fork me on GitHub

分布式id雪花算法

基本原理

image-20220715235457678

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
public class SnowflakeIdWorker {
/**
* 开始时间截 (2015-01-01)
*/
private final long twepoch = 1420041600000L;
/**
* 机器id所占的位数
*/
private final long workerIdBits = 5L;
/**
* 数据标识id所占的位数
*/
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;
/**
* 机器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;
/**
* 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095)
*/
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
/**
* 工作机器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 工作ID (0~31)
* @param datacenterId 数据中心ID (0~31)
*/
public SnowflakeIdWorker(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 SnowflakeId
*/
public synchronized long nextId() {
long timestamp = timeGen();
// 如果当前时间小于上一次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 = tilNextMillis(lastTimestamp);
}
}
// 时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}
// 上次生成ID的时间截
lastTimestamp = timestamp;
// 移位并通过或运算拼到一起组成64位的ID
return ((timestamp - twepoch) << timestampLeftShift) //
| (datacenterId << datacenterIdShift) //
| (workerId << workerIdShift) //
| sequence;
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
* @param lastTimestamp 上次生成ID的时间截
* @return 当前时间戳
*/
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/**
* 返回以毫秒为单位的当前时间
* @return 当前时间(毫秒)
*/
protected long timeGen() {
return System.currentTimeMillis();
}

public static void main(String[] args) throws InterruptedException {
SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0);
for (int i = 0; i < 10; i++) {
long id = idWorker.nextId();
Thread.sleep(1);
System.out.println(id);
}
}
}

代码实现细节

  • nextId()方法是加锁的,同进程内需要竞争锁,因为内部有赋值维护sequence(序列号)和lastTimestamp(上次生成id的时间戳)。
  • 参数是组成机器部分的dataCenterId和workerId,各占5个Bit位。
  • 时钟回退是有可能发生的,如果发生了之后是可能会有分布式id重复的问题,这时候直接报错。(条件是当前时间戳 timeStampe < lastTimeStamp)
  • 如果当前时间戳timeStamp和lastTimeStamp相等,说明是同一个时间戳的获取分布式id的流程,这时候通过sequence增加来分配id。
  • 如果sequence + 1之后和sequence的掩码做 &操作,如果算出为0,则说明12位的sequence发生了溢出,这时要将timeStamp更新为下一个时间戳来获取分布式id。
  • 最后根据雪花算法,将移动对应的位之后再做或操作生成对应的64位id。

位运算的运用

  • sequence++之后判断是否溢出(大于对应的sequence的值的范围),用的是 sequence++ & sequenceMask == 0 来判断,这里掩码是2^12 - 1。与运算是二进制位都是1的时候才是1,其余为0。这里2^12-1的二进制位是 011111111111 做与操作结果为0 说明是和10000000000 做了与操作,即代表了溢出。
  • 最后按照雪花算法从高到低的位置左移对应的长度,再做或操作。或操作是当全为0时,结果的二进制位为0, 其余情况为1。高位比如时间戳已经左移了(10 + 12= 22位),后面的22位都是0, 此时和代表机器位置的数字做或操作,即将机器的10位二进制数字直接填充对应的位置即可。同理sequence的二进制位也一样。这样通过或运算就将 三部分的二进制位拼接了起来。

雪花算法流程

image-20220715235518521

-------------本文结束感谢您的阅读-------------

本文标题:分布式id雪花算法

文章作者:夸克

发布时间:2020年03月02日 - 23:03

最后更新:2022年07月16日 - 00:07

原始链接:https://zhanglijun1217.github.io/2020/03/02/分布式id雪花算法/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。