雪花算法是 Twitter 开源的一种分布式 ID 生成算法,用于在分布式系统中生成全局唯一、趋势递增的 64 位长整型 ID。
它的核心思想是:使用一个 64 位的 long 类型数字作为全局唯一 ID,并将这 64 位划分为若干部分,每部分存储不同的信息。
ID结构
1.符号位(1位)
始终为0,用于标识ID是正数。
2.时间戳(41位)
时间戳占据了整个ID的41位,精确到毫秒级,可以支持69年的时间戳。这使得雪花算法能够支持未来数十年的唯一性。时间戳部分还提供了排序的功能,可以根据时间戳来对数据进行排序。
3.机器ID(10位)
工作机器ID占据了ID的10位,可以支持最多1024个工作节点。这使得在同一台机器上运行的不同应用程序实例可以使用不同的工作机器ID来生成唯一的ID。
4.序列号(12位)
序列号占据了ID的12位,可以支持每个节点每毫秒产生4096个唯一的ID。这使得在同一台机器上运行的不同应用程序实例可以生成唯一的ID,即使在毫秒级别内也能保证唯一性。
雪花算法的特点
优点
- 高性能:完全在内存中生成,没有数据库 I/O 开销,单节点每秒可生成数万甚至数十万个 ID。
- 全局唯一:在分布式环境下,通过机器 ID 和时间戳的组合保证唯一性。
- 趋势递增:生成 ID 按时间趋势递增,有利于数据库索引和排序。
- 无外部依赖:不依赖数据库、Redis 等外部存储,部署简单。
- 位数可控:可根据业务需求调整时间戳、机器 ID、序列号的位数。
缺点
- 强依赖系统时钟:如果机器时钟回拨,可能导致 ID 冲突或重复。
- 单毫秒序列号有限:单节点每毫秒最多生成 4096 个 ID,超出会阻塞到下一毫秒,极端高并发下可能成为瓶颈。
- 机器 ID 需要手动分配:在大规模集群中,机器 ID 的管理和分配需要额外机制。
- 长整型在部分语言中精度问题 :如 JavaScript 中
Number类型最大安全整数为2^53 - 1,雪花 ID 超过该值会导致精度丢失,通常需要序列化为字符串传输。
以下是雪花算法的简单实现
public class SnowflakeIdGenerator {
// 起始时间戳(2020-01-01 00:00:00)
private final long epoch = 1577836800000L;
// 各部分位数
private final long workerIdBits = 10L;
private final long sequenceBits = 12L;
// 最大机器 ID(1023)
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
// 位移量
private final long workerIdShift = sequenceBits;
private final long timestampShift = sequenceBits + workerIdBits;
// 序列号掩码(4095)
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
private long workerId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdGenerator(long workerId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(
String.format("workerId 必须在 0 到 %d 之间", maxWorkerId));
}
this.workerId = workerId;
}
public synchronized long nextId() {
long timestamp = currentTime();
// 处理时钟回拨:如果当前时间小于上次生成 ID 的时间,则等待
if (timestamp < lastTimestamp) {
long offset = lastTimestamp - timestamp;
if (offset <= 5) {
// 如果回拨在 5ms 内,等待追赶
try {
Thread.sleep(offset);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("等待时钟追赶时被中断", e);
}
timestamp = currentTime();
} else {
throw new RuntimeException(
String.format("时钟回拨超过 5ms,拒绝生成 ID。当前时间差:%d ms", offset));
}
}
// 同一毫秒内生成 ID
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
// 当前毫秒序列号用完,等待下一毫秒
timestamp = waitNextMillis(lastTimestamp);
}
} else {
// 新的毫秒,序列号重置
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - epoch) << timestampShift) // 时间戳左移 22 位
| (workerId << workerIdShift) // 机器 ID 左移 12 位
| sequence; // 序列号占据低 12 位
}
private long waitNextMillis(long lastTimestamp) {
long timestamp = currentTime();
while (timestamp <= lastTimestamp) {
timestamp = currentTime();
}
return timestamp;
}
private long currentTime() {
return System.currentTimeMillis();
}
}
public class Main {
public static void main(String[] args) {
SnowflakeIdGenerator generator = new SnowflakeIdGenerator(1);
for (int i = 0; i < 10; i++) {
System.out.println(generator.nextId());
}
}
}
时钟回拨问题及解决方案
时钟回拨是雪花算法钟最最需要注意的问题,当系统的时间回到过去时( NTP 同步、手动调整、虚拟机漂移),可能产生重复时间戳,导致ID重复
解决方案
1.直接sleep到正确时间(只适合小的幅度)
2.检测到回拨直接拒绝生成
3.持久化最后时间戳 将其存到文件或数据库 重启时检查,避免宕机恢复后时间回拨。
4.弄个逻辑时钟 max 判断当前时间和上一个时间 避免回退