雪花算法(JAVA单例不用修改版)

43 阅读7分钟

一、简介

雪花算法(Snowflake Algorithm)是一种用于生成分布式系统中唯一ID的算法。起初由Twitter设计,用于解决分布式系统中唯一ID的需求。这一算法的目标是生成全局唯一、有序的64位整数ID,以确保数据不冲突、不重复。

二、雪花算法组成:

固定值:

1bit,最高位是符号位,0 表示正,1 表示负,固定为 0,如果是 1 就是负数了。

时间戳:

41bit,存储毫秒级时间戳(41 位的长度可以使用 69 年)。

标识位(存储机器码):

10bit,上面中的 机器id(5bit)和 服务id(5bit)统一叫作“标识位”,两个标识位组合起来最多可以支持部署 1024 个节点。

序列号:

12bit,用于表示在同一毫秒内生成的多个ID的序号。如果在同一毫秒内生成的ID超过了4096个(2的12次方),则需要等到下一毫秒再生成ID。

img

三、雪花算法使用中可能存在的问题

1、时钟回拨问题

时钟回拨问题指的是在分布式系统中,服务器的系统时钟发生倒退(即时间被调回到一个较早的时间点),导致系统内时间不连续、时间戳错乱的问题。这在使用时间戳生成唯一ID、排序或者进行协调时,会产生严重的影响。

解决方法:

  • NTP时钟同步:使用网络时间协议(NTP)服务来保持服务器之间的时钟同步,减少时钟回拨的可能性。
  • 等待时钟追赶:当检测到时钟回拨时(本地存时间戳,每次生产ID时比较,小于当前时间则轮训),算法会暂停生成新的ID,直到系统时钟恢复正常。这种方法简单但可能会导致服务暂停,影响用户体验。

2、分布式生成的ID并非绝对随时间递增

由于在分布式中,各个机器的机器ID不同,导致生成的ID并非绝对随时间递增。

3、服役时间

雪花算法(Snowflake)的服役时间大约为69年‌。超过该时间可能会出现重复ID。

四、代码实现

1、SnowflakeIdGenerator(ID生成)

import com.alibaba.fastjson.JSONObject;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Random;

/**
 * 雪花ID 生成
 */
@Slf4j
public class SnowflakeIdGenerator {

    private static volatile SnowflakeIdGenerator snowflakeIdGenerator = null;


    // ==============================字段==============================
    /**
     * 机器ID
     * 唯一性:不支持热更新
     */
    private final long workerId;
    /**
     * 数据中心ID
     * 唯一性:不支持热更新
     */
    private final long datacenterId;
    /**
     * 序列号
     */
    private long sequence = 0L;

    // 配置参数
    private static final long MAX_WORKER_ID = 31L;
    private static final long MAX_DATACENTER_ID = 31L;
    private static final long TIMESTAMP_BITS = 41L;
    private static final long MAX_TIMESTAMP = ~(-1L << TIMESTAMP_BITS);
    private static final long WORKER_ID_BITS = 5L;
    private static final long DATACENTER_ID_BITS = 5L;
    private static final long SEQUENCE_BITS = 12L;

    // 位移偏移量
    private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS;
    private static final long WORKER_ID_SHIFT = SEQUENCE_BITS + DATACENTER_ID_BITS;
    private static final long DATACENTER_ID_SHIFT = SEQUENCE_BITS;

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


    // ==============================构造函数==============================

    /**
     * 构造函数
     *
     * @param workerId     机器ID (0 - 31)
     * @param datacenterId 数据中心ID (0 - 31)
     */
    private SnowflakeIdGenerator(long workerId, long datacenterId) {
        // 检查workerId和datacenterId是否合法
        validateParameters(workerId, datacenterId);

        // 设置workerId和datacenterId
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }


    /**
     * 无参构造方法
     * workerId 为根据IP最后一位取值
     * datacenterId 为 0-31 随即取值
     */
    private SnowflakeIdGenerator() {
        // 获取ip最后一段
        String ip = String.valueOf(getIpLastSegment());
        Random random = new Random();
        // 确保workId在0-31范围内
        int workerId = getIpLastSegment() % 32;
        int datacenterId = random.nextInt(32);
        log.info("SnowflakeIdGenerator 机器【{}】使用默认配置 workerId={} datacenterId={}", ip, workerId, datacenterId);
        // 检查workerId和datacenterId是否合法
        validateParameters(workerId, datacenterId);

        // 设置workerId和datacenterId
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }

    // ==============================单例==============================

    /**
     * 雪花算法 单例模式
     */
    public static SnowflakeIdGenerator getSnowflakeIdGenerator() {
        if (snowflakeIdGenerator == null) {
            synchronized (SnowflakeIdGenerator.class) {
                if (snowflakeIdGenerator == null) {
                    try {
                        // 初始化
                        snowflakeIdGenerator = configurationInit();
                    } catch (Exception e) {
                        log.error("getSnowflakeIdGenerator 获取 配置文件【snowflakeId】失败", e);
                        // 报错 采用默认算法
                        snowflakeIdGenerator = new SnowflakeIdGenerator();
                    }
                }
            }
        }
        return snowflakeIdGenerator;
    }

    // ==============================方法==============================

    /**
     * 获取IP的最后一段
     */
    private static int getIpLastSegment() {
        int ipLastSegment;
        String hostAddress = null;
        try {
            hostAddress = InetAddress.getLocalHost().getHostAddress();
            String[] segments = hostAddress.split("\.");
            if (segments.length < 4) {
                throw new IllegalArgumentException("getIpLastSegment 不是IPV4地址 hostAddress=" + hostAddress);
            }
            ipLastSegment = Integer.parseInt(segments[3]);
        } catch (IllegalArgumentException e) {
            log.error("getIpLastSegment 获取IP 失败", e);
            Random random = new Random();
            ipLastSegment = random.nextInt(256);
            log.error("getIpLastSegment hostAddress={} 获取随即ip={}", hostAddress, ipLastSegment);
        } catch (UnknownHostException e) {
            log.error("getIpLastSegment 获取IP 失败", e);
            Random random = new Random();
            ipLastSegment = random.nextInt(256);
        }
        return ipLastSegment;
    }

    /**
     * 生成唯一ID
     *
     * @return 唯一ID
     */
    public synchronized long generateUniqueId() {
        long timestamp = getCurrentTimestamp();

        // 检查时钟回退情况
        if (timestamp < lastTimestamp) {
            throw new RuntimeException("Clock moved backwards. Refusing to generate ID.");
        }

        // 同一毫秒内自增序列号
        if (timestamp == lastTimestamp) {
            sequence = (sequence + 1) & ((1 << SEQUENCE_BITS) - 1);  // 自增并掩码
            if (sequence == 0) {
                // 序列号用完,等待下一毫秒
                timestamp = getNextTimestamp(lastTimestamp);
            }
        } else {
            sequence = 0L;  // 不同毫秒重置序列号
        }

        lastTimestamp = timestamp;

        // 组合生成唯一ID
        return ((timestamp << TIMESTAMP_SHIFT) |
                (datacenterId << DATACENTER_ID_SHIFT) |
                (workerId << WORKER_ID_SHIFT) |
                sequence);
    }

// 设置2020年为起始点
private static final long EPOCH = 1577836800000L; // 2020-01-01

    /**
     * 获取当前时间戳
     *
     * @return 当前时间戳(毫秒)
     */
    private long getCurrentTimestamp() {
        return System.currentTimeMillis() - EPOCH;  // 相对时间戳
    }

    /**
     * 等待下一毫秒,直到获得新的时间戳
     *
     * @param lastTimestamp 上次生成ID的时间戳
     * @return 新的时间戳
     */
    private long getNextTimestamp(long lastTimestamp) {
        long timestamp = getCurrentTimestamp();
        while (timestamp <= lastTimestamp) {
            timestamp = getCurrentTimestamp();
        }
        return timestamp;
    }

    /**
     * 初始化
     */
    private static SnowflakeIdGenerator configurationInit() {
        // 获取ip最后一段
        String ip = String.valueOf(getIpLastSegment());
        // 配置类似于 { "39": { "workerId": 13, "datacenterId": 13 } }
        String config = SpringBootBeanUtil.getEnvironment().getProperty("snowflakeId.config");
        log.info("configurationInit 机器【{}】的配置 snowflakeId.config={}", ip, config);
        // 没有配置项 用默认的
        if (StringUtils.isBlank(config)) {
            return new SnowflakeIdGenerator();
        }
        JSONObject json = JSONObject.parseObject(config)
                .getJSONObject(ip);
        // 没有配置项 用默认的
        if (json == null) {
            log.info("configurationInit 机器【{}】的 无配置", ip);
            return new SnowflakeIdGenerator();
        }
        // 校验配置参数 如果没问题就使用配置的参数
        Parameter parameter = json.toJavaObject(Parameter.class);
        Integer workerId = parameter.getWorkerId();
        Integer datacenterId = parameter.getDatacenterId();
        log.info("configurationInit 机器【{}】的 workerId={} datacenterId={}", ip, workerId, datacenterId);
        if (workerId == null || datacenterId == null) {
            throw new IllegalArgumentException("configurationInit 机器【" + ip + "】配置有误");
        }
        validateParameters(workerId, datacenterId);
        return new SnowflakeIdGenerator(workerId, datacenterId);
    }


    /**
     * 校验机器ID是否合法
     */
    private static void validateParameters(long workerId, long datacenterId) {
        if (workerId > MAX_WORKER_ID || workerId < 0) {
            throw new IllegalArgumentException("Invalid workerId: " + workerId);
        }
        if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) {
            throw new IllegalArgumentException("Invalid datacenterId: " + datacenterId);
        }
    }

    /**
     * 配置参数
     */
    @Data
    private static class Parameter {
        private Integer workerId;
        private Integer datacenterId;
    }
}

2、SpringBootBeanUtil(用于获取配置文件)

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;

@Component
@SuppressWarnings("unused")
public class SpringBootBeanUtil implements ApplicationContextAware, EnvironmentAware {
    private static final Logger log = LoggerFactory.getLogger(SpringBootBeanUtil.class);

    private static ApplicationContext applicationContext;


    private static Environment environment;

    @Override
    public void setEnvironment(final Environment environment) {
        if (SpringBootBeanUtil.applicationContext == null) {
            SpringBootBeanUtil.environment = environment;
        }
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if (SpringBootBeanUtil.applicationContext == null) {
            SpringBootBeanUtil.applicationContext = applicationContext;
        }
        log.info("========ApplicationContext配置成功========");
        log.info("========在普通类可以通过调用SpringUtils.getAppContext()获取applicationContext对象========");
        log.info("========applicationContext=" + SpringBootBeanUtil.applicationContext + "========");
    }

    /**
     * 获取applicationContext
     *
     * @return ApplicationContext
     */
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    /**
     * 获取applicationContext
     *
     * @return ApplicationContext
     */
    public static Environment getEnvironment() {
        return environment;
    }

    /**
     * 通过name获取 Bean.
     *
     * @param name name
     * @return Object Object
     */
    public static Object getBean(String name) {
        return getApplicationContext().getBean(name);
    }

    /**
     * 通过class获取Bean.
     *
     * @param clazz clazz
     * @return T
     */
    public static <T> T getBean(Class<T> clazz) {
        return getApplicationContext().getBean(clazz);
    }

    /**
     * 通过name,以及Clazz返回指定的Bean
     *
     * @param name  name
     * @param clazz clazz
     * @return T
     */
    public static <T> T getBean(String name, Class<T> clazz) {
        return getApplicationContext().getBean(name, clazz);
    }
}

3、application.properties(配置文件,一般放到配置中心区)

# 机器的 IPV4 最后一段为 39 就是说 机器 39 的 workerId = 13 datacenterId = 13 后面的同上
snowflakeId.config={"39":{"workerId":13,"datacenterId":13},"40":{"workerId":14,"datacenterId":14}}

4、SnowflakeIdTest

package com.example.test;

import com.example.test.utils.SnowflakeIdGenerator;
import com.example.test.utils.ThreadPoolUtil;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadPoolExecutor;

@SpringBootTest
@Slf4j
public class SnowflakeIdTest {

    @Test
    public void testId() throws InterruptedException {
        // 根据配置文件 单例构建一个 雪花算法实例 如果没有配置则根据IP和随机数生产 机器ID(10bit)
        SnowflakeIdGenerator snowflakeIdGenerator = SnowflakeIdGenerator.getSnowflakeIdGenerator();
        // 线程池 最大线程数 为 200 最大 为 500
        // tomcat 最大线程数 为 200
        ThreadPoolExecutor threadPool = ThreadPoolUtil.getThreadPool();
        CountDownLatch countDownLatch = new CountDownLatch(10000);
        // 模拟并发
        // 由于加了线程池有等待,拿到雪花算法单例后生成ID也有锁,所以本方法的性能其实并不高,只是方便各位看到该生成方法ID的唯一性
        for (int i = 0; i < 10000; i++) {
            threadPool.execute(() -> {
                long id = snowflakeIdGenerator.generateUniqueId();
                log.info("id={}", id);
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
    }
}

5、ThreadPoolUtil(线程池)

package com.example.test.utils;

import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 *  线程池工具类,单例模式,全局唯一
 * @author ZHAOPINGAN
 */
public class ThreadPoolUtil {

    private static volatile ThreadPoolExecutor threadPool = null;

    private ThreadPoolUtil(){}

    public static ThreadPoolExecutor getThreadPool() {
        if (threadPool == null) {
            synchronized (ThreadPoolUtil.class) {
                if (threadPool == null) {
                    threadPool = new ThreadPoolExecutor(
                            // 核心线程数
                            200,
                            500,
                            // 10s 过期时间
                            10000L, TimeUnit.MILLISECONDS,
                            // 队列 1000
                            new LinkedBlockingQueue<>(10000),
                            // 线程池工厂
                            Executors.defaultThreadFactory(),
                            // 拒绝策略 丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息
                            new ThreadPoolExecutor.AbortPolicy()
                    );
                }
            }
        }
        return threadPool;
    }
}

6、查看日志(id有点多,只给大伙看一下获取的配置参数)