MyBatis-Plus雪花算法实现源码解读

 更新时间:2023年12月14日 09:06:06   作者:护发师兄  
雪花算法是一种用于生成唯一标识符(ID)的分布式算法,雪花算法的设计目标是在分布式系统中生成全局唯一的ID,同时保证ID的有序性和趋势递增,这篇文章主要介绍了MyBatis-Plus雪花算法实现源码解析,需要的朋友可以参考下

1. 雪花算法(Snowflake Algorithm)

雪花算法(Snowflake Algorithm)是一种用于生成唯一标识符(ID)的分布式算法。最初由 Twitter 公司开发,用于生成其内部分布式系统中的唯一ID。雪花算法的设计目标是在分布式系统中生成全局唯一的ID,同时保证ID的有序性和趋势递增。

雪花算法生成的ID是64位的整数,分为以下几个部分:

  • 符号位(1位) 为了适配部分预研没有无符号整数,所以这一位空缺,并且一般为0。
  • 时间戳(41位): 使用当前时间戳,精确到毫秒级别。这可以确保在一定时间内生成的ID是唯一的。由于使用的是41位,所以雪花算法可以支持68年的唯一ID生成(2^41毫秒,大约69.7年)。
  • 机器ID(10位): 分配给生成ID的机器的唯一标识符。这样可以确保在同一时间戳内,不同机器生成的ID不会冲突。一般情况下,需要提前配置每台机器的唯一标识符,然后在运行时使用。
  • 序列号(12位): 在同一时间戳内,同一机器上生成的ID的序列号。用于防止同一毫秒内生成的ID发生冲突。当在同一毫秒内生成多个ID时,通过递增序列号来区分它们。
1位41位5位5位12位
00000000000 0000000000 0000000000 0000000000 000000000000000000000 00
符号位(一般为0)时间戳ms 大约可以表示69.7年mac地址混淆mac地址与JVM-PID共同混淆序列号

雪花算法生成的ID具有以下特点:

  • 全局唯一性: 在整个分布式系统中,每个生成的ID都是唯一的。
  • 有序性: 由于时间戳占据了大部分位数,生成的ID是趋势递增的,使得生成的ID在数据库索引上有较好的性能。
  • 分布式: 不同机器上生成的ID不会冲突,可以在分布式系统中使用。

2. 流程

2.1 MyBatis-Plus全局唯一ID生成器初始化

MyBatis-Plus启动后,会通过IdentifierGeneratorAutoConfiguration类进行项目的自动配置。

注意:IdentifierGeneratorAutoConfiguration类是被@Lazy注解了,所以他是懒加载,所以有的项目会在启动后往日志表插入一条记录来预热MyBatis-Plus

自动配置的内容是往项目注入Bean,该Bean主要是用于全局唯一ID的生成。其中传入的参数是第一个非回环地址的InetAddress

注意:IdentifierGenerator是接口,DefaultIdentifierGenerator是其一个实现类

@Bean
@ConditionalOnMissingBean
public IdentifierGenerator identifierGenerator(InetUtils inetUtils) {
    return new DefaultIdentifierGenerator(inetUtils.findFirstNonLoopbackAddress());
}

会直接生成一个Sequence

public DefaultIdentifierGenerator(InetAddress inetAddress) {
    this.sequence = new Sequence(inetAddress);
}

这是Sequence的构造器。它会设置datacenterIdworkerId

public Sequence(InetAddress inetAddress) {
    this.inetAddress = inetAddress;
    this.datacenterId = getDatacenterId(maxDatacenterId);
    this.workerId = getMaxWorkerId(datacenterId, maxWorkerId);
    // 打印初始化语句
    initLog();
}

这是datacenterId的获取部分,里面可以看到它主要是mac地址混淆得到

注意:这里得到的datacenterId还没有经过截取,是64位的

/**
* 数据标识id部分
*/
protected long getDatacenterId(long maxDatacenterId) {
long id = 0L;
try {
    if (null == this.inetAddress) {
        this.inetAddress = InetAddress.getLocalHost();
    }
    NetworkInterface network = NetworkInterface.getByInetAddress(this.inetAddress);
    if (null == network) {
        id = 1L;
    } else {
        // 获取mac地址
        byte[] mac = network.getHardwareAddress();
        // 混淆
        if (null != mac) {
            id = ((0x000000FF & (long) mac[mac.length - 2]) | (0x0000FF00 & (((long) mac[mac.length - 1]) << 8))) >> 6;
            id = id % (maxDatacenterId + 1);
        }
    }
} catch (Exception e) {
    logger.warn(" getDatacenterId: " + e.getMessage());
}
return id;
}

这是获取workerId的方法,可以看到workerId是由mac地址和JVM-PID共同混淆得出的

/**
 * 获取 maxWorkerId
 */
protected long getMaxWorkerId(long datacenterId, long maxWorkerId) {
    StringBuilder mpid = new StringBuilder();
    mpid.append(datacenterId);
    String name = ManagementFactory.getRuntimeMXBean().getName();
    if (StringUtils.isNotBlank(name)) {
        /*
         * GET jvmPid
         */
        mpid.append(name.split(StringPool.AT)[0]);
    }
    /*
     * MAC + PID 的 hashcode 获取16个低位
     */
    return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);
}

2.2 获取全局唯一ID流程

注意:若之前没有获取过全局唯一ID,那么它会走一遍2.1的全部流程。

如果是使用MyBatis-PlusIdType.ASSIGN_ID会到IdWorker类中获取全局唯一ID

其中,会调用以下方法获取全局唯一ID(long)

/**
 * 获取唯一ID
 *
 * @return id
 */
public static long getId(Object entity) {
    return IDENTIFIER_GENERATOR.nextId(entity).longValue();
}

进入nextId方法的具体实现,发现它是使用sequencenextId方法

@Override
public Long nextId(Object entity) {
    return sequence.nextId();
}

下面包含一些自己的注释

注意:nextId方法是被synchronized修饰的,是同步方法

/**
 * 获取下一个 ID
 *
 * @return 下一个 ID
 */
public synchronized long nextId() {
    long timestamp = timeGen();
    // 闰秒
    // 这里会判断是否发生时钟偏移,若偏移在5ms以内会重新尝试重新获取时间,看是否能够重新获取正确的时间。
    // 因为偶尔会有闰秒的存在
    if (timestamp < lastTimestamp) {
        long offset = lastTimestamp - timestamp;
        if (offset <= 5) {
            try {
                wait(offset << 1);
                timestamp = timeGen();
                if (timestamp < lastTimestamp) {
                    throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", offset));
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        } else {
            throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", offset));
        }
    }
    if (lastTimestamp == timestamp) {
        // 相同毫秒内,序列号自增
        sequence = (sequence + 1) & sequenceMask;
        if (sequence == 0) {
            // 同一毫秒的序列数已经达到最大
            // 序列数(毫秒内自增位)为12位,最大每毫秒分配4096个
            // 序列数最大的时候会等待到下一毫秒才会分配时间戳
            timestamp = tilNextMillis(lastTimestamp);
        }
    } else {
        // 不同毫秒内,序列号置为 1 - 2 随机数
        // 这里序列号置为1-2的随机数是为了方便后续分库分表的时候hash比较均匀
        sequence = ThreadLocalRandom.current().nextLong(1, 3);
    }
    lastTimestamp = timestamp;
    // twepoch 是 时间起始标记点,作为基准,一般取系统的最近时间(一旦确定不能变动)
    // 因为前面已经说过41位的时间戳可以分配69.7年,如果从1970.1.1开始数,那么时间戳可能在未来某一天大于41位
    // 时间戳部分 | 数据中心部分 | 机器标识部分 | 序列号部分
    return ((timestamp - twepoch) << timestampLeftShift)
        | (datacenterId << datacenterIdShift)
        | (workerId << workerIdShift)
        | sequence;
}

这是生成时间的方法,其中使用了SystemClock,这是一个有趣的实现

protected long timeGen() {
    return SystemClock.now();
}

SystemClock类,这个类的主要思想就是用一个任务线程池以固定速率去获取系统时间,若在同一时间间隔内,那么直接返回,而不需要再次访问系统时间。其实主要是因为System.currentTimeMillis()jni方法,jni方法由于存在内存复制和数据转换,所以是比较耗时的。

/**
 * 高并发场景下System.currentTimeMillis()的性能问题的优化
 *
 * <p>System.currentTimeMillis()的调用比new一个普通对象要耗时的多(具体耗时高出多少我还没测试过,有人说是100倍左右)</p>
 * <p>System.currentTimeMillis()之所以慢是因为去跟系统打了一次交道</p>
 * <p>后台定时更新时钟,JVM退出时,线程自动回收</p>
 * <p>10亿:43410,206,210.72815533980582%</p>
 * <p>1亿:4699,29,162.0344827586207%</p>
 * <p>1000万:480,12,40.0%</p>
 * <p>100万:50,10,5.0%</p>
 *
 * @author hubin
 * @since 2016-08-01
 */
public class SystemClock {
	// 定期更新时间戳的时间单位
    private final long period;
    // 记录当前时间戳的原子类,因为可能存在并发线程使用
    private final AtomicLong now;
    private SystemClock(long period) {
        this.period = period;
        this.now = new AtomicLong(System.currentTimeMillis());
        scheduleClockUpdating();
    }
    private static SystemClock instance() {
        return InstanceHolder.INSTANCE;
    }
    public static long now() {
        return instance().currentTimeMillis();
    }
    public static String nowDate() {
        return new Timestamp(instance().currentTimeMillis()).toString();
    }
    // 这里是有一个定期更新方法
    // 里面有一个定时线程池,它会以固定的时间间隔(period)在类里面更新当前的时间戳
    private void scheduleClockUpdating() {
        ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(runnable -> {
            Thread thread = new Thread(runnable, "System Clock");
            thread.setDaemon(true);
            return thread;
        });
        scheduler.scheduleAtFixedRate(() -> now.set(System.currentTimeMillis()), period, period, TimeUnit.MILLISECONDS);
    }
    // 获取事件
    private long currentTimeMillis() {
        return now.get();
    }
    // 默认事件间隔为1ms
    private static class InstanceHolder {
        public static final SystemClock INSTANCE = new SystemClock(1);
    }
}

至此,已经介绍完MyBatis-Plus获取全局唯一ID的实现。如有错误,烦请指出。

到此这篇关于MyBatis-Plus雪花算法实现源码解读的文章就介绍到这了,更多相关MyBatis-Plus雪花算法内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java基础之MapReduce框架总结与扩展知识点

    Java基础之MapReduce框架总结与扩展知识点

    本章,是MapReduce的最终章,我在写本章的时候,发现前面忘记介绍MpaTask与ReduceTask了,所以本章补上哈,另外还有两个扩展的知识点,讲完这些,我会对整个MapReduce进行总结一下,让大家再次了解MapReduce的工作流程,更加清晰地认识MapReduce ,需要的朋友可以参考下
    2021-05-05
  • 详谈ServiceLoader实现原理

    详谈ServiceLoader实现原理

    下面小编就为大家带来一篇详谈ServiceLoader实现原理。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-02-02
  • Java截取字符串的方法

    Java截取字符串的方法

    这篇文章主要介绍了Java截取字符串的方法,实例分析了java中substring方法的使用技巧,需要的朋友可以参考下
    2015-05-05
  • Java如何获取Json中的数据实例代码

    Java如何获取Json中的数据实例代码

    这篇文章主要给大家介绍了关于Java如何获取Json中数据的相关资料,我们在日常开发中少不了和JSON数据打交道,那么我们来看看JAVA中常用的JSON获取方式,需要的朋友可以参考下
    2023-09-09
  • Spring Boot Actuator未授权访问漏洞的问题解决

    Spring Boot Actuator未授权访问漏洞的问题解决

    Spring Boot Actuator 端点的未授权访问漏洞是一个安全性问题,可能会导致未经授权的用户访问敏感的应用程序信息,本文就来介绍一下解决方法,感兴趣的可以了解一下
    2023-09-09
  • Hibernate中5个核心接口知识点整理

    Hibernate中5个核心接口知识点整理

    在本篇文章里小编给大家整理的是一篇关于Hibernate中5个核心接口知识点整理等内容,有兴趣的朋友们跟着学习参考下。
    2021-08-08
  • 解决Java Redis删除HashMap中的key踩到的坑

    解决Java Redis删除HashMap中的key踩到的坑

    这篇文章主要介绍了解决Java Redis删除HashMap中的key踩到的坑,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-02-02
  • Java十道入门易踩坑题分析后篇

    Java十道入门易踩坑题分析后篇

    这篇文章总结分析了Java入门容易碰到的几点易进坑的题目,对于新手小白刚开始学Java非常有益处,让你少走避开弯路,感兴趣的朋友快来看看吧
    2022-01-01
  • 浅析Java中并发工具类的使用

    浅析Java中并发工具类的使用

    在JDK的并发包里提供了几个非常有用的并发工具类。CountDownLatch、CyclicBarrier和Semaphore工具类提供了一种并发流程控制的手段,Exchanger工具类提供了在线程间交换数据的一种方法。本文主要介绍了它们的使用,需要的可以参考一下
    2022-12-12
  • 使用Jackson来实现Java对象与JSON的相互转换的教程

    使用Jackson来实现Java对象与JSON的相互转换的教程

    这篇文章主要介绍了使用Jackson来实现Java对象与JSON的互相转换的教程,文中罗列了3中Jackson的使用方式,需要的朋友可以参考下
    2016-01-01

最新评论