一、雪花算法简介
雪花算法是一种高效、分布式全局唯一 ID 生成器,能够生成的 ID 具有时间有序、可排序、趋势递增等特点,实现简单,生成的 ID 唯一性和稳定性得到保障,广泛应用于分布式系统中。 在对雪花算法深入了解之前,我们先来看一下一个非常基础的 ID 生成器。
public class IdGenerator { private static Long id = 0L; public static synchronized Long nextId() { return ++id; } }
这个 IdGenerator 是简单的单机 ID 生成器,通过同步保证了 ID 的唯一性。但是,这种做法有两个问题。首先,由于 synchronized 关键字的使用,多线程场景下,性能会大打折扣;其次,由于是单机,可能出现重复 ID,当然这样的概率相对较低,但是在大量生成 ID 的场景中,也无法避免。因此,需要一种分布式的、简单的,高效的可以提升整体系统的性能和稳定性的 ID 生成器。
二、雪花算法详解
雪花算法的核心是一个 64 位的 long 型数字,其中,第 1 个 bit 为不用,接下来的 41 个 bit 用于表示时间戳,最高位为符号位不用,其中高 5 个 bit 是数据中心标识,5 个 bit 是机器标识,低 12 个 bit 是序号。
public class SnowflakeIdGenerator { // 数据中心ID private long dataCenterId; // 机器ID private long machineId; // 序列号 private long sequence = 0L; // 上一次时间戳 private long lastTimestamp = -1L; public SnowflakeIdGenerator(long dataCenterId, long machineId) { if (dataCenterId > MAX_DATACENTER_ID || dataCenterId < 0) { throw new IllegalArgumentException("DatacenterId can't be greater than MAX_DATACENTER_ID or less than 0"); } if (machineId > MAX_MACHINE_ID || machineId < 0) { throw new IllegalArgumentException("MachineId can't be greater than MAX_MACHINE_ID or less than 0"); } this.dataCenterId = dataCenterId; this.machineId = machineId; } public synchronized long nextId() { long currentTimestamp = timeGen(); if (currentTimestamp < lastTimestamp) { throw new RuntimeException("Clock moved backwards. Refusing to generate id"); } if (currentTimestamp == lastTimestamp) { sequence = (sequence + 1) & SEQUENCE_MASK; if (sequence == 0) { currentTimestamp = tilNextMillis(lastTimestamp); } } else { sequence = 0L; } lastTimestamp = currentTimestamp; return ((currentTimestamp - TWEPOCH) << TIMESTAMP_LEFT_SHIFT) | (dataCenterId << DATACENTER_ID_LEFT_SHIFT) | (machineId << MACHINE_ID_LEFT_SHIFT) | sequence; } private long tilNextMillis(long lastTimestamp) { long timestamp = timeGen(); while (timestamp <= lastTimestamp) { timestamp = timeGen(); } return timestamp; } private long timeGen() { return System.currentTimeMillis(); } public static void main(String[] args) { SnowflakeIdGenerator idWorker = new SnowflakeIdGenerator(1, 1); for (int i = 0; i < 1000; i++) { long id = idWorker.nextId(); System.out.println(Long.toBinaryString(id)); System.out.println(id); } } }
这个 SnowflakeIdGenerator 是一个典型的 Java 实现,它具有选取数据中心 ID 和机器 ID 的功能。这个算法的核心是 nextId() 方法,是一个同步方法,主要包括四个部分。
第一部分获取当前时间戳,并检查时间戳是否被改过。
第二部分是与上一个时间戳比较,保证没有时间上的回拨,并计算序列号。
第三部分则是更新时间戳、序号。
第四部分则是拼装成 64 位 long 型 ID。
三、雪花算法优化
在实际应用中,我们可能需要对雪花算法进行迭代和优化,出于不同的业务场景,也许会有不同的优化方式。这里我们提出一些常见的优化方式。
1. 高效的防止时钟回拨策略
有时,时钟回拨是不可避免的。为了消除回拨带来的影响,我们可以采用 NTP 同步系统时钟的方式。但是在极端情况下,毫秒级别的时钟回拨的确会发生。比如说,在云服务器上,有时由于机器迁移或者其他原因时钟可能会回拨,而这种回拨比较微小和短暂,但对于雪花算法而言影响却比较大。因此,为了解决这个问题,我们需要对时钟回拨进行优化。
public synchronized long nextId() { long currentTimestamp = timeGen(); if (currentTimestamp < lastTimestamp) { long offset = lastTimestamp - currentTimestamp; if (offset < MAX_BACKWARD_MS) { try { wait(offset << 1); currentTimestamp = timeGen(); if (currentTimestamp < lastTimestamp) { throw new RuntimeException("Clock moved backwards. Refusing to generate id"); } } catch (Exception e) { throw new RuntimeException(e); } } else { throw new RuntimeException("Clock moved backwards. Refusing to generate id"); } } if (currentTimestamp == lastTimestamp) { sequence = (sequence + 1) & SEQUENCE_MASK; if (sequence == 0) { currentTimestamp = tilNextMillis(lastTimestamp); } } else { sequence = 0L; } lastTimestamp = currentTimestamp; return ((currentTimestamp - TWEPOCH) << TIMESTAMP_LEFT_SHIFT) | (dataCenterId << DATACENTER_ID_LEFT_SHIFT) | (machineId << MACHINE_ID_LEFT_SHIFT) | sequence; }
可以看到,在 nextId() 方法中增加了 wait() 方法,同步等待一段时间,再次获取当前时间戳,如果时间戳依然小于上次时间戳,则抛出异常。这种方式虽然可以满足单数时钟回拨带来的影响,但是多次时钟回拨,会导致异常被抛出。因此,我们基于这个思路,可以采取更加高效的方式求解。
public synchronized long nextId() { long currentTimestamp = timeGen(); if (currentTimestamp < lastTimestamp) { long offset = lastTimestamp - currentTimestamp; if (offset < MAX_BACKWARD_MS) { currentTimestamp = lastTimestamp; } else { throw new RuntimeException("Clock moved backwards. Refusing to generate id for " + offset + " milliseconds"); } } if (currentTimestamp == lastTimestamp) { sequence = (sequence + 1) & SEQUENCE_MASK; if (sequence == 0) { currentTimestamp = tilNextMillis(lastTimestamp); } } else { sequence = 0L; } lastTimestamp = currentTimestamp; return ((currentTimestamp - TWEPOCH) << TIMESTAMP_LEFT_SHIFT) | (dataCenterId << DATACENTER_ID_LEFT_SHIFT) | (machineId << MACHINE_ID_LEFT_SHIFT) | sequence; }
可以看到,只需要将等待的时间减少一半,直接使用 lastTimestamp 替代 currentTimestamp 即可,这样的做法显著提升了程序的性能和安全性。
2. 更为严格的数据安全性
在一些情况下,为了严格保障数据生成的唯一性,我们可能需要将 SnowflakeIdGenerator 的接口单例化,也就是说,在一个 JVM 实例上,每一个工作对象都是唯一的。
public class SnowflakeIdGenerator { private static volatile SnowflakeIdGenerator instance; public static SnowflakeIdGenerator getInstance(int dataCenterId, int machineId) { if (instance == null) { synchronized (SnowflakeIdGenerator.class) { if (instance == null) { instance = new SnowflakeIdGenerator(dataCenterId, machineId); } } } return instance; } }
这样,一旦有重复的机器标识或者数据中心标识,就会抛出运行时异常。
四、小结
雪花算法是一种高效、分布式全局唯一 ID 生成器,能够生成的 ID 具有时间有序、可排序、趋势递增等特点,实现简单,生成的 ID 唯一性和稳定性得到保障,广泛应用于分布式系统中。而在雪花算法的实现过程中,我们也可以根据不同的具体业务场景,进行一些优化和改进。