当前位置: 首页 > news >正文

雪花算法生成唯一ID并解决时钟回拨问题实战

        我们知道我们生成唯一ID的时候最常见的是使用雪花算法,但这个算法有个非常致命的问题就是时钟回拨的问题,一旦发生时钟回拨,就可能产生重复的ID,那我们应该如何解决这个问题呢?

       我们都知道雪花算法默认的64位分布情况是1位符号位+41位时间戳+10位工作机器ID+12位序列号,我们为了解决时钟回拨问题,需要调整下工作机器和序列号的位数,增加时钟序列位,调整后的64位分布是1位符号位+41位时间戳+8位工作机器ID+4位时钟序列号+10位序列号。

       我们之所以搞这个时钟序列位,是为了解决当发生时钟回拨时,我们不能用已经生成过ID的时钟序列继续生成ID,而是应该在它的基础上加1再生成,由于我们时钟回拨一共4位,最多可以设置16个数字,因此我们不能让这个时钟回拨序列一直递增,而是应该在指定条件下恢复到0。

        我们为了解决更特殊的时钟回拨问题(比如某次时钟回拨一下子回拨到了比较靠前的时间)我们引入了时钟回拨组的概念。具体实现代码如下所示。

        经过对时钟回拨序列以及回拨分组的巧妙运用,我们基本上解决了因时钟回拨而导致的生成重复ID的问题。

       需要注意的一点是,为了避免因服务器回拨次数太多而导致groupBackClockDataMap过大,我们可以在生成ID的过程中检查这个map中有哪些时间已经是比较久远的了,比如超过了1天,那么就可以从这个map中移除掉了。基本上即使人为修改服务器时间也在一天之内。

package com.concurrency.demo;

import lombok.Data;

import java.util.*;

public class Snowflake {
    // 各部分的位数定义(可根据需求调整)
    //private static final int TIMESTAMP_BITS = 41;     // 时间戳占41位(69年范围)
    private static final int CLOCK_SEQ_BITS = 4;      // 时钟序列占4位(支持15次回拨)
    private static final int MACHINE_BITS = 8;        // 机器ID占4位(0~256)
    private static final int SEQUENCE_BITS = 10;      // 序列号占10位(每毫秒1024个ID)

    // 最大值计算(防溢出),下面这些变量用于位运算使用的
    //时钟序列最大正整数,经过如下运算得到15
    private static final long MAX_CLOCK_SEQ = ~(-1L << CLOCK_SEQ_BITS);
    //机器位最大正整数,经过如下运算得到255
    private static final long MAX_MACHINE = ~(-1L << MACHINE_BITS);
    //序列号最大正整数,经过如下运算得到1023
    private static final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS);

    // 时间戳起始点,我们目前时间是2025年,我们就以2025年1月1日开始(2025-01-01 00:00:00)这样我们就可以
    //使用雪花算法一直到2025 + 69 = 2094年,这个足以公司使用了
    private static final long EPOCH = 1738339200000L;

    // 各部分的左移位数,雪花算法原本共分1个符号位+41位时间戳+10位机器+12位序列号,现在我们将机器位和序列号位数都做了修改
    //将机器位数由10位减到了8位,将序列号由12位改为了10位,匀出来的4位数给了时钟序列,因此现在的组成就成了
    //1位符号位+41位时间戳+8位机器+4位时钟序列+10位序列号。因此对于总共64位来讲,我们要先给序列号腾10个位置,因此需要让
    //时钟序列左移10位
    private static final int CLOCK_SEQ_SHIFT = SEQUENCE_BITS;
    //机器比时钟序列高4位,因此机器位需要向左移动10+4也就是14位
    private static final int MACHINE_SHIFT = SEQUENCE_BITS + CLOCK_SEQ_BITS;
    //时间戳比机器位又高8位,因此时间戳需要向左移动10 + 4 + 8共计22位
    private static final int TIMESTAMP_SHIFT = SEQUENCE_BITS + CLOCK_SEQ_BITS + MACHINE_BITS ;

    // 核心变量,这些核心变量一旦服务重启就丢失了,因此为了避免因内存丢失而导致生成ID重复,我们可以将这些关键变量
    //保存到我们的redis当中,等服务重启后,从redis中读取这些关键信息。
    private long lastTimestamp = -1L;
    //定义一个变量用来记录需要重置因时钟回拨而递增的时钟序列的时间。我们这里可以举个例子
    //假如我们第一次在10点10分50秒发生了第一次时钟回拨,回拨到了10点10分40秒,然后单号继续生成,时间来到10点11分00秒
    //这时又发生了一次时钟回拨,本次又回退到了10点10分45秒,可以看到第二次回拨后的时间依然比第一次发生时钟回拨的时间
    //小,那么resetMaxClockSequenceTimeStamp记录的就只是第一次发生时钟回拨的时间,第二次发生时钟回拨的时间做为
    //resetMaxClockSequenceTimeStamp的子集,它们组成一个组
    //继续生成单号,如果第三次发生时钟回拨的时间是10点12分15秒,而本次回拨到了10点12分05秒,那么很明显本次回拨到的
    //时间比第一次发生时钟回拨的时间分组内的时间都要大,这时候我们才将resetMaxClockSequenceTimeStamp设置为第三次发生时钟回拨的时间
    //也就创建了第二个分组,以此类推
    private long resetMaxClockSequenceTimeStamp = -1L;
    //定义一个变量,记录发生时钟回拨时,服务器前面已经生产过id的最晚时间,这个时间只能越来越大,不可回退
    private long maxGenerateTimestamp = -1L;
    private long clockSequence = 0L;  // 时钟序列(0~15)
    private long maxBackClockSequence = 0L; //记录同一时刻时针回拨最大的次数
    private long sequence = 0L;       // 序列号(0~1023)

    //专门记录时钟回拨的数据,并且按组进行记录
    private Map<Long, GroupBackClockData> groupBackClockDataMap = new HashMap<Long, GroupBackClockData>();


    private final long machineId;

    /**
     * 构造函数,每台服务机器ID必须不一样,而且为了避免服务重启等因素导致机器ID丢失,我们可以把机器ID入库
     * 服务器第一次启动的时候生成一个唯一ID保存到数据库中,以后服务重启,根据ip等条件查出来机器ID即可
     * @param machineId    机器ID (0~15)
     */
    public Snowflake(long machineId) {
        if (machineId < 0 || machineId > MAX_MACHINE) {
            throw new IllegalArgumentException("机器ID不合法");
        }
        this.machineId = machineId;
    }

    /**
     * 生成唯一ID
     */
    public synchronized long nextId() {
        long currentTimestamp = getCurrentTimestamp();
        long groupBackClockDataKey = -1L;
        // 处理时钟回拨
        if (currentTimestamp < lastTimestamp) {
            //第一次发生时钟回拨时,resetMaxClockSequenceTimeStamp是-1,将第一次发生回拨的时间点lastTimestamp赋值给它
            if (resetMaxClockSequenceTimeStamp == -1L) {
                resetMaxClockSequenceTimeStamp = lastTimestamp;
                GroupBackClockData groupBackClockData = new GroupBackClockData();
                groupBackClockData.setBackClockTimeStamp(resetMaxClockSequenceTimeStamp);
                groupBackClockDataMap.put(resetMaxClockSequenceTimeStamp, groupBackClockData);
            }
            //如果本次回退到的时间比resetMaxClockSequenceTimeStamp记录的时间靠前,说明我们可能要在前面已经多次回拨
            //到某个时间点以前,时钟序列号也递增过多次的场景下生成ID,那么我们只能在原来已经递增的时钟序列号基础上
            //继续递增,最大可以达到15,我们时钟回拨基本上不可能发生多次回拨的时间都在某个时间点以前,都是交叉向前
            //推进的。因此我们就基本上解决了时钟回拨问题
            if (currentTimestamp <= resetMaxClockSequenceTimeStamp) {
                //需要校验本次时钟回拨是不是回拨的很多,比如跨组回拨了,那么本次时钟回拨应该找到currentTimestamp
                //小于的最早组数据,然后把本次时钟回拨划归到那个组进行保存
                Long matchBackClockTimestamp = this.getMatchBackClockTimestamp(currentTimestamp);
                //说明本次时钟回拨跨组了,我们就需要在匹配的比较早的这个分组的时钟序列递增1作为本次时钟回拨后所要
                //使用的时钟序列,避免ID重复
                if (matchBackClockTimestamp < resetMaxClockSequenceTimeStamp) {
                    clockSequence = groupBackClockDataMap.get(matchBackClockTimestamp).getMaxClockSequence();
                    groupBackClockDataMap.get(matchBackClockTimestamp).getSubBackClockTimeStampSet().add(lastTimestamp);
                    groupBackClockDataKey = matchBackClockTimestamp;
                } else {
                     //这里之所以需要给clockSequence赋值maxBackClockSequence是因为下方
                    // if (currentTimestamp > maxGenerateTimestamp)的地方会将clockSequence归0,由于始终再次回拨到了
                    //前面发生过始终回拨的时间前面,那么我们只能在原来已经递增到的时钟序列的基础上继续向上递增
                    clockSequence = groupBackClockDataMap.get(resetMaxClockSequenceTimeStamp).getMaxClockSequence();
                    //currentTimestamp小于等于resetMaxClockSequenceTimeStamp并且lastTimestamp不等于resetMaxClockSequenceTimeStamp
                    //说明本次发生回滚的时间点lastTimestamp应该属于resetMaxClockSequenceTimeStamp这个组内
                    if (lastTimestamp != resetMaxClockSequenceTimeStamp) {
                        groupBackClockDataMap.get(resetMaxClockSequenceTimeStamp).getSubBackClockTimeStampSet().add(lastTimestamp);
                    }
                    groupBackClockDataKey = resetMaxClockSequenceTimeStamp;
                }
            } else {
                //判断一下本次时钟回拨,回拨到的时间点是不是比前一个分组中所有的回拨时间点都大,如果是的话,说明本次时钟回拨
                //应该开启一个新的分组了
                Long matchBackClockTimestamp = this.getMatchBackClockTimestamp(currentTimestamp);
                if (matchBackClockTimestamp == -1L) {
                    resetMaxClockSequenceTimeStamp = lastTimestamp;
                    //一个新组产生了,我们要生成GroupBackClockData
                    GroupBackClockData groupBackClockData = new GroupBackClockData();
                    groupBackClockData.setBackClockTimeStamp(resetMaxClockSequenceTimeStamp);
                    groupBackClockDataMap.put(resetMaxClockSequenceTimeStamp, groupBackClockData);
                    groupBackClockDataKey = resetMaxClockSequenceTimeStamp;
                } else {
                    clockSequence = groupBackClockDataMap.get(matchBackClockTimestamp).getMaxClockSequence();
                    if (lastTimestamp != resetMaxClockSequenceTimeStamp) {
                        groupBackClockDataMap.get(matchBackClockTimestamp).getSubBackClockTimeStampSet().add(lastTimestamp);
                    }
                    groupBackClockDataKey = matchBackClockTimestamp;
                }
            }
            // 使用递增的时钟序列抵消回拨
            clockSequence = (clockSequence + 1) & MAX_CLOCK_SEQ;
            maxBackClockSequence = clockSequence;
            groupBackClockDataMap.get(groupBackClockDataKey).setMaxClockSequence(maxBackClockSequence);
        } else {
            // 如果当前时间已经超过了前面生成过ID的最大时间,那么说明这个时间之后还没有生成过ID,这时候可以让
            //时钟序列归0了
            if (currentTimestamp > maxGenerateTimestamp) {
                clockSequence = 0;
                maxBackClockSequence = 0;
            }
        }

        // 同一毫秒内生成ID
        if (currentTimestamp == lastTimestamp) {
            sequence = (sequence + 1) & MAX_SEQUENCE;
            if (sequence == 0) {
                // 序列号耗尽,等待下一毫秒
                currentTimestamp = waitNextMillis(currentTimestamp);
            }
        } else {
            //如果不在同一毫秒内了,那么sequence从0重新开始
            sequence = 0L;
        }
        //这里要分情况讨论,如果lastTimestamp的时间比当前时间大,那么在将lastTimestamp设置为比较小的当前时间之前
        //要先将lastTimestamp赋值给maxGenerateTimestamp,这样可以保证maxGenerateTimestamp始终是最新的生成ID的时间
        if (lastTimestamp > currentTimestamp) {
            maxGenerateTimestamp = lastTimestamp;
            lastTimestamp = currentTimestamp;
        } else {
            //如果不是时钟回拨,时间正常往前推进,我们要把当前时间赋值给lastTimestamp,然后赋值给maxGenerateTimestamp
            lastTimestamp = currentTimestamp;
            maxGenerateTimestamp = lastTimestamp;
        }

        // 拼接各部分生成最终ID
        return ((currentTimestamp - EPOCH) << TIMESTAMP_SHIFT)
                | (machineId << MACHINE_SHIFT)
                | (clockSequence << CLOCK_SEQ_SHIFT)
                | sequence;
    }

    /**
     * 去groupBackClockDataMap中获取currentTimestamp小于等于某个分组内的时钟回拨时间,并且这个分组是最早的
     * @param currentTimestamp
     * @return
     */
    private Long getMatchBackClockTimestamp(long currentTimestamp) {
        long matchBackClockTimestamp = -1L;
        //把groupBackClockDataMap的key按照从小到大的顺序排序,然后从小开始遍历,哪个组的时间晚于currentTimestamp
        //就说明本次时钟回拨属于这个组
        Long[] keyList = (Long[])groupBackClockDataMap.keySet().toArray();
        Arrays.sort(keyList);
        for (Long key : keyList) {
            if (currentTimestamp <= key) {
                matchBackClockTimestamp = key;
                break;
            } else {
                Set<Long> subBackClockTimeStampSet = groupBackClockDataMap.get(key).getSubBackClockTimeStampSet();
                for (Long subTimestamp : subBackClockTimeStampSet) {
                    if (currentTimestamp <= subTimestamp) {
                        matchBackClockTimestamp = key;
                        break;
                    }
                }
            }
        }
        return matchBackClockTimestamp;
    }

    /**
     * 阻塞到下一毫秒
     */
    private long waitNextMillis(long currentTimestamp) {
        long timestamp = getCurrentTimestamp();
        while (timestamp <= currentTimestamp) {
            timestamp = getCurrentTimestamp();
        }
        return timestamp;
    }

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

    /**
     * 我们把每个resetMaxClockSequenceTimeStamp以及凡是正常始终回拨回拨到这个时间之前的称为一组
     */
    @Data
    private class GroupBackClockData {
        //backClockTimeStamp对应了resetMaxClockSequenceTimeStamp的值
        private Long backClockTimeStamp;
        //每个resetMaxClockSequenceTimeStamp是一组,这一组使用到的最大的时钟序列号
        private Long maxClockSequence;
        //每一组中可能有一些时钟回拨回拨到了backClockTimeStamp靠前的时间,这些回拨时间共享本组最大的时钟回拨序列号
        //如果后面某个时间点发生指针回拨跨度比较大,跨了resetMaxClockSequenceTimeStamp,那么把本次指针回拨的记录也
        //记录到当前backClockTimeStamp这一组的subBackClockTimeStampList当中并且将maxClockSequence递增1
        private Set<Long> subBackClockTimeStampSet = new HashSet<Long>();
    }

    public static void main(String[] args) throws InterruptedException {
        final Snowflake generator = new Snowflake(200);
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            long l = generator.nextId();
        }
        //System.out.println(l);
        long end = System.currentTimeMillis();
        System.out.println("耗时:" + (end - start));
    }
}

相关文章:

  • vue3+vite项目引入electron运行为桌面项目
  • fork: retry: No child processes-linux18
  • Kotlin 2.1.0 入门教程(十七)接口
  • SpringCloud面试题----微服务下为什需要链路追踪系统
  • 2月14(信息差)
  • 【算法工程】解决linux下Aspose.slides提示No usable version of libssl found以及强化推理模型的短板
  • Win7本地化部署deepseek-r1等大模型详解
  • Base64 PDF解析器
  • 大模型为什么离不开PyTorch
  • python使用try-except-else处理异常
  • AndroidStudio查看Sqlite和SharedPreference
  • 利用蓝耘智算平台深度搭建deepseek R1模型,进行深度机器学习
  • git 提示 fatal: The remote end hung up unexpectedly
  • 网络安全 “免疫力”:从人体免疫系统看防御策略
  • Windchill开发-电子仓相关对象信息查询SQL
  • CCF-GESP 等级考试 2024年9月认证C++二级真题解析
  • 《网络编程卷2:进程间通信》第八章:共享内存深度解析与多进程高性能通信实践
  • 【前端OCR】如何用paddlejs开发一个属于前端本地的OCR文本识别功能
  • 江科大51单片机学习笔记(2)
  • 在Linux中Redis不支持lua脚本的处理方法
  • 泰安网站建设优化技术/安徽seo优化规则
  • 自已怎样网站/网络营销创意案例
  • 做网站是先买域名还是/杭州seo搜索引擎优化公司
  • 网络课程系统网站建设费用/优化搜狗排名
  • 公司申请网站建设申请理由/德芙巧克力软文推广
  • 网站制作培训费用/如何营销推广自己的产品