雪花算法(Snowflake)是 Twitter 开源的分布式 ID 生成算法,用于在分布式系统中生成全局唯一且趋势递增的 ID。以下是对其简介、优缺点、原理及代码实现的详细解析:

一、雪花算法简介

        据国家大气研究中心的查尔斯·奈特称,一般的雪花大约由10^19个水分子组成。在雪花形成过程中,会形成不同的结构分支,所以说大自然中不存在两片完全一样的雪花,每一片雪花都拥有自己漂亮独特的形状。雪花算法表示生成的id如雪花般独一无二。

        snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID。其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0。

二、雪花算法的优缺点

优点
  1. 全局唯一性:通过数据中心 ID 和机器 ID 确保不同节点生成的 ID 不重复。
  2. 趋势递增:生成的 ID 按时间有序递增,有利于数据库索引优化。
  3. 高性能:本地生成,无需依赖数据库或分布式协调服务,QPS 理论值约为 409.6 万 / 秒。
  4. 灵活可配置:可根据业务需求调整各部分位数(如增加机器 ID 位数、减少序列号位数)。
缺点
  1. 依赖系统时钟:若服务器时钟回拨,可能导致生成重复 ID(需额外处理)。
  2. ID 位数限制:64 位 ID 的各部分长度固定,需提前规划各字段的分配。
  3. 单节点生成上限:每毫秒最多生成 4096 个 ID,高并发场景可能需多节点扩展。

三、雪花算法原理

1)1位保留 (基本不用)

1位标识:由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0,所以这第一位都是0。

2)41位时间戳 

接下来 41 位存储毫秒级时间戳,41位可以表示2^41-1个毫秒的值,转化成单位年则是:(2^41−1)/(1000∗60∗60∗24∗365)=69年 。

41位时间戳 :也就是说这个时间戳可以使用69年不重复,大概可以使用 69 年。

注意:41位时间截不是存储当前时间的时间截,而是存储时间截的差值“当前时间截 – 开始时间截”得到的值。

这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的,一般设置好后就不要去改变了,切记!!!

因为,雪花算法有如下缺点:依赖服务器时间,服务器时钟回拨时可能会生成重复 id。

3)10位机器

        10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId,最多可以部署 2^10=1024 台机器。这里的5位可以表示的最大正整数是2^5−1=31,即可以用0、1、2、3、….31这32个数字,来表示不同的datecenterId(数据中心ID),或workerId(工作机器ID)
 

4) 12bit序列号2

用来记录同毫秒内产生的不同id,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号。

QPS(每秒查询率)的计算基于 “单台机器每秒可生成的最大 ID 数”:

  • 1 秒 = 1000 毫秒
  • 每毫秒最多生成 4096 个 ID
  • 单台机器理论 QPS = 4096×1000=4,096,000(即 409.6 万 / 秒)。

这一数值是单节点的极限性能,实际中受系统处理能力(如 CPU、内存)限制,可能无法达到,但足以支撑绝大多数高并发场景(例如秒杀、峰值订单处理等)。

四、雪花算法代码实现(java)

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

五、注意事项

  1. 机器 ID 分配:需确保每个节点的 datacenter_id 和 worker_id 组合唯一,可通过配置文件或分布式协调服务(如 ZooKeeper)分配。
  2. 时钟同步:建议所有服务器通过 NTP 服务定期同步时间,减少时钟回拨风险。
  3. 性能优化:高并发场景下,可通过预分配多个 Snowflake 实例(每个线程一个)减少锁竞争。

更多推荐