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位 |
|---|---|---|---|---|
| 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的构造器。它会设置datacenterId与workerId
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-Plus的IdType.ASSIGN_ID会到IdWorker类中获取全局唯一ID
其中,会调用以下方法获取全局唯一ID(long)
/**
* 获取唯一ID
*
* @return id
*/
public static long getId(Object entity) {
return IDENTIFIER_GENERATOR.nextId(entity).longValue();
}
进入nextId方法的具体实现,发现它是使用sequence的nextId方法
@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的实现。如有错误,烦请指出。
MyBatis-Plus雪花算法实现源码解析的更多相关文章
- 数据结构算法 - ConcurrentHashMap 源码解析
五个线程同时往 HashMap 中 put 数据会发生什么? ConcurrentHashMap 是怎么保证线程安全的? 在分析 HashMap 源码时还遗留这两个问题,这次我们站在 Java 多线程 ...
- [源码解析] PyTorch 如何实现后向传播 (4)---- 具体算法
[源码解析] PyTorch 如何实现后向传播 (4)---- 具体算法 目录 [源码解析] PyTorch 如何实现后向传播 (4)---- 具体算法 0x00 摘要 0x01 工作线程主体 1.1 ...
- 【MyBatis源码解析】MyBatis一二级缓存
MyBatis缓存 我们知道,频繁的数据库操作是非常耗费性能的(主要是因为对于DB而言,数据是持久化在磁盘中的,因此查询操作需要通过IO,IO操作速度相比内存操作速度慢了好几个量级),尤其是对于一些相 ...
- MyBatis源码解析之数据源(含数据库连接池简析)
一.概述: 常见的数据源组件都实现了javax.sql.DataSource接口: MyBatis不但要能集成第三方的数据源组件,自身也提供了数据源的实现: 一般情况下,数据源的初始化过程参数较多,比 ...
- Mybatis源码解析(一)(2015年06月11日)
一.简介 先看看Mybatis的源码结构图,Mybatis3.2.7版本包含的包共计19个,其他版本可能会少. 每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为 ...
- Mybatis源码解析-DynamicSqlSource和RawSqlSource的区别
XMLLanguageDriver是ibatis的默认解析sql节点帮助类,其中的方法其会调用生成DynamicSqlSource和RawSqlSource这两个帮助类,本文将对此作下简单的简析 应用 ...
- MyBatis源码解析(四)——DataSource数据源模块
原创作品,可以转载,但是请标注出处地址:http://www.cnblogs.com/V1haoge/p/6634880.html 1.回顾 上一文中解读了MyBatis中的事务模块,其实事务操作无非 ...
- mybatis源码-解析配置文件(四-1)之配置文件Mapper解析(cache)
目录 1. 简介 2. 解析 3 StrictMap 3.1 区别HashMap:键必须为String 3.2 区别HashMap:多了成员变量 name 3.3 区别HashMap:key 的处理多 ...
- MyBatis源码解析【7】接口式编程
前言 这个分类比较连续,如果这里看不懂,或者第一次看,请回顾之前的博客 http://www.cnblogs.com/linkstar/category/1027239.html 修改例子 在我们实际 ...
- mybatis源码-解析配置文件(四)之配置文件Mapper解析
在 mybatis源码-解析配置文件(三)之配置文件Configuration解析 中, 讲解了 Configuration 是如何解析的. 其中, mappers作为configuration节点的 ...
随机推荐
- Mybatis-plus SQL效率插件PerformanceInterceptor无效->替换为p6spy
使用mybatis-plus时,需要加入执行的sql分析 发现mybatis-plus中的PerformanceInterceptor无效了 查了信息发现 3.2.0 版本之后把这个功能可剔除了 可同 ...
- 每日一库:fsnotify简介
fsnotify是一个用Go编写的文件系统通知库.它提供了一种观察文件系统变化的机制,例如文件的创建.修改.删除.重命名和权限修改.它使用特定平台的事件通知API,例如Linux上的inotify,m ...
- WPF学习 - 自定义窗体(二)
上一篇文章写了如何创建自定义窗体:使用 WindowChrome 或者 WindowStyle="None"这两种方式.本文将讲述如何设置窗体的效果(以阴影效果为例),以及在效果模 ...
- 一文解锁vue3中hooks的使用姿势
vue3 中的 hooks 是什么? 简单来说如果你的函数中用到了诸如 ref,reactive,onMounted 等 vue 提供的 api 的话,那么它就是一个 hooks 函数,如果没用到它就 ...
- 手把手教你搭建springbootsecurity+jwt,全面了解
研究了两周了springbootsecurity+jwt的使用,终于搭起来了,这里跟大家分享下. 首先,不了解jwt的可以提前去查下相关资料,我之前也有讲过,大家可以先看下: https://www ...
- 使用 Python ssh 远程登陆服务器的最佳方案
在使用 Python 写一些脚本的时候,在某些情况下,我们需要频繁登陆远程服务去执行一次命令,并返回一些结果. 在 shell 环境中,我们是这样子做的. sshpass -p ${passwd} s ...
- 基于FPGA 的SDRAM控制器
SDRAM基本信息 储存能力计算 4X16X4=256(Mbit),注意不是MByte SDRAM控制 sdram包含两个部分:sdram_ctrl.fifo_ctrl. sdram_ctrl:其顶层 ...
- install_failed_uid_changed no root 没有root手机
主页 问题 Installation error: INSTALL_FAILED_UID_CHANGED no root Installation error: INSTALL_FAILED_UID_ ...
- 栈和堆的区别、FreeRTOS 中的任务栈
栈和堆的区别.FreeRTOS 中的任务栈 01 堆和栈的概念 堆 功能 堆是一块用于动态分配内存的区域,用于存储程序运行时动态创建的对象.堆的大小可以在程序运行时动态调整. 特点 堆的分配和释放是由 ...
- python接口自动化之request请求,如何使用 Python调用 API?
Python实战 | 如何使用 Python 调用 API 一.HTTP 请求HTTP 请求是在 HTTP 协议下的一种数据格式,用于向服务器发送请求,其通常由请求行.请求头和请求体三部分构成,请求头 ...