一、引言
在分布式系统中,生成全局唯一ID是一个常见的需求。传统的UUID(通用唯一识别码)虽然能够生成全局唯一的ID,但由于其无序性,在数据库插入数据时会导致频繁的页分裂,从而影响性能。雪花算法(Snowflake Algorithm)由Twitter开源,用于生成全局唯一的64位ID,具有高性能、有序性、可扩展性等特点。
二、雪花算法的整体设计
雪花算法生成的64位ID由以下几部分组成:
-
第一位(符号位):由于ID都是正整数,所以第一位始终为0。
-
时间戳(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冲突问题。
### 7. 分布式环境中的部署
在分布式环境中,每个工作机器都需要运行一个雪花算法实例,并分配一个唯一的工作机器id和数据中心id。这样,每个实例都可以独立地生成全局唯一的ID,并且不同实例生成的ID之间不会发生冲突。
实现算法
以下是一个简化版的雪花算法实现,使用Java语言编写。这个版本省略了一些边缘情况的详细处理(如时钟回拨的复杂处理),但展示了算法的基本结构和位运算操作。
public class SnowflakeIdWorker {
// 起始的时间戳(毫秒),通常是系统上线的时间
private long twepoch = 1609459200000L; // 示例:2021-01-01 00:00:00
// 机器ID所占的位数
private final long workerIdBits = 5L;
// 数据中心ID所占的位数
private final long datacenterIdBits = 5L;
// 最大机器ID
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
// 最大数据中心ID
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
// 序列号所占的位数
private final long sequenceBits = 12L;
// 机器ID左移位数
private final long workerIdShift = sequenceBits;
// 数据中心ID左移位数
private final long datacenterIdShift = sequenceBits + workerIdBits;
// 时间截左移位数
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
// 序列号掩码
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
// 上次生成ID的时间戳
private long lastTimestamp = -1L;
// 同一毫秒内的序列号
private long sequence = 0L;
// 工作机器ID和数据中心ID
private final long workerId;
private final long datacenterId;
/**
* 构造函数,初始化工作机器ID和数据中心ID
*
* @param workerId 工作机器ID
* @param datacenterId 数据中心ID
*/
public SnowflakeIdWorker(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;
}
/**
* 生成下一个ID
*
* @return 下一个ID
*/
public synchronized long nextId() {
long timestamp = timeGen();
// 如果当前时间小于上一次ID生成的时间戳,则等待到下一毫秒
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 {
// 如果是新的时间戳,序列号重置为0
sequence = 0L;
}
// 更新上一次ID生成的时间戳
lastTimestamp = timestamp;
// 移位并通过或运算拼到一起组成64位的ID
return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift) | (workerId << workerIdShift) | sequence;
}
/**
* 等待直到下一毫秒
*
* @param lastTimestamp 上次生成ID的时间戳
* @return 下一毫秒的时间戳
*/
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/**
* 获取系统当前毫秒时间戳
*
* @return 当前毫秒时间戳
*/
protected long timeGen() {
return System.currentTimeMillis();
}
public static void main(String[] args) {
// 假设我们有一个工作机器ID为1,数据中心ID为1的环境
long workerId = 1L;
long datacenterId = 1L;
// 创建一个SnowflakeIdWorker实例
SnowflakeIdWorker idWorker = new SnowflakeIdWorker(workerId, datacenterId);
// 生成并打印10个ID作为示例
for (int i = 0; i < 10; i++) {
long id = idWorker.nextId();
System.out.println(id);
}
}
在这个main
方法中,我们首先定义了两个长整型变量workerId
和datacenterId
,它们分别表示工作机器ID和数据中心ID。然后,我们使用这两个ID创建了一个SnowflakeIdWorker
的实例。最后,我们使用一个循环来生成并打印10个ID作为示例。
请注意,这个简化的雪花算法实现没有处理时钟回拨的复杂情况。在实际应用中,如果系统时钟发生回拨,可能会导致ID生成器生成重复的ID。为了解决这个问题,通常需要更复杂的逻辑来检测并处理时钟回拨的情况。此外,这个实现也假设了workerId
和datacenterId
在系统中是唯一的,并且它们的值在创建SnowflakeIdWorker
实例之后不会改变。如果这些假设不成立,那么生成的ID可能会发生冲突。
四、总结
雪花算法通过精心设计的位分配和运算规则,在分布式系统中生成了全局唯一的ID。其有序性使得数据库插入数据时能够减少页分裂,提高性能。同时,雪花算法还具有高性能、可扩展性强等优点,因此在分布式系统中得到了广泛应用。需要注意的是,在实际应用中,需要合理设置起始时间戳、工作机器id和数据中心id等参数,以确保生成的ID满足业务需求。