雪花算法适用于高性能生成ID的场景,类似于高并发生成用户ID,订单ID这样的场景。
雪花算法用在分布式环境中,需要防止ID冲突问题,以下是解决冲突的一个思路。
package com.xbd.bigdata.util.snowflake;
import com.xbd.bigdata.util.IpUtils;
import lombok.extern.slf4j.Slf4j;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
/**
* @Description: 静态内部类单例模式实现雪花算法
* https://www.cnblogs.com/qdhxhz/p/11372658.html
* 参考: Java位运算原理及使用讲解 https://www.cnblogs.com/findbetterme/p/10787118.html
* @Author: usr1999
* @Create: 2021/10/28
*/
@Slf4j
public class SnowflakeIdUtils {
/**
* 私有的 静态内部类
*/
private static class SnowFlake {
/**
* 内部类对象(单例模式)
*/
private static final SnowflakeIdUtils.SnowFlake SNOW_FLAKE = new SnowflakeIdUtils.SnowFlake();
/**
* 起始的时间戳
*/
private final long START_TIMESTAMP = 1635264000000L;
/**
* 序列号占用位数
*/
private final long SEQUENCE_BIT = 12;
/**
* 机器标识占用位数
*/
private final long MACHINE_BIT = 10;
/**
* 时间戳位移位数
*/
private final long TIMESTAMP_LEFT = SEQUENCE_BIT + MACHINE_BIT;
/**
* 最大序列号(4095)
*/
private final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BIT);
/**
* 最大机器编号(1023)
*/
private final long MAX_MACHINE_ID = ~(-1L << MACHINE_BIT);
/**
* 生成id机器标识部分
*/
private long machineIdPart;
/**
* 序列号
*/
private long sequence = 0L;
/**
* 上一次时间戳
*/
private long lastTimestamp = -1L;
/**
* 最大容忍时间, 单位毫秒, 即如果时钟只是回拨了该变量指定的时间, 那么等待相应的时间即可;
* 考虑到sequence服务的高性能, 这个值不易过大
*/
private static final long MAX_BACKWARD_MS = 5;
/**
* 构造函数初始化机器编码
*/
private SnowFlake() {
// 获得本机机器编码
long localIp;
String address = null;
try {
InetAddress addr = IpUtils.getLocalHostLANAddress();
address = addr.getHostAddress();
localIp = IpUtils.ipToLong(address);
} catch (UnknownHostException e) {
log.error("获取本机IP异常", e);
localIp = System.currentTimeMillis();
}
// 机器标识: localIp & MAX_MACHINE_ID最大不会超过1023, 再左位移12位
machineIdPart = (localIp & MAX_MACHINE_ID) << SEQUENCE_BIT;
log.info("Snowflake初始化, address={}, localIp={}, machineIdPart={}", address, localIp, machineIdPart);
}
/**
* 获取雪花ID
*/
public synchronized long nextId() {
long currentTimestamp = timeGen();
// 闰秒:如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if (currentTimestamp < lastTimestamp) {
long offset = lastTimestamp - currentTimestamp;
if (offset <= MAX_BACKWARD_MS) {
try {
this.wait(offset << 1);
currentTimestamp = timeGen();
if (currentTimestamp < lastTimestamp) {
log.error("时钟回拔, 再次获取系统时间依然小于上一次时间戳, currentTimestamp={}, lastTimestamp={}",
currentTimestamp, lastTimestamp);
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", offset));
}
} catch (Exception e) {
log.error("时钟回拔, 生成SnowflakeID异常", e);
throw new RuntimeException(e);
}
} else {
log.error("时钟回拔, 当前时间和上一次时间戳的差大于5ms, currentTimestamp={}, lastTimestamp={}",
currentTimestamp, lastTimestamp);
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", offset));
}
}
// 避免机器时钟回拨
/*while (currentStamp < lastStamp) {
//服务器时钟被调整了,ID生成器停止服务.
throw new RuntimeException(String.format("时钟已经回拨. Refusing to generate id for %d milliseconds", lastStamp - currentStamp));
}*/
// 同一毫秒内, 序号+1
if (currentTimestamp == lastTimestamp) {
sequence = (sequence + 1) & MAX_SEQUENCE;
// 同一毫秒内序列号溢出
if (sequence == 0) {
// 阻塞到下一个毫秒,获得新的时间戳
currentTimestamp = getNextMill();
}
} else {
// 不同毫秒内,序号从0开始
sequence = 0L;
}
lastTimestamp = currentTimestamp;
// 时间戳部分+机器标识部分+序列号部分 (不同位表示不同的指标数)
return (currentTimestamp - START_TIMESTAMP) << TIMESTAMP_LEFT | machineIdPart | sequence;
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
*/
private long getNextMill() {
long mill = timeGen();
while (mill <= lastTimestamp) {
mill = timeGen();
}
return mill;
}
/**
* 返回以毫秒为单位的当前时间
*/
protected long timeGen() {
return System.currentTimeMillis();
}
}
/**
* 获取long类型雪花ID
*/
public static long uniqueLong() {
return SnowflakeIdUtils.SnowFlake.SNOW_FLAKE.nextId();
}
/**
* 获取String类型雪花ID
*/
public static String uniqueLongHex() {
return String.format("%016x", uniqueLong());
}
/**
* 测试
*/
public static void main(String[] args) throws InterruptedException {
//计时开始时间
long start = System.currentTimeMillis();
//让100个线程同时进行
final CountDownLatch latch = new CountDownLatch(100);
//判断生成的20万条记录是否有重复记录
final Map<Long, Integer> map = new ConcurrentHashMap();
for (int i = 0; i < 100; i++) {
//创建100个线程
new Thread(() -> {
for (int s = 0; s < 2000; s++) {
long snowID = SnowflakeIdUtils.uniqueLong();
log.info("生成雪花ID={}", snowID);
Integer put = map.put(snowID, 1);
if (put != null) {
log.error("主键重复snowID={}", snowID);
throw new RuntimeException("主键重复");
}
}
latch.countDown();
}).start();
}
//让上面100个线程执行结束后,在走下面输出信息
latch.await();
log.info("生成20万条雪花ID总用时={}", System.currentTimeMillis() - start);
}
}
主要优化部分是每个机器节点生成机器标识部分,这里使用IP做为生成机器标识编码的计算因子,先获取不同节点的服务器IP,如果获取IP抛出异常则取当前时间的毫秒数做为IP的long类型数字,目的是保证每个服务器的IP参数不一致。