雪花算法
使用场景
用于分布式业务系统,生成一个唯一的业务标识。
数据结构
- 符号位:占1bit,默认符号位为0。
- 时间戳:占41bit,精准到毫秒。
- 机器编码:占10bit,高位 5 bit 是数据中心 ID(集群ID),低位 5 bit 是机器 ID,最多可以容纳 1024 个节点。
- 序列号:占用12bit,当从同一毫秒开始,可以一直累加,最多可以累加到4095,一共可以产生2的12次幂 4096 个ID。
思路
- 1.获取当前时间的毫秒时间戳。
- 2.如果发现当前时间戳和上一次时间相同,说明是同毫秒请求,那么将序列号加1(如果序列号不够用了,就延时到下一个毫秒时间戳)。
- 3.如果发现当前时间戳大于上一次时间,说明是新的毫秒时间请求,那么将序列号置为0。
- 4.更新时间戳。
- 5.通过移位,将时间戳、数据中心、机器的值进行移位,放到指定的范围内,生成分布式ID。
实现
public class SnowFlakeGen {
// ============================= 时间戳、数据中心、机器、序列号的位数 ====================================
/** 开始时间截 (2022-08-10) */
private final long twepoch = 1660129904936L;
/** 数据中心id所占的位数 */
private final long datacenterIdBits = 5L;
/** 机器id所占的位数 */
private final long workerIdBits = 5L;
/** 序列在id中占的位数 */
private final long sequenceBits = 12L;
// ============================= 数据中心、机器最大值 ==========================================
/** 支持的最大机器id值,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
/** 支持的最大数据中心id值,结果是31 */
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
// =================== 机器id向左移位、数据中心id向左移位、时间截向左移位 ======================================
/** 机器id向左移12位 */
private final long workerIdShift = sequenceBits;
/** 数据中心id向左移17位(12+5) */
private final long datacenterIdShift = sequenceBits + workerIdBits;
/** 时间截向左移22位(5+5+12) */
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
// ================================ 序列号掩码 0b11111111111 ============================================
/** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
// ============================= 时间戳、数据中心、机器、序列号的值 ==========================================
/** 工作机器id(0~31) */
private long workerId;
/** 数据中心id(0~31) */
private long datacenterId;
/** 毫秒内序列号(0~4095) */
private long sequence = 0L;
/** 上次生成id的时间截 */
private long lastTimestamp = -1L;
/**
* 构造函数
* @param workerId 机器ID (0~31)
* @param datacenterId 数据中心ID (0~31)
*/
public SnowFlakeGen(long workerId, long datacenterId) {
// 输入的机器id不能超过最大值
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
// 输入的数据中心id不能超过最大值
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 SnowflakeId
*/
public synchronized long genNextId() {
// 获取当前的时间戳
long timestamp = timeGen();
// 如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if (timestamp < lastTimestamp) {
throw new RuntimeException(
String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
long startValue = randGen();
// 如果是同一时间生成的,则进行毫秒内序列号
if (lastTimestamp == timestamp) {
// sequenceMask :(0b111111111111=0xfff=4095)
sequence = (sequence + 1) & sequenceMask;
/**
* 同一毫秒+数据中心+机器:序列号最大只有4096
* 如果超出了,那么就将请求,延时到下一个毫秒
*/
if (sequence == startValue) {
//阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
}
// 时间戳改变,毫秒内序列号重置,从0L开始
else {
sequence = startValue;
}
// 重新设置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();
}
/**
* 生成1-10内的long型随机数
* @return
*/
protected long randGen() {
Random random = new Random();
Integer randValue = random.nextInt(10);
long startValue = randValue.longValue();
return startValue;
}
}
雪花算法缺点
- 时间回拨,会出现重复ID。
- 水平切分分表时,如果采用对当前生成的分布式ID取模的方式,生成的分布式ID的序列号都是从0开始的,这样会造成数据不平均,那么我们对于起始序列号可以采用随机方式,起始序列号范围:0000 ~ 1001(就是0到9)。
/**
* 生成1-10内的long型随机数
* @return
*/
protected long randGen() {
Random random = new Random();
Integer randValue = random.nextInt(10);
long startValue = randValue.longValue();
return startValue;
}
这样就保证了每毫秒的第一个数据足够随机。