面对自增主键与UUID产生的问题,有没有解决方案呢?答案是有的,即 Snowflake(雪花算法)。
雪花算法(Snowflake)是 Twitter 公司分布式项目采用的 ID 生成算法。他呢主要是根据时间顺序,结合机器ID与序列,生成一个定长的数字。
这个数字就可以作为数据库的主键,他既可以保证是连续的也可以保证在分布式系统中是唯一的。Snowflake 每毫秒可以生成 416 万个 ID,这对于我们在分布式系统中使用的场景已经足够了。
该算法生成的是一个64位的ID,故在Java下正好可以通过8字节的long类型存放。所生成的ID结构如下所示:
符号位
最高位是符号位,为保证生成的ID是正数,故不使用,其值恒为0
时间戳
用来记录时间戳的毫秒数。一般地,我们会选用系统上线的时间作为时间戳的相对起点,而不使用JDK默认的时间戳起点(1970-01-01 00:00:00)。41位长度的时间戳可以保证使用69年。对于一般的项目而言,这个时间长度绝对是够用了
时间戳
用来记录时间戳的毫秒数。一般地,我们会选用系统上线的时间作为时间戳的相对起点,而不使用JDK默认的时间戳起点(1970-01-01 00:00:00)。41位长度的时间戳可以保证使用69年。对于一般的项目而言,这个时间长度绝对是够用了
序列号
最低的12位为序列号,可用于标识、区分同一个计算机在相同毫秒时间内的生产的ID
综上所述,Snowflake 雪花算法生成的ID不是随机的,而是按时间顺序升序排列的;且可以保证在分布式高并发环境下生成的ID不会发生重复
Java实现:
/**
* Snowflake 基于雪花算法的ID生成器
*/
public class SnowflakeIdGenerator {
/**
* ID中41位时间戳的起点 (2020-01-01 00:00:00.00)
* @apiNote 一般地,选用系统上线的时间
*/
private final long startPoint = 1577808000000L;
/**
* 序列号位数
*/
private final long sequenceBits = 12L;
/**
* 机器ID位数
*/
private final long workerIdBits = 5L;
/**
* 数据中心ID位数
*/
private final long dataCenterIdBits = 5L;
/**
* 序列号最大值, 4095
* @apiNote 4095 = 0xFFF,其相当于是序列号掩码
*/
private final long sequenceMask = -1L^(-1L<<sequenceBits);
/**
* 机器ID最大值, 31
*/
private final long maxWorkerId = -1L^(-1L<<workerIdBits);
/**
* 数据中心ID最大值, 31
*/
private final long maxDataCenterId = -1L^(-1L<<dataCenterIdBits);
/**
* 机器ID左移位数, 12
*/
private final long workerIdShift = sequenceBits;
/**
* 数据中心ID左移位数, 12+5
*/
private final long dataCenterIdShift = sequenceBits + workerIdBits;
/**
* 时间戳左移位数, 12+5+5
*/
private final long timeStampShift = sequenceBits + workerIdBits + dataCenterIdBits;
/**
* 数据中心ID, Value Range: [0,31]
*/
private long dataCenterId;
/**
* 机器ID, Value Range: [0,31]
*/
private long workerId;
/**
* 相同毫秒内的序列号, Value Range: [0,4095]
*/
private long sequence = 0L;
/**
* 上一个生成ID的时间戳
*/
private long lastTimeStamp = -1L;
/**
* 构造器
* @param dataCenterId 数据中心ID
* @param workerId 机器中心ID
*/
public SnowflakeIdGenerator(Long dataCenterId, Long workerId) {
if(dataCenterId==null || dataCenterId<0 || dataCenterId>maxDataCenterId
|| workerId==null || workerId<0 || workerId>maxWorkerId) {
throw new IllegalArgumentException("输入参数错误");
}
this.dataCenterId = dataCenterId;
this.workerId = workerId;
}
/**
* 获取ID
* @return
*/
public synchronized long nextId() {
long currentTimeStamp = System.currentTimeMillis();
//当前时间小于上一次生成ID的时间戳,系统时钟被回拨
if( currentTimeStamp < lastTimeStamp ) {
throw new RuntimeException("系统时钟被回拨");
}
// 当前时间等于上一次生成ID的时间戳,则通过序列号来区分
if( currentTimeStamp == lastTimeStamp ) {
// 通过序列号掩码实现只取 (sequence+1) 的低12位结果,其余位全部清零
sequence = (sequence + 1) & sequenceMask;
if(sequence == 0) { // 该时间戳下的序列号已经溢出
// 阻塞等待下一个毫秒,并获取新的时间戳
currentTimeStamp = getNextMs(lastTimeStamp);
}
} else { // 当前时间大于上一次生成ID的时间戳,重置序列号
sequence = 0;
}
// 更新上次时间戳信息
lastTimeStamp = currentTimeStamp;
// 生成此次ID
long nextId = ((currentTimeStamp-startPoint) << timeStampShift)
| (dataCenterId << dataCenterIdShift)
| (workerId << workerIdShift)
| sequence;
return nextId;
}
/**
* 阻塞等待,直到获取新的时间戳(下一个毫秒)
* @param lastTimeStamp
* @return
*/
private long getNextMs(long lastTimeStamp) {
long timeStamp = System.currentTimeMillis();
while(timeStamp<=lastTimeStamp) {
timeStamp = System.currentTimeMillis();
}
return timeStamp;
}
}