背景

分布式高并发的环境下,最常见的就是每年双十一的十二点,大量用户同时抢购同一商品,毫秒级的时间下可能生成数万个订单,此时确保生成订单ID的唯一性变得至关重要。此外,在秒杀环境下,不仅要保障ID唯一性、还得确保ID生成的优先度,先抢购到的要优先创建。

原理

雪花算法(snowflake)最早是twitter内部使用分布式环境下的唯一ID生成算法。

雪花算法使用64位long类型的数据存储id

0 - 0000000000 0000000000 0000000000 0000000000 0 - 0000000000 - 000000000000

符号位                     时间戳                                                         机器码                序列号

最高位表示符号位,其中0代表整数,1代表负数,而id一般都是正数,所以最高位为0。

41位存储毫秒级时间戳,这个时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截) * 得到的值),这里的的开始时间截,一般是我们的ID生成器开始使用的时间,一般为项目创建时间,就是下面实现中代码的twepoch 属性,生成器根据时间戳插值进行初次尝试创建ID。

10位存储机器码,最多支持1024台机器,当并发量非常高,同时有多个请求在同一毫秒到达,可以根据机器码进行第二次生成。机器码可以根据实际需求进行二次划分,比如两个机房操作可以一个机房分配5位机器码。

12位存储序列号,当同一毫秒有多个请求访问到了同一台机器后,此时序列号就派上了用场,为这些请求进行第三次创建,最多每毫秒每台机器产生2的12次方也就是4096个id,满足了大部分场景的需求。

总的来说雪花算法有以下几个优点:

  • 能满足高并发分布式系统环境下ID不重复
  • 基于时间戳,可以保证基本有序递增
  • 不依赖第三方的库或者中间件
  • 生成效率极高

ps:在Web开发中需要跟js打交道,而js支持最大的整型范围为53位,超过这个范围就会丢失精度,53之内可以直接由js读取,超过53位就需要转换成字符串才能保证js处理正确。53位存储的话,32位存储秒级时间戳,5位存储机器码,16位存储序列化,这样每台机器每秒可以生产65536个不重复的id。

实现

雪花算法的实现严重依赖时间,因此如果发现时间回退需要抛出异常。

package com.example.springbootmybatis.controller;

/**
* Description :
* Created by Resumebb
* Date :2022/4/12
*/
public class SnowflakeIdWorker{
/** 开始时间截 (这个用自己业务系统上线的时间) */
private final long twepoch = 1575365018000L;

/** 机器id所占的位数 */
private final long workerIdBits = 10L;

/** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);

/** 序列在id中占的位数 */
private final long sequenceBits = 12L;

/** 机器ID向左移12位 */
private final long workerIdShift = sequenceBits;

/** 时间截向左移22位(10+12) */
private final long timestampLeftShift = sequenceBits + workerIdBits;

/** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
private final long sequenceMask = -1L ^ (-1L << sequenceBits);

/** 工作机器ID(0~1024) */
private long workerId;

/** 毫秒内序列(0~4095) */
private long sequence = 0L;

/** 上次生成ID的时间截 */
private long lastTimestamp = -1L;

//==============================Constructors=====================================
/**
* 构造函数
* @param workerId 工作ID (0~1024)
*/
public SnowMaker(long workerId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("workerId can't be greater than %d or less than 0", maxWorkerId));
}
this.workerId = workerId;
}

// ==============================Methods==========================================
/**
* 获得下一个ID (该方法是线程安全的)
* @return SnowflakeId
*/
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 {
sequence = 0L;
}

//上次生成ID的时间截
lastTimestamp = timestamp;

//移位并通过或运算拼到一起组成64位的ID
return ((timestamp - twepoch) << timestampLeftShift) //
| (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();
}
}

测试:

class SnowMakerTest {
@Test
void tilNextMillis() {
SnowflakeIdWorker snowMaker = new SnowflakeIdWorker(0);
for (int i = 0; i < 100; i++) {
long id = snowMaker.nextId();
System.out.println(id);
}
}
}

雪花算法原理及实现_雪花算法