一、雪花算法的使用
1.1 引入Hutool依赖包
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.4</version>
</dependency>
1.2 使用Spring管理Snowflake
保证在项目启动之前,初始化好hutool工具类;
package com.conrurrency.config;
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;
import org.springframework.context.annotation.*;
@Configuration
public class SnowflakeConfig {
/**
* 默认返回单例的Snowflake对象,集群情况下不会出现分布式ID重复的情况
* @return snowflake对象
*/
@Bean
public Snowflake snowflake() {
return IdUtil.getSnowflake();
}
}
1.3 雪花算法的使用
1.3.1 生成全局唯一ID
package com.conrurrency.service;
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.*;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.util.*;
@SpringBootTest
@Slf4j
public class SnowflakeTest {
@Resource
private Snowflake snowflake;
/**
* 生成10000个全局唯一ID,并判断是否有重复的
*/
@Test
public void test() {
Set<String> set = new HashSet<>();
boolean repeat = false;
for (int i = 0; i < 10000; i++) {
String id = snowflake.nextIdStr();
log.info(id);
if (!set.contains(id)) {
set.add(id);
} else {
repeat = true;
}
}
Assertions.assertFalse(repeat);
}
}
1.3.2 根据全局唯一ID获取dataCenterId和workerId
package com.conrurrency.service;
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.*;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.util.*;
@SpringBootTest
@Slf4j
public class SnowflakeTest {
public static void main(String[] args) {
Snowflake snowflake = IdUtil.getSnowflake();
// 根据全局唯一ID获取dataCenterId
long dataCenterId = snowflake.getDataCenterId(1546029975069294619L);
// 根据全局唯一ID获取workerId
long workerId = snowflake.getWorkerId(1546029975069294619L);
// 14
System.out.println(dataCenterId);
// 8
System.out.println(workerId);
}
}
二、分布式场景下,如何保证雪花算法唯一
2.1 保证分布式系统中各个节点的标识位不同
因为雪花算法由41bit的时间戳、5bit的dataCenterId、5bit的workerId、12bit的序列号组成。
5bit的dataCenterId、5bit的workerId的可以称为机器的标识位。
在不出现时钟回拨的情况下,只要机器的标识位相同,生成的雪花算法一定唯一。
我们可以看下Hutool工具类是如何保证dataCenterId和workerId唯一的?
cn.hutool.core.IdUtil类中有一个静态方法getSnowflake(),即获取Snowflake对象的静态方法,该方法使用反射创建一个单例的Snowflake对象,即调用该方法的话会默认初始化Snowflake的无参构造方法;
package cn.hutool.core.util;
public class IdUtil {
public static Snowflake getSnowflake() {
return Singleton.get(Snowflake.class);
}
}
下面我们看下Snowflake的所有构造方法。
package cn.hutool.core.lang;
public class Snowflake implements Serializable {
private static final long DATA_CENTER_ID_BITS = 5L;
// 最大支持数据中心节点数0~31,一共32个
private static final long MAX_DATA_CENTER_ID = -1L ^ (-1L << DATA_CENTER_ID_BITS);
/**
* 无参构造,使用自动生成的工作节点ID和数据中心ID
* MAX_DATA_CENTER_ID为31
* MAX_WORKER_ID为31
*/
public Snowflake() {
this(IdUtil.getWorkerId(IdUtil.getDataCenterId(MAX_DATA_CENTER_ID), MAX_WORKER_ID));
}
/**
* 构造
*
* @param workerId 终端ID
*/
public Snowflake(long workerId) {
this(workerId, IdUtil.getDataCenterId(MAX_DATA_CENTER_ID));
}
/**
* 构造
*
* @param workerId 终端ID
* @param dataCenterId 数据中心ID
*/
public Snowflake(long workerId, long dataCenterId) {
this(workerId, dataCenterId, false);
}
/**
* 构造
*
* @param workerId 终端ID
* @param dataCenterId 数据中心ID
* @param isUseSystemClock 是否使用{@link SystemClock} 获取当前时间戳
*/
public Snowflake(long workerId, long dataCenterId, boolean isUseSystemClock) {
this(null, workerId, dataCenterId, isUseSystemClock);
}
}
Snowflake的无参构造方法,默认会使用自动生成的工作节点ID和数据中心ID。
下面可以研究下Hutool是如何生成wokerId和dataCenterId的?
因为生成wokerId需要依赖dataCenterId,所以先看下如何生成dataCenterId的。
初始化Snowflake对象时,会调用IdUtil.getDataCenterId(MAX_DATA_CENTER_ID)生成dataCenterId,MAX_DATA_CENTER_ID为31,下面是IdUtil.getDataCenterId的源码。
package cn.hutool.core.util;
public class IdUtil {
/**
* 获取数据中心ID<br>
* 数据中心ID依赖于本地网卡MAC地址。
* <p>
* 此算法来自于mybatis-plus#Sequence
* </p>
*
* @param maxDatacenterId 最大的中心ID
* @return 数据中心ID
* @since 5.7.3
*/
public static long getDataCenterId(long maxDatacenterId) {
long id = 1L;
// 获得本机物理mac地址,通常情况下,不同服务器的mac地址是不同的。
final byte[] mac = NetUtil.getLocalHardwareAddress();
if (null != mac) {
id = ((0x000000FF & (long) mac[mac.length - 2])
| (0x0000FF00 & (((long) mac[mac.length - 1]) << 8))) >> 6;
id = id % (maxDatacenterId + 1);
}
return id;
}
}
由上述算法可知,dataCenterId是由服务器的mac地址与32取模得到的,所以dataCenterId在服务器数量少的情况下一定是不同的。
初始化Snowflake对象时,会调用IdUtil.getWorkerId(IdUtil.getDataCenterId(MAX_DATA_CENTER_ID), MAX_WORKER_ID);生成workerId。
下面是IdUtil.getWorkerId方法的源码
package cn.hutool.core.util;
public class IdUtil {
/**
* 获取机器ID,使用进程ID配合数据中心ID生成<br>
* 机器依赖于本进程ID或进程名的Hash值。
*
* <p>
* 此算法来自于mybatis-plus#Sequence
* </p>
*
* @param datacenterId 数据中心ID
* @param maxWorkerId 最大的机器节点ID
* @return ID
* @since 5.7.3
*/
public static long getWorkerId(long datacenterId, long maxWorkerId) {
final StringBuilder mpid = new StringBuilder();
mpid.append(datacenterId);
try {
mpid.append(RuntimeUtil.getPid());
} catch (UtilException igonre) {
//ignore
}
/*
* MAC + PID 的 hashcode 获取16个低位
*/
return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);
}
}
由上述算法可知,workerId是使用进程ID配合数据中心ID生成,由于不同机器的数据中心ID在服务器数量少的情况下,一定是不同的。
通过以上代码分析可以得出一个结论,使用Hutool工具类生成雪花算法时,分布式系统中,雪花算法的值要保证全局唯一,一个必要条件是保证分布式各个节点的mac地址必须不同。
2.2 单节点服务器不允许出现时钟回调的情况
2.3 保证各个节点服务器的时钟一致
2.4 如果出现了时钟回拨的情况,如何解决?(满帮二面当时有问到过这个问题)
/**
* 下一个ID
*
* @return ID
*/
public synchronized long nextId() {
long timestamp = genTime(); // 获取当前时间戳
if (timestamp < this.lastTimestamp) { // b
if(this.lastTimestamp - timestamp < timeOffset) { // c
// 容忍指定的回拨,避免NTP校时造成的异常
timestamp = lastTimestamp;
} else{
// 如果服务器时间有问题(时钟后退) 报错。
throw new IllegalStateException(StrUtil.format("Clock moved backwards. Refusing to generate id for {}ms", lastTimestamp - timestamp));
}
}
if (timestamp == this.lastTimestamp) { // d
final long sequence = (this.sequence + 1) & SEQUENCE_MASK;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
this.sequence = sequence;
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - twepoch) << TIMESTAMP_LEFT_SHIFT)
| (dataCenterId << DATA_CENTER_ID_SHIFT)
| (workerId << WORKER_ID_SHIFT)
| sequence;
}
代码解释
b:如果当前时间戳小于上一次生成的Id的时间戳,则说明发生了时钟回拨。
c:如果发生了时钟回拨,并且
- 发生的时钟回拨在容忍的时间范围(默认是 2s,可以自定义设置)之内,将当前时间戳设置为上一次生成的Id的时间戳,并且打印告警日志。
- 发生的时钟回拨在容忍的时间范围(默认是 2s,可以自定义设置)之外,则直接抛异常;
d:timestamp == this.lastTimestamp 即当前时间戳等于上一次生成的Id的时间戳,说明发生了时钟回拨,并且在容忍的时间范围之内,则设置将雪花算法的后 12 位序列化加一来保证唯一性。