是什么

SnowFlake 中文意思为雪花,故称为雪花算法。最早是 Twitter 公司在其内部用于分布式环境下生成唯一 ID。在 2014 年开源 scala 语言版本。

ID 结构

1676949468000

雪花算法原理就是生成一个的 64 位比特位的 long 类型的唯一 id。

  • 第 1 位。二进制中数字表达形式最高位为 1 的代表负数,为 0 代表正数。我们生成的 id 一般都使用整数,所以这个最高位固定是0
  • 接下来 41 位存储毫秒级时间戳,2^41/(1000_60_60_24_365)=69,大概可以使用 69 年。
  • 再接下 10 位存储机器码,包括 5 位 datacenterId 和 5 位 workerId。最多可以部署 2^10=1024 台机器。
  • 最后 12 位存储序列号。同一毫秒时间戳时,通过这个递增的序列号来区分。即对于同一台机器而言,同一毫秒时间戳下,可以生成 2^12=4096 个不重复 id。

算法实现(Java)

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
import java.util.Set;
import java.util.TreeSet;

/**
* @description 雪花算法ID生成器
*/
public class SnowflakeIdGenerator {

// 初始时间戳(纪年),可用雪花算法服务上线时间戳的值
// 1649059688068:2022-04-04 16:08:08
private static final long INIT_EPOCH = 1649059688068L;

// 记录最后使用的毫秒时间戳,主要用于判断是否同一毫秒,以及用于服务器时钟回拨判断
private long lastTimeMillis = -1L;

// dataCenterId占用的位数
private static final long DATA_CENTER_ID_BITS = 5L;
// dataCenterId占用5个比特位,最大值31
// 0000000000000000000000000000000000000000000000000000000000011111
private static final long MAX_DATA_CENTER_ID = ~(-1L << DATA_CENTER_ID_BITS);
// datacenterId
private long datacenterId;

// workId占用的位数
private static final long WORKER_ID_BITS = 5L;
// workId占用5个比特位,最大值31
// 0000000000000000000000000000000000000000000000000000000000011111
private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);
// workId
private long workerId;

// 最后12位,代表每毫秒内可产生最大序列号,即 2^12 - 1 = 4095
private static final long SEQUENCE_BITS = 12L;
// 掩码(最低12位为1,高位都为0),主要用于与自增后的序列号进行位与,如果值为0,则代表自增后的序列号超过了4095
// 0000000000000000000000000000000000000000000000000000111111111111
private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);
// 同一毫秒内的最新序号,最大值可为 2^12 - 1 = 4095
private long sequence;

// workId位需要左移的位数 12
private static final long WORK_ID_SHIFT = SEQUENCE_BITS;
// dataCenterId位需要左移的位数 12+5
private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
// 时间戳需要左移的位数 12+5+5
private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS;

public SnowflakeIdGenerator(long datacenterId, long workerId) {

// 检查datacenterId的合法值
if (datacenterId < 0 || datacenterId > MAX_DATA_CENTER_ID) {
throw new IllegalArgumentException(
String.format("datacenterId值必须大于0并且小于%d", MAX_DATA_CENTER_ID));
}

// 检查workId的合法值
if (workerId < 0 || workerId > MAX_WORKER_ID) {
throw new IllegalArgumentException(String.format("workId值必须大于0并且小于%d", MAX_WORKER_ID));
}

this.workerId = workerId;
this.datacenterId = datacenterId;
}

/**
* 通过雪花算法生成下一个id,注意这里使用synchronized同步
*
* @return 唯一id
*/
public synchronized long nextId() {

long currentTimeMillis = System.currentTimeMillis();

// 当前时间小于上一次生成id使用的时间,可能出现服务器时钟回拨问题
if (currentTimeMillis < lastTimeMillis) {
throw new RuntimeException(
String.format("可能出现服务器时钟回拨问题,请检查服务器时间。当前服务器时间戳:%d,上一次使用时间戳:%d", currentTimeMillis,
lastTimeMillis));
}

if (currentTimeMillis == lastTimeMillis) { // 还是在同一毫秒内,则将序列号递增1,序列号最大值为4095

// 序列号的最大值是4095,使用掩码(最低12位为1,高位都为0)进行位与运行后如果值为0,则自增后的序列号超过了4095
// 那么就使用新的时间戳
sequence = (sequence + 1) & SEQUENCE_MASK;
if (sequence == 0) {
currentTimeMillis = tilNextMillis(lastTimeMillis);
}

} else { // 不在同一毫秒内,则序列号重新从0开始,序列号最大值为4095
sequence = 0;
}

// 记录最后一次使用的毫秒时间戳
lastTimeMillis = currentTimeMillis;

// 核心算法,将不同部分的数值移动到指定的位置,然后进行或运行
return ((currentTimeMillis - INIT_EPOCH) << TIMESTAMP_SHIFT) | (datacenterId
<< DATA_CENTER_ID_SHIFT) | (workerId << WORK_ID_SHIFT) | sequence;
}

/**
* 获取指定时间戳的接下来的时间戳,也可以说是下一毫秒
*
* @param lastTimeMillis 指定毫秒时间戳
* @return 时间戳
*/
private long tilNextMillis(long lastTimeMillis) {
long currentTimeMillis = System.currentTimeMillis();
while (currentTimeMillis <= lastTimeMillis) {
currentTimeMillis = System.currentTimeMillis();
}
return currentTimeMillis;
}

public static void main(String[] args) {
SnowflakeIdGenerator snowflakeIdGenerator = new SnowflakeIdGenerator(1, 2);

// 生成50个id
Set<Long> set = new TreeSet<>();
for (int i = 0; i < 50; i++) {
set.add(snowflakeIdGenerator.nextId());
}
System.out.println(set.size());
System.out.println(set);

// 验证生成100万个id需要多久
long startTime = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
snowflakeIdGenerator.nextId();
}
System.out.println(System.currentTimeMillis() - startTime);
}
}

改良版

图片

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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
/*
* Copyright 1999-2019 Seata.io Group.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.seata.common.util;

import java.net.NetworkInterface;
import java.util.Enumeration;
import java.util.Random;
import java.util.concurrent.atomic.AtomicLong;

/**
* @author funkye
* @author selfishlover
*/
public class IdWorker {

/**
* Start time cut (2020-05-03)
*/
private final long twepoch = 1588435200000L;

/**
* The number of bits occupied by workerId
*/
private final int workerIdBits = 10;

/**
* The number of bits occupied by timestamp
*/
private final int timestampBits = 41;

/**
* The number of bits occupied by sequence
*/
private final int sequenceBits = 12;

/**
* Maximum supported machine id, the result is 1023
*/
private final int maxWorkerId = ~(-1 << workerIdBits);

/**
* business meaning: machine ID (0 ~ 1023)
* actual layout in memory:
* highest 1 bit: 0
* middle 10 bit: workerId
* lowest 53 bit: all 0
*/
private long workerId;

/**
* timestamp and sequence mix in one Long
* highest 11 bit: not used
* middle 41 bit: timestamp
* lowest 12 bit: sequence
*/
private AtomicLong timestampAndSequence;

/**
* mask that help to extract timestamp and sequence from a long
*/
private final long timestampAndSequenceMask = ~(-1L << (timestampBits + sequenceBits));

/**
* instantiate an IdWorker using given workerId
* @param workerId if null, then will auto assign one
*/
public IdWorker(Long workerId) {
initTimestampAndSequence();
initWorkerId(workerId);
}

/**
* init first timestamp and sequence immediately
*/
private void initTimestampAndSequence() {
long timestamp = getNewestTimestamp();
long timestampWithSequence = timestamp << sequenceBits;
this.timestampAndSequence = new AtomicLong(timestampWithSequence);
}

/**
* init workerId
* @param workerId if null, then auto generate one
*/
private void initWorkerId(Long workerId) {
if (workerId == null) {
workerId = generateWorkerId();
}
if (workerId > maxWorkerId || workerId < 0) {
String message = String.format("worker Id can't be greater than %d or less than 0", maxWorkerId);
throw new IllegalArgumentException(message);
}
this.workerId = workerId << (timestampBits + sequenceBits);
}

/**
* get next UUID(base on snowflake algorithm), which look like:
* highest 1 bit: always 0
* next 10 bit: workerId
* next 41 bit: timestamp
* lowest 12 bit: sequence
* @return UUID
*/
public long nextId() {
waitIfNecessary();
long next = timestampAndSequence.incrementAndGet();
long timestampWithSequence = next & timestampAndSequenceMask;
return workerId | timestampWithSequence;
}

/**
* block current thread if the QPS of acquiring UUID is too high
* that current sequence space is exhausted
*/
private void waitIfNecessary() {
long currentWithSequence = timestampAndSequence.get();
long current = currentWithSequence >>> sequenceBits;
long newest = getNewestTimestamp();
if (current >= newest) {
try {
Thread.sleep(5);
} catch (InterruptedException ignore) {
// don't care
}
}
}

/**
* get newest timestamp relative to twepoch
*/
private long getNewestTimestamp() {
return System.currentTimeMillis() - twepoch;
}

/**
* auto generate workerId, try using mac first, if failed, then randomly generate one
* @return workerId
*/
private long generateWorkerId() {
try {
return generateWorkerIdBaseOnMac();
} catch (Exception e) {
return generateRandomWorkerId();
}
}

/**
* use lowest 10 bit of available MAC as workerId
* @return workerId
* @throws Exception when there is no available mac found
*/
private long generateWorkerIdBaseOnMac() throws Exception {
Enumeration<NetworkInterface> all = NetworkInterface.getNetworkInterfaces();
while (all.hasMoreElements()) {
NetworkInterface networkInterface = all.nextElement();
boolean isLoopback = networkInterface.isLoopback();
boolean isVirtual = networkInterface.isVirtual();
if (isLoopback || isVirtual) {
continue;
}
byte[] mac = networkInterface.getHardwareAddress();
return ((mac[4] & 0B11) << 8) | (mac[5] & 0xFF);
}
throw new RuntimeException("no available mac found");
}

/**
* randomly generate one as workerId
* @return workerId
*/
private long generateRandomWorkerId() {
return new Random().nextInt(maxWorkerId + 1);
}
}

算法优缺点

雪花算法有以下几个优点:

  • 高并发分布式环境下生成不重复 id,每秒可生成百万个不重复 id。
  • 基于时间戳,以及同一时间戳下序列号自增,基本保证 id 有序递增。
  • 不依赖第三方库或者中间件。
  • 算法简单,在内存中进行,效率高。

雪花算法有如下缺点:

  • 依赖服务器时间,服务器时钟回拨时可能会生成重复 id。算法中可通过记录最后一个生成 id 时的时间戳来解决,每次生成 id 之前比较当前服务器时钟是否被回拨,避免生成重复 id。

注意事项

其实雪花算法每一部分占用的比特位数量并不是固定死的。例如你的业务可能达不到 69 年之久,那么可用减少时间戳占用的位数,雪花算法服务需要部署的节点超过 1024 台,那么可将减少的位数补充给机器码用。

注意,雪花算法中 41 位比特位不是直接用来存储当前服务器毫秒时间戳的,而是需要当前服务器时间戳减去某一个初始时间戳值,一般可以使用服务上线时间作为初始时间戳值。

对于机器码,可根据自身情况做调整,例如机房号,服务器号,业务号,机器 IP 等都是可使用的。对于部署的不同雪花算法服务中,最后计算出来的机器码能区分开来即可。