最近我需要为运行的分布式系统某部分模块构造系统唯一的ID, 而 ID 需要是数字的形式,并应该尽量的短。不得不说,这是一个有趣的问题
1 若干实现策略#
查阅完相关的资料,发现为分布式系统生成唯一 ID 方法挺多的,例如:
- UUID
- 使用一个 ticket server, 即中央的服务器,各个节点都从中央服务器取 ID
- Twitter 的 Snowflake 算法
- Boundary 的 flake 算法
其中 UUID 生成的 ID 是字符串+数字,不适用; ticket server 的做法略麻烦,笔者
并不想为了个 ID 还要去访问中央服务器;剩下就是 Snowflake 和 flake 算法,
flake 算法生成的是 128 位的 ID, 略长;所以最后笔者选择了 Snowflake 算法。
2 Snowflake 算法实现#
本来 Twitter 的算法是有相应实现的,不过后来删除了;笔者就只好自己卷起袖子自己 实现了:(
虽说 Twitter 没有了相应的实现,但是 Snowflake 算法原理很简单,实现起来并不难.
2.1 Snowflake 算法#
Snowflake 算法生成 64 位的 ID, ID 的格式是 41 位的时间戳 + 10 位的截断的 mac 地址 + 12 位递增序列:
1
2
3
4
| """id format =>
timestamp | machineId|sequence
41 | 10 |12
"""
|
2.2 Snowflake 实现#
2.2.1 生成时间戳#
Java 内置了生成精确到毫秒的时间戳的方法,非常便利:
1
| long timestamp = System.currentTimeMillis();
|
2.2.2 生成递增序列#
12 bits 的最大值是 2**12=4096, 所以生成递增序列也非常简单:
1
2
3
4
5
6
7
| private final long sequenceMax = 4096; //2**12
private volatile long sequence = 0L;
public void generateId(){
//do something
sequence = (sequence + 1) % sequenceMax;
//do something
}
|
2.2.3 获取 Mac 地址#
我们通过获取当前机器的 IP 地址以获取对应的物理地址:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| protected long getMachineId() throws GetHardwareIdFailedException {
try {
InetAddress ip = InetAddress.getLocalHost();
NetworkInterface network = NetworkInterface.getByInetAddress(ip);
long id;
if (network == null) {
id = 1;
} else {
byte[] mac = network.getHardwareAddress();
id =
((0x000000FF & (long) mac[mac.length - 1]) |
(0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6;
}
return id;
} catch (SocketException e) {
throw new GetHardwareIdFailedException(e);
} catch (UnknownHostException e) {
throw new GetHardwareIdFailedException(e);
}
}
|
又因为 Mac 地址是6 个字节 (48 bits),而需要的只是 10 bit, 所以需要取最低位的 2个字节 (16 bits),然后右移 6 bits 以获取 10 个 bits 的 Mac地址
2.3 Snowflake 完整代码#
下面是 Snowflake 的 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
| public class IdGenerator {
// id format =>
// timestamp |datacenter | sequence
// 41 |10 | 12
private final long sequenceBits = 12;
private final long machineIdBits = 10L;
private final long MaxMachineId = -1L ^ (-1L << machineIdBits);
private final long machineIdShift = sequenceBits;
private final long timestampLeftShift = sequenceBits + machineIdBits;
private static final Object lock=new Object();
private final long twepoch = 1288834974657L;
private final long machineId;
private final long sequenceMax = 4096; //2**12
private volatile long lastTimestamp = -1L;
private volatile long sequence = 0L;
private static volatile IdGenerator instance;
public static IdGenerator getInstance() throws Exception {
IdGenerator generator=instance;
if (instance == null) {
synchronized(lock){
generator=instance;
if(generator==null){
generator=new IdGenerator();
instance=generator;
}
}
}
return generator;
}
private IdGenerator() throws Exception {
machineId = getMachineId();
if (machineId > MaxMachineId || machineId < 0) {
throw new Exception("machineId > MaxMachineId");
}
}
public synchronized Long generateLongId() throws Exception {
long timestamp = System.currentTimeMillis();
if (timestamp < lastTimestamp) {
throw new Exception(
"Clock moved backwards. Refusing to generate id for " + (
lastTimestamp - timestamp) + " milliseconds.");
}
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) % sequenceMax;
if (sequence == 0) {
timestamp = tillNextMillis(lastTimestamp);
}
} else {
sequence = 0;
}
lastTimestamp = timestamp;
Long id = ((timestamp - twepoch) << timestampLeftShift) |
(machineId << machineIdShift) |
sequence;
return id;
}
protected long tillNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
protected long getMachineId() throws GetHardwareIdFailedException {
try {
InetAddress ip = InetAddress.getLocalHost();
NetworkInterface network = NetworkInterface.getByInetAddress(ip);
long id;
if (network == null) {
id = 1;
} else {
byte[] mac = network.getHardwareAddress();
id =
((0x000000FF & (long) mac[mac.length - 1]) |
(0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6;
}
return id;
} catch (SocketException e) {
throw new GetHardwareIdFailedException(e);
} catch (UnknownHostException e) {
throw new GetHardwareIdFailedException(e);
}
}
}
|
正如笔者所言,算法并不难,就是分别获取时间戳, mac 地址,和递增序列号,然后移位得到 ID. 但是在具体的实现中还是有一些需要注意的细节的。
2.3.1 线程同步#
因为算法中使用到递增的序列号来生成 ID,而在实际的开发或者生产环境中很可能不止一个线程在使用 IdGenerator 这个类,如果这样就很容易出现不同线程的竞争问题,所以我使用了单例模式来生成 ID, 一方面更符合生成器的设计,另一方面因为对生成 ID的方法进行了同步,就保证了不会出现竞争问题。
2.3.2 同一毫秒生成多个 ID#
因为序列号长度是 12个 bit, 那么序列号最大值就是2**12=4096了,此外时间戳是精确到毫秒的,这就是意味着,当一毫秒内,产生超过 4096 个 ID 的时候就会出现重复的ID.
这样的情况并不是不可能发生,所以要对此进行处理;所以在 generateId() 函数中:
1
2
3
4
5
6
| if (lastTimestamp == timestamp) {
sequence = (sequence + 1) % sequenceMax;
if (sequence == 0) {
timestamp = tillNextMillis(lastTimestamp);
}
}
|
有以上的一段代码。当现在的时间戳与之前的时间戳一致,那么就意味着还是同一毫秒,如果序列号为 0, 就说明已经产生了 4096 个 ID了,继续产生 ID,就会出现重复 ID, 所以要等待一毫秒,这个就是 tillNextMills() 函数的作用了。
3 小结#
算法虽然简单,但是在找到 Snowflake 算法之前,笔者尝试了挺多的算法,但是都是因为不符合要求而被一一否决, 而 Snowflake 算法虽然简单,但是胜在实用。最后附上我写的 snowflake 算法的 Python 实现: Snowfloke