在 Spring Boot 中使用雪花算法(Snowflake)来生成分布式唯一 ID 是一种常见的实践。雪花算法由 Twitter 提出,由于其高效性和分布式环境中的唯一性,广泛应用于各种场景中。
雪花算法
雪花算法生成的 64 位长整型 ID 一般由以下几部分组成:
- 时间戳:通常占用 41 位,用来存储毫秒级的时间戳。
- 数据中心 ID:通常占用若干位,用来标识数据中心。
- 机器 ID:通常占用若干位,用来标识同一数据中心内的不同机器。
- 序列号:通常占用 12 位,用来记录同一毫秒内产生的不同 ID。
实现步骤
-
引入依赖
在你的
pom.xml文件中添加必要的依赖,例如常用的依赖没有特别指定,可以使用 Spring Boot 的核心依赖。 -
编写 Snowflake 算法类
你可以自己实现一个简单版本的雪花算法,也可以使用已有的库。以下是一个简单的 Java 实现示例:
public class SnowflakeIdGenerator { private final long twepoch = 1288834974657L; private final long workerIdBits = 5L; private final long datacenterIdBits = 5L; private final long maxWorkerId = -1L ^ (-1L << workerIdBits); private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); private final long sequenceBits = 12L; private final long workerIdShift = sequenceBits; private final long datacenterIdShift = sequenceBits + workerIdBits; private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; private final long sequenceMask = -1L ^ (-1L << sequenceBits); private long workerId; private long datacenterId; private long sequence = 0L; private long lastTimestamp = -1L; public SnowflakeIdGenerator(long workerId, long datacenterId) { if (workerId > maxWorkerId || workerId < 0) { throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId)); } if (datacenterId > maxDatacenterId || datacenterId < 0) { throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId)); } this.workerId = workerId; this.datacenterId = datacenterId; } public synchronized long nextId() { long timestamp = timeGen(); if (timestamp < lastTimestamp) { throw new RuntimeException( String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); } if (lastTimestamp == timestamp) { sequence = (sequence + 1) & sequenceMask; if (sequence == 0) { timestamp = tilNextMillis(lastTimestamp); } } else { sequence = 0L; } lastTimestamp = timestamp; return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift) | (workerId << workerIdShift) | sequence; } protected long tilNextMillis(long lastTimestamp) { long timestamp = timeGen(); while (timestamp <= lastTimestamp) { timestamp = timeGen(); } return timestamp; } protected long timeGen() { return System.currentTimeMillis(); } } -
使用 Snowflake 算法
创建一个 Spring Boot 服务,并使用该服务生成唯一 ID:
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class IdController { private final SnowflakeIdGenerator idGenerator = new SnowflakeIdGenerator(1, 1); @GetMapping("/generate-id") public long generateId() { return idGenerator.nextId(); } } -
测试
运行你的 Spring Boot 应用程序,访问
/generate-id端点,即可获取到一个基于雪花算法生成的唯一 ID。
nextId方法解析
详细解释
-
同步关键字(synchronized)
- 确保在多线程环境下同一时间只有一个线程能执行此方法,防止生成重复的 ID。
-
获取当前时间戳
long timestamp = timeGen();- 调用
timeGen()方法获取当前的毫秒级时间戳。
-
时钟回拨校验
if (timestamp < lastTimestamp)- 检查当前时间戳是否小于上一次记录的时间戳。如果是,则说明系统时钟出现了回拨,此时抛出异常以避免生成重复 ID。
-
同一毫秒内的序列号处理
-
if (lastTimestamp == timestamp)- 如果当前时间戳与上次的时间戳相同,则增加序列号以区分在同一毫秒内生成的不同 ID。
sequence = (sequence + 1) & sequenceMask;使用位运算确保序列号不会超过其最大值。if (sequence == 0)当序列号达到最大值后,等待到下一毫秒再继续生成新的 ID。
-
-
更新最后时间戳
lastTimestamp = timestamp;- 更新记录的最后时间戳为当前时间戳。
-
生成唯一 ID
-
return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift) | (workerId << workerIdShift) | sequence; -
将不同字段的信息(时间戳、数据中心 ID、机器 ID、序列号)通过位移和位或运算组合成一个 64 位的长整型 ID。
(timestamp - twepoch) << timestampLeftShift)计算自定义纪元后的时间差,并左移到对应的位置。(datacenterId << datacenterIdShift)和(workerId << workerIdShift)将数据中心 ID 和机器 ID 移到它们各自的位置。| sequence将序列号放置在最低有效位。
-
小结
nextId 方法利用时间戳、数据中心 ID、机器 ID 和序列号来生成全局唯一的 ID,通过位移操作将这些元素组合成一个长整型数。该方法确保即使在高并发环境中,生成的 ID 也具有唯一性和顺序性。在实际应用中,这一方法能高效支持分布式系统中的唯一标识需求。
思考题:雪花模型一毫秒内最多生成多少个id
由于序列号占用 12 位,这表示在同一个节点上,同一毫秒可以生成的 ID 数量最多为 (2^{12} = 4096) 个。
因此,如果你的系统有多台机器,每台机器都能在每毫秒生成最多 4096 个唯一 ID,那么总体上你可以通过增加机器数量来扩大每毫秒可以生成的 ID 总量。不过,单台机器在单毫秒内直接生成的 ID 数量上限就是 4096 个。