雪花算法 Snowflake 原理分析与 Java 实现

317 阅读5分钟

雪花算法 Snowflake

一种由 Twitter 开发的分布式全局唯一 ID 生成算法,它生成的 ID 是一个 64 位的整数,也就是 Java 中的 long 类型。

具备以下特点:

  1. 整体有序性:基于时间戳生成,大致按照时间递增。
  2. 高性能:生成速度快,适用于高并发场景。
  3. 分布式:可以在多个节点生成唯一 ID。
  4. 高扩展性:通过调整位数分配,可以适应不同场景需求。

原理分析

标准的 64 位雪花算法 ID 分为以下几个部分:

1. 符号位(1 bit): 始终为 0,表示 ID 为正数。

2. 时间戳相对值(41 bit): 表示当前时间与基准时间的差值,可以支持 69 年的时间范围(2^41 / (1000 * 60 * 60 * 24 * 365))。

3. 数据中心 ID(5 bit): 用于标识数据中心,可以支持最多 2^5 = 32 个数据中心。

4. 机器 ID(5 bit): 用于标识每个数据中心中的机器,可以支持最多 2^5 = 32 台机器。

5. 序列号(12 bit): 表示同一毫秒内生成的序号,可以支持每毫秒生成 2^12 = 4096 个。

符号位时间间戳相对值数据中心 ID机器 ID序列号
00000000000 0000000000 0000000000 0000000000 00000000000000000000000

注意事项

  1. 在一个节点里,由于机器 ID 相同,如果创建多个 SnowflakeIdGenerator 实例,会生成相同 Id,所以 SnowflakeIdGenerator 应该设计成单例。
  2. 在多个节点里,如果使用相同的机器 ID,也可能会生成相同 Id,所以要确保每个节点分配的机器 ID 是唯一的。
  3. 系统时间回退,可能导致生成重复 ID,也就是当前系统时间,小于上一次生成 Id 的时间,可以拒绝生成,抛出异常,也可以使用额外的时间偏移量进行补偿。

机器 ID 分配策略

  1. 保证唯一性:避免分配重复的机器 ID。
  2. 自动化分配:通过动态机制分配机器 ID ,减少人工干预。
  3. 兼容性:支持不同的运行环境(容器、虚拟机、本地多实例)。

常见分配策略

  1. 中心化分配: 使用集中式的配置管理或注册服务分配机器 ID,如 ZooKeeper、Consul 等。
  2. 去中心化分配: 从环境或系统信息推导出唯一机器 ID ,如利用 IP、MAC 地址、容器 hostname、进程 ID 等计算出机器 ID。

容器部署:基于容器 ID 、hostname 生成。

虚拟机部署:基于 IP 地址、MAC 地址生成。

本地多实例:基于进程 ID、端口号生成。

ID 长度问题

已知 long 的最大值为 2^63 - 1 = 9223372036854775807,十进制长度为 19 位。

也就是说,随着时间推移,当前时间与基准时间的差值越来也大,雪花 ID 的长度也会变长。

Java 实现

本地简单代码示例,基准时间为 2024-01-01, 默认使用进程 ID 当做机器 ID,系统时间回退时,拒绝生成,并抛出异常。

import java.lang.management.ManagementFactory;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 雪花算法
 *
 * @author sheng
 */
public class Snowflake {

	public static final Snowflake INSTANCE = new Snowflake(getMachineId());

	// 自定义时间戳基准 2024 年 1 月 1 日)
	private static final long EPOCH = 1704067200000L;
	// 默认机器 ID
	private static final long DEFAULT_MACHINE_ID = 1L;

	// 时间戳位数
	private static final long TIMESTAMP_BITS = 41L;
	// 机器 ID 位数
	private static final long MACHINE_ID_BITS = 10L;
	// 递增序列号位数
	private static final long SEQUENCE_BITS = 12L;

	// 机器 ID 左移位数
	private static final long MACHINE_ID_LEFT_SHIFT = SEQUENCE_BITS;
	// 时间戳左移位数
	private static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + MACHINE_ID_BITS;

	// 最大时间戳
	private static final long MAX_TIMESTAMP = (1L << TIMESTAMP_BITS) - 1;
	// 最大机器 ID
	private static final long MAX_MACHINE_ID = (1 << MACHINE_ID_BITS) - 1;
	// 最大序列号
	private static final long MAX_SEQUENCE = (1 << SEQUENCE_BITS) - 1;

	private final long machineId; // 机器 ID
	private long lastTimestamp = -1L;  // 上次生成 ID 的时间戳
	private long sequence = 0L;   // 递增序列号

	private final ReentrantLock lock = new ReentrantLock();

	// 私有化构造方法
	private Snowflake(long machineId) {
		// 校验机器 ID
		if (machineId < 0 || machineId > MAX_MACHINE_ID) {
			throw new IllegalArgumentException(
				String.format("MachineId 必须在 0 - %d 之间", MAX_MACHINE_ID));
		}
		this.machineId = machineId;
	}

	/**
	 * 生成唯一 ID
	 */
	public long nextId() {
		lock.lock();
		try {
			long timestamp = currentTimeMillis();
			// 检查时间是否回拨
			if (timestamp < lastTimestamp) {
				throw new RuntimeException("系统时钟回退,拒绝生成 ID");
			}
			// 检查时间戳是否溢出
			long relativeTimestamp = timestamp - EPOCH;
			if (relativeTimestamp > MAX_TIMESTAMP) {
				throw new RuntimeException("时间戳超出可用范围");
			}
			// 同一毫秒内递增序列号
			if (timestamp == lastTimestamp) {
				sequence = (sequence + 1) & MAX_SEQUENCE;
				// 序列号溢出,等待下一毫秒
				if (sequence == 0) {
					timestamp = waitForNextMillis(lastTimestamp);
				}
			} else {
				sequence = 0L;
			}

			lastTimestamp = timestamp;
			// 生成雪花算法 ID
			return  (relativeTimestamp << TIMESTAMP_LEFT_SHIFT)
				| (machineId << MACHINE_ID_LEFT_SHIFT)
				| sequence;
		} finally {
			lock.unlock();
		}
	}

	/**
	 * 获取当前时间戳(毫秒)
	 */
	private long currentTimeMillis() {
		return System.currentTimeMillis();
	}

	/**
	 * 等待下一毫秒
	 */
	private long waitForNextMillis(long lastTimestamp) {
		long timestamp = currentTimeMillis();
		while (timestamp <= lastTimestamp) {
			timestamp = currentTimeMillis();
		}
		return timestamp;
	}

	/**
	 * 机器 ID
	 * 使用进程 ID
	 */
	private static long getMachineId() {
		try {
			// 格式:<pid>@<hostname>
			String name = ManagementFactory.getRuntimeMXBean().getName();
			String pid = name.split("@")[0];
			// 保留后十位
			return Long.parseLong(pid) & MAX_MACHINE_ID;
		} catch (Exception e) {
			throw new RuntimeException("获取机器 ID 失败", e);
		}
	}


}

测试

/**
 * @author sheng
 */
public class SnowflakeIdTest {

	public static void main(String[] args) {
		Snowflake snowflake = Snowflake.INSTANCE;
		for (int i = 0; i < 100; i++) {
			System.out.println(snowflake.nextId());
		}
	}

}

结果

130472881724493824
130472881728688128
130472881728688129
130472881728688130
130472881728688131
130472881728688132
130472881728688133