Zer0e's Blog

浅谈雪花算法

字数统计: 1.4k阅读时长: 5 min
2020/10/09 Share

写在前面

最近学习也有点陷入迷茫状态,不知道学些什么,也不知道写点什么。那就想起什么就写点啥,就当是重新学习。
今天要来讲的是分布式系统中常用的一种算法,雪花算法。

正文

什么是雪花算法

雪花算法(snowflake),它是Twitter公司使用的在分布式系统中生成唯一ID的一个算法,在2014年开源。
雪花算法是在高并发环境下生成为ID的算法,它能保证分布式环境下ID不重复,且基本有序递增,并且不依赖与其他的第三方库。

雪花算法的原理

雪花算法的原理十分简单,它生成的ID由64bit组成,其中包括:

  • 1bit的标志位,由于二进制中最高为1的都是负数,并且生成的id一般都是整数,所以这一位通常为0.
  • 41bit的时间戳,可以存储69年的时间,一般来说起始时间戳为系统上线的时间戳。
  • 10bit的机器id,可以部署1024个节点。
  • 12bit序列号,用来表示同一时间戳下生成的不同id,在同一个时间戳下可以表示4095个序列号。

因为刚好是64bit,所以在Java中刚好由long类型来存储。倒不如说是这个算法可能一开始就想好使用long类型来存储。

代码实现

当然最重要的还是如何生成ID啦,那生成的算法网上有很多,我就依照可能大家看的最多的代码,来讲讲自己的理解。

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
public class SnowFlake {
// 业务上线时间 2020-10-09 00:00:00
private final long startTimestamp = 1602172800L;


// 机器id所占位数
private final long workerIdBits = 10L;
// 支持的最大机器id
private final long maxWorkerId = ~(-1L << workerIdBits);
//序列号占用的位数
private final long sequenceBits = 12L;
//最大序列号
private final long sequenceMask = ~(-1L << sequenceBits);

// 机器ID向左移12位
private final long workerIdShift = sequenceBits;
// 时间截向左移22位(10+12)
private final long timestampLeftShift = sequenceBits + workerIdBits;

//机器id,通过构造器传入
private long workerId;
//序列号
private long sequence = 0L;
//上一次生成id的时间戳
private long lastTimestamp = -1L;

public SnowFlake(long workerId) {
// 首先机器id不能小于0或大于2^workerIdBits - 1
if (workerId < 0 || workerId > maxWorkerId){
throw new RuntimeException("机器id不正确");
}
this.workerId = workerId;
}

// 此方法必须是线程安全的
public synchronized long nextId(){
long timestamp = System.currentTimeMillis();
//如果当前时间小于上一次生成id的时间,说明系统时钟被回退过,需要抛出异常
if (timestamp < lastTimestamp){
throw new RuntimeException("系统时间被回退");
}
//如果当前时间戳与上次生成id的时间戳相等,则增加序列
if (lastTimestamp == timestamp){
// 这里是为了避免超出sequenceBits,即超出后会变成0
sequence = (sequence + 1) & sequenceMask;
//如果等于0说明溢出
if (sequence == 0){
// 重新获取时间戳
timestamp = getNextMillis(lastTimestamp);
}
}
// 不等于上次生成id的时间则序列号直接为0
else{
sequence = 0L;
}
lastTimestamp = timestamp;
//通过位运算将三个部分进行拼接
return ((timestamp - startTimestamp) << timestampLeftShift)
| (workerId << workerIdShift)
| sequence;
}
// 阻塞到下一个毫秒,直到获取新的时间戳
// 由于获取下一个时间戳前进行了时钟回滚判断,所以这里不会长时间阻塞
private long getNextMillis(long lastTimestamp){
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp){
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}

在代码中都有相应的注释。其中需要理解的可能是生成最大值的位运算,比如maxWorkerId = ~(-1L << workerIdBits);
首先负数在计算机中是使用补码进行表示,补码是负数绝对值的原码,取反码再加一。-1的绝对值是1,1的原码就是最后一位是1,取反码,就是0变1,1变0,再加1,就得到了全是1的二进制。
然后左移workerIdBits位,也就是12位,然后再取反,就得到了最大值,即1023。
那其实取最大值有两种写法,一种是maxWorkerId = -1L ^ (-1L << workerIdBits);还有一种就是上面的写法,一种是异或一种是取反,得到的结果都是相同的。我个人也不确定哪种比较好,idea中提示我使用取反操作,那就当取反操作可能更好一些。当然这只是idea的建议,真正要区分的话还得看两种方法的底层速度比较,我对位运算不熟悉,希望有大佬能比较一下。

后续一些思考

那雪花算法并不算难,通常情况下我们可以对雪花算法进行魔改。比如我可能不需要用到1024个节点啊,那么就把workIdBit减少,然后时间戳增加或者序列号增加。
又或者我这个系统可能用不到69年,那就可以适当减少时间戳比特位。
那还有就是workid的问题,即workid该怎么决定?那我们可以通过JVM传参,即-D选项,那如果是通过容器化部署的话,可能没办法传入不同参数,那可以通过某些规则来计算workid,比如可以通过该台机器的ip地址或者mac地址来计算workid,但是需要保证workid不重复,否则即使在并发不高的情况下,id也可能重复,至于实现,这里便不再展开,感兴趣的可以自己写写。

CATALOG
  1. 1. 写在前面
  2. 2. 正文
    1. 2.1. 什么是雪花算法
    2. 2.2. 雪花算法的原理
    3. 2.3. 代码实现
  3. 3. 后续一些思考