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雪花算法内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Spring事务&Spring整合MyBatis的两种方式

    Spring事务&Spring整合MyBatis的两种方式

    这篇文章主要介绍了Spring事务&Spring整合MyBatis的两种方式,本文结合实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-02-02
  • Java毕业设计之多用户宿舍管理系统的实现

    Java毕业设计之多用户宿舍管理系统的实现

    这篇文章主要介绍了基于Java实现的多用户宿舍管理系统,本文采用了jsp、servlet、jdbc等技术,文中示例代码讲解详细,需要的可以参考一下
    2022-02-02
  • Flutter实现文本组件、图标及按钮组件的代码

    Flutter实现文本组件、图标及按钮组件的代码

    这篇文章主要介绍了Flutter实现文本组件、图标及按钮组件的代码,本文给大家介绍的非常详细,具有一定的参考借鉴价值 ,需要的朋友可以参考下
    2019-07-07
  • Java实现本地缓存的方式汇总

    Java实现本地缓存的方式汇总

    引入缓存,主要用于实现系统的高性能,高并发,这篇文章主要介绍了Java实现本地缓存的几种方式,本文结合示例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-07-07
  • 使用ftpClient下载ftp上所有文件解析

    使用ftpClient下载ftp上所有文件解析

    最近项目需要写个小功能,需求就是实时下载ftp指定文件夹下的所有文件(包括子目录)到本地文件夹中,保留文件到目录路径不变。今天小编给大家分享使用ftpClient下载ftp上所有文件的方法,需要的的朋友参考下吧
    2017-04-04
  • java把字符串写入文件里的简单方法分享

    java把字符串写入文件里的简单方法分享

    这篇文章主要介绍了java把字符串写入到文件里的简单方法,这是跟一个外国朋友学的代码,需要的朋友可以参考下
    2014-03-03
  • 深入解析Spring Boot 的SPI机制详情

    深入解析Spring Boot 的SPI机制详情

    这篇文章主要介绍了深入解析Spring Boot的SPI机制详情,SPI是JDK内置的一种服务提供发现机制,可以用来启用框架扩展和替换组件,主要用于框架中开发,更多相关介绍,感兴趣的小伙伴可以参考一下下面文章内容
    2022-08-08
  • java、js中实现无限层级的树形结构方法(类似递归)

    java、js中实现无限层级的树形结构方法(类似递归)

    下面小编就为大家带来一篇java、js中实现无限层级的树形结构方法(类似递归)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2016-11-11
  • 浅谈Sharding-JDBC强制路由案例实战

    浅谈Sharding-JDBC强制路由案例实战

    本文主要介绍了浅谈Sharding-JDBC强制路由案例实战,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-07-07
  • Idea安装Eslint插件提示:Plugin NativeScript was not installed的问题

    Idea安装Eslint插件提示:Plugin NativeScript was not installed的问题

    这篇文章主要介绍了Idea安装Eslint插件提示:Plugin NativeScript was not installed的问题,本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-10-10

最新评论