Mybatis 自定义插件生成雪花 ID 做为主键项目
先附上项目项目 GitHub 地址
有关 Mybatis 雪花 ID 主键插件前面写了两篇博客作为该项目落地的铺垫.
1,Mybatis 框架 ---Mybatis 插件原理
2,java 算法 --- 静态内部类实现雪花算法
该插件项目可以直接运用于实际开发中, 作为分布式数据库表主键 ID 使用.
一, 项目概述
1, 项目背景
在生成表主键 ID 时, 我们可以考虑主键自增 或者 UUID, 但它们都有很明显的缺点
主键自增: 1, 自增 ID 容易被爬虫遍历数据. 2, 分表分库会有 ID 冲突.
UUID: 1, 太长, 并且有索引碎片, 索引多占用空间的问题 2, 无序.
雪花算法就很适合在分布式场景下生成唯一 ID, 它既可以保证唯一又可以排序, 该插件项目的原理是
通过拦截器拦截 Mybatis 的 insert 语句, 通过自定义注解获取到主键, 并为该主键赋值雪花 ID, 插入数据库中.
2, 技术架构
项目总体技术选型
SpringBoot2.1.7 + Mybatis + Maven3.5.4 + MySQL + lombok(插件)
3, 使用方式
在你需要做为主键的属性上添加 @AutoId 注解, 那么通过插件可以自动为该属性赋值主键 ID.
- public class TabUser {
- /**
- * id(添加自定义注解)
- */
- @AutoId
- private Long id;
- /**
- * 姓名
- */
- private String name;
- // 其它属性 包括 get,set 方法
- }
4, 项目测试
配置好数据库连接信息, 直接启动 Springboot 启动类 Application.java, 访问 localhost:8080/save-foreach-user 就可以看到数据库数据已经有雪花 ID 了.
如图
二, 项目代码说明
在正式环境中只要涉及到插入数据的操作都被该插件拦截, 并发量会很大. 所以该插件代码即要保证线程安全又要保证高可用. 所以在代码设计上做一些说明.
1, 线程安全
这里的线程安全主要是考虑产生雪花 ID 的时候必须是线程安全的, 不能出现同一台服务器同一时刻出现了相同的雪花 ID, 这里是通过
静态内部类单例模式 + synchronized
来保证线程安全的, 具体有关生成雪花 ID 的代码这里就不粘贴.
2, 高可用
我们去思考消耗性能比较大的地方可能出要出现在两个地方
) 雪花算法生成雪花 ID 的过程.
) 通过类的反射机制找到哪些属性带有 @AutoId 注解的过程.
第一点
其实在静态内部类实现雪花算法这篇博客已经简单测试过, 生成 20 万条数据, 大约在 1.7 秒能满足实际开发中我们的需要.
第二点
这里是有比较好的解决方案的, 可以通过两点去改善它.
1), 在插件中添加了一个 Map 处理器
- /**
- * key 值为 Class 对象 value 可以理解成是该类带有 AutoId 注解的属性, 只不过对属性封装了一层.
- * 它是非常能够提高性能的处理器 它的作用就是不用每一次一个对象经来都要看下它的哪些属性带有 AutoId 注解
- * 毕竟类的反射在性能上并不友好. 只要 key 包含该 Class, 那么下次同样的 class 进来, 就不需要检查它哪些属性带 AutoId 注解.
- */
- private Map<Class, List<Handler>> handlerMap = new ConcurrentHashMap<>();
插件部分源码
- public class AutoIdInterceptor implements Interceptor {
- /**
- * Map 处理器
- */
- private Map<Class, List<Handler>> handlerMap = new ConcurrentHashMap<>();
- /**
- * 某某方法
- */
- private void process(Object object) throws Throwable {
- Class handlerKey = object.getClass();
- List<Handler> handlerList = handlerMap.get(handlerKey);
- // 先判断 handlerMap 是否已存在该 class, 不存在先找到该 class 有哪些属性带有 @AutoId
- if (handlerList == null) {
- handlerMap.put(handlerKey, handlerList = new ArrayList<>());
- // 通过反射 获取带有 AutoId 注解的所有属性字段, 并放入到 handlerMap 中
- }
- // 为带有 @AutoId 赋值 ID
- for (Handler handler : handlerList) {
- handler.accept(object);
- }
- }
- }
2) 添加 break label(标签)
这个就比较细节了, 因为上面的 process 方法不是线程安全的, 也就是说可能存在同一时刻有 N 个线程进入 process 方法, 那么这里可以优化如下:
- // 添加了 SYNC 标签
- SYNC:
- if (handlerList == null) {
- // 此时 handlerList 确实为 null, 进入这里
- synchronized (this) {
- handlerList = handlerMap.get(handlerKey);
- // 但到这里发现它已经不是为 null 了, 因为可能被其它线程往 map 中插入数据, 那说明其实不需要在执行下面的逻辑了, 直接跳出 if 体的 SYNC 标签位置.
- // 那么也就不会执行 if (handlerList == null) {} 里面的逻辑.
- if (handlerList != null) {
- break SYNC;
- }
- }
- }
这里虽然很细节, 但也是有必要的, 毕竟这里并发量很大, 这样设计能一定程度提升性能.
我相信, 无论今后的道路多么坎坷, 只要抓住今天, 迟早会在奋斗中尝到人生的甘甜. 抓住人生中的一分一秒, 胜过虚度中的一月一年!(7)
来源: https://www.cnblogs.com/qdhxhz/p/11407815.html