雪花组成

第一位(符号位):由于ID都是正整数,所以第一位始终为0。(负数的二进制第一位为1)

时间戳(41位):记录时间戳的差值(相对于某个固定时间点),单位是毫秒。41位时间戳可以使用69年(从1970年开始,可用至2039年)。

工作机器id(10位):用于标识不同的工作机器(如不同的服务器实例),支持在同一数据中心内部署最多1024台机器。

如果存在跨机房部署的情况下可以把这10个bit位,拆分成两个5bit,前5个bit表示机房id,后面5个表示机器的id

序列号(12位):用于在同一毫秒内产生不同的ID,支持每个工作机器在同一毫秒内产生最多4096个ID。

实现流程

  1. 初始化参数
    在启动服务时,需要为每个工作机器分配一个唯一的工作机器id和一个数据中心id。这些id通常在服务配置中指定,并且在整个服务运行期间保持不变。

  2. 生成时间戳
    每次生成ID时,首先获取当前时间戳(毫秒级),并计算其与起始时间戳的差值。起始时间戳是一个固定的时间点,通常设置为服务的启动时间或某个固定的时间点。

  3. 分配工作机器id和数据中心id
    根据服务的配置,将工作机器id和数据中心id分别放入ID的相应位置。这些id在初始化时分配,并在整个服务运行期间保持不变。

  4. 生成序列号
    在同一毫秒内,如果多次调用生成ID的方法,需要使用序列号来区分不同的ID。序列号从0开始递增,当序列号达到最大值(4095)时,需要等待下一毫秒才能继续生成ID。

  5. 组装ID
    将时间戳差值、工作机器id、数据中心id和序列号按照指定的位数进行位移和或运算,生成最终的64位ID。

  6. 处理时钟回拨问题
    由于时钟误差或网络延迟等原因,可能会出现时钟回拨的情况。为了处理这种情况,雪花算法通常使用一个缓存机制来存储最近生成的时间戳。如果当前时间戳小于缓存中的时间戳,则拒绝生成ID并等待一段时间再试。这样可以避免由于时钟回拨导致的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
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
package com.yy;

import java.util.concurrent.atomic.LongAdder;

/**
* ID生成器,基于Snowflake算法实现。
* 用于生成全局唯一且递增的ID。
*/
public class IdGenerator {

// 起始时间戳,用于计算相对于此时间的毫秒数
public static final long START_STAMP = DateUtil.get("2022-1-1").getTime();

// 定义数据中心位数、机器位数和序列号位数
public static final long DATA_CENTER_BIT = 5L;
public static final long MACHINE_BIT = 5L;
public static final long SEQUENCE_BIT = 12L;

// 根据位数计算最大值
public static final long DATA_CENTER_MAX = ~(-1L << DATA_CENTER_BIT);
public static final long MACHINE_MAX = ~(-1L << MACHINE_BIT);
public static final long SEQUENCE_MAX = ~(-1L << SEQUENCE_BIT);

// 定义时间戳、数据中心、机器号和序列号的位移
public static final long TIMESTAMP_LEFT = DATA_CENTER_BIT + MACHINE_BIT + SEQUENCE_BIT;
public static final long DATA_CENTER_LEFT = MACHINE_BIT + SEQUENCE_BIT;
public static final long MACHINE_LEFT = SEQUENCE_BIT;

// 数据中心ID、机器ID和序列号
private long dataCenterId;
private long machineId;
private LongAdder sequenceId = new LongAdder();
// 记录上一次的时间戳,用于处理时钟回拨问题
private long lastTimeStamp = -1L;

/**
* 构造函数,初始化ID生成器。
* @param dataCenterId 数据中心ID,用于标识不同的数据中心
* @param machineId 机器ID,用于标识同一数据中心内的不同机器
* @throws IllegalArgumentException 如果数据中心ID或机器ID超出范围,抛出此异常
*/
public IdGenerator(long dataCenterId, long machineId) {
if(dataCenterId > DATA_CENTER_MAX || machineId > MACHINE_MAX){
throw new IllegalArgumentException("你传入的数据中心编号或机器号不合法.");
}
this.dataCenterId = dataCenterId;
this.machineId = machineId;
}

/**
* 生成唯一ID。
* @return 生成的唯一ID
* @throws RuntimeException 如果检测到时钟回拨,抛出此异常
*/
public long getId(){
// 获取当前时间戳
long currentTime = System.currentTimeMillis();
long timeStamp = currentTime - START_STAMP;

// 检查时钟回拨
if(timeStamp < lastTimeStamp){
throw new RuntimeException("您的服务器进行了时钟回调.");
}

// 处理同一时间戳内的序列号自增
if (timeStamp == lastTimeStamp){
sequenceId.increment();
if(sequenceId.sum() >= SEQUENCE_MAX){
timeStamp = getNextTimeStamp();
sequenceId.reset();
}
} else {
sequenceId.reset();
}

// 更新上一次的时间戳
lastTimeStamp = timeStamp;
long sequence = sequenceId.sum();
// 组合时间戳、数据中心ID、机器ID和序列号,生成最终ID
return timeStamp << TIMESTAMP_LEFT | dataCenterId << DATA_CENTER_LEFT
| machineId << MACHINE_LEFT | sequence;
}

/**
* 获取下一个时间戳,用于处理时钟回拨情况。
* @return 下一个时间戳
*/
private long getNextTimeStamp() {
long current = System.currentTimeMillis() - START_STAMP;
// 如果当前时间与上一次时间相同,则循环等待直到时间戳改变
while (current == lastTimeStamp){
current = System.currentTimeMillis() - START_STAMP;
}
return current;
}

public static void main(String[] args) {
IdGenerator idGenerator = new IdGenerator(1,2);
for (int i = 0; i < 1000; i++) {
new Thread(() -> System.out.println(idGenerator.getId())).start();
}
}
}

算法存在的问题以及解决方案

  1. 系统时钟回拨:如果服务器的时间突然回拨,可能导致生成的时间戳变小,从而生成重复的ID。
  2. 机器ID配置错误:如果在不同的服务器上配置了相同的机器ID,那么这些服务器生成的ID就可能出现重复。
  3. 并发量超出设计范围:如果同一台机器在同一毫秒内的并发请求超过了4096次,那么序列号就会耗尽,从而导致ID重复。

解决方案

  1. 系统时钟同步:确保所有服务器的系统时钟是同步的,并且精确到毫秒。可以使用NTP(Network Time Protocol)等协议来同步系统时钟。
  2. 合理分配机器ID:为每个服务器分配唯一的机器ID,并确保这些ID不会发生冲突。可以使用配置文件、数据库或者服务发现等方式来管理和分配机器ID。
  3. 优化并发处理:如果服务器的并发量非常高,可以考虑优化系统的并发处理能力,比如使用连接池、异步处理等方式来降低同一毫秒内的请求量。
  4. 引入容错机制:在生成ID时,可以增加一些容错机制来避免ID重复。例如,当检测到系统时钟回拨时,可以暂停生成ID,直到系统时钟恢复正常;当检测到即将生成重复的ID时,可以调整时间戳或序列号来避免重复。