Mybatis自定義插件生成雪花ID做為主鍵項目
先附上項目項目GitHub地址
spring-boot-mybatis-interceptor
有關Mybatis雪花ID主鍵插件前面寫了兩篇博客作為該項目落地的鋪墊。
該插件項目可以直接運用於實際開發中,作為分布式數據庫表主鍵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、高可用
我們去思考消耗性能比較大的地方可能出要出現在兩個地方
1)雪花算法生成雪花ID的過程。
2)通過類的反射機制找到哪些屬性帶有@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)