首页 > 编程语言 >MyBatis-Plus雪花算法实现源码解析

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

时间:2023-12-13 16:55:06浏览次数:44  
标签:return long 生成 public mac 源码 Plus MyBatis ID

1. 雪花算法(Snowflake Algorithm)

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

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

  1. 符号位(1位) 为了适配部分预研没有无符号整数,所以这一位空缺,并且一般为0。
  2. 时间戳(41位): 使用当前时间戳,精确到毫秒级别。这可以确保在一定时间内生成的ID是唯一的。由于使用的是41位,所以雪花算法可以支持68年的唯一ID生成(2^41毫秒,大约69.7年)。
  3. 机器ID(10位): 分配给生成ID的机器的唯一标识符。这样可以确保在同一时间戳内,不同机器生成的ID不会冲突。一般情况下,需要提前配置每台机器的唯一标识符,然后在运行时使用。
  4. 序列号(12位): 在同一时间戳内,同一机器上生成的ID的序列号。用于防止同一毫秒内生成的ID发生冲突。当在同一毫秒内生成多个ID时,通过递增序列号来区分它们。
1位 41位 5位 5位 12位
0 0000000000 0000000000 0000000000 0000000000 0 00000 00000 0000000000 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位,最大每毫秒分配2048个
            // 序列数最大的时候会等待到下一毫秒才会分配时间戳
            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的实现。如有错误,烦请指出。

标签:return,long,生成,public,mac,源码,Plus,MyBatis,ID
From: https://www.cnblogs.com/jonil/p/17899440.html

相关文章

  • springboot+VUE——mybatis分页和Element Plus的分页组件实践
    分页只有基础的分页功能,跳转和动态选择每页展示多少条数据的功能可以参考ElementPlus的分页组件自行配置并且传入相应的方法即可!<scriptlang="ts"setup>/***分页变量数据*/constpagination=ref({ current_page:1, // 当前页码,此处默认为第一页 total_data:0......
  • mybatis 批量插入
    <insertid="xxxxxxx"parameterType="xxx.xx.xxxx.xxxxx.xxxxxx">insertintot_form_detail(xxxx1,xxxx2,xxxx3,xxxx4,xxxx5)values<foreachitem="item&......
  • Netty04-优化与源码
    优化与源码分析1.优化1.1扩展序列化算法序列化,反序列化主要用在消息正文的转换上序列化时,需要将Java对象变为要传输的数据(可以是byte[],或json等,最终都需要变成byte[])反序列化时,需要将传入的正文数据还原成Java对象,便于处理目前的代码仅支持Java自带的序列化,反......
  • 智能AI问答系统ChatGPT网站系统源码(附系统部署教程)
    chatGPTGPT4.0,Midjourney绘画,相信对大家应该不感到陌生吧?简单来说,GPT-4技术比之前的GPT-3.5相对来说更加智能,会根据用户的要求生成多种内容甚至也可以和用户进行创作交流。然而,GPT-4对普通用户来说都是需要额外付费才可以使用。所以今天小编就整理一个真正免费的AI工具。不是一......
  • MybatisPlus中@TebleField注解的使用
    我们使用MybatisPlus框架操作数据库,一般有两种方式:一种是比较传统的方式,自己写Mapper.xml文件和SQL语句来操作数据库;另一种是使用MybatisPlus框架提供的BaseMapper或者IService接口中提供的增删改查方法。如果使用MybatisPlus中提供的BaseMapper和IService接口中的方法,就需要定......
  • MyBatis
    简介特性MyBatis是支持定制化SQL、存储过程以及高级映射的优秀的持久层框架。MyBatis避免了几乎所有的JDBC代码和手动设置参数以及获取结果集。MyBatis可以使用简单的XML或注解用于配置和原始映射,将接口和ava的POJO(PlainOldJavaObjects,普通的Java对象)映射成数据库中的......
  • 通过PowerShellPlus示例脚本学习PowerShell之-输出SQLServer服务属性
    ##=====================================================================##Title:Get-MSSQL-ServerAttrib-Csv##Description:ConnecttoSQLServerandoutputserverattributestoCSV##Author:Idera##Date:1/28/2009##Input......
  • 深入了解RPM包管理与Nginx源码包管理
    在Linux系统中,RPM(RedHatPackageManager)是一种流行的软件包管理工具,用于安装、查询和卸载软件包。同时,在编译和安装Nginx等软件时,源码包管理也是一个重要的方面。本文将详细介绍RPM包管理的基本操作以及Nginx源码包管理的方法。RPM包管理1.安装RPM包使用命令可以轻松安装RPM......
  • 保护信息安全的利器 —— DataSecurity Plus
    在当今数字化时代,数据安全已成为企业不可或缺的一环。随着信息技术的飞速发展,数据不仅是企业的核心资产,也承载着用户、客户以及公司的隐私。为了保护这些宝贵的数据,企业需要强大而综合的安全解决方案。在这个背景下,ManageEngine的DataSecurityPlus崭露头角,成为信息安全领域的璀璨......
  • drf实战和源码剖析----学习笔记1
     学自:bilibili武沛齐老师.武老师讲课:清晰,连贯,实用,透彻,真乃名师!#1.什么是前后端分离-不分离,主要用于后台系统(CRUD)和用户量上的情况,开发起来代价小-分离,面向用户,大量,几十万-根据实际情况#2.什么是drf?作用是什么?#3.必备工具:postman做为后端开发人员,要......