Mybatis實(shí)現(xiàn)分表插件
背景
事情是醬紫的,阿星的上級(jí)leader
負(fù)責(zé)記錄信息的業(yè)務(wù),每日預(yù)估數(shù)據(jù)量是15
萬(wàn)左右,所以引入sharding-jdbc
做分表。
上級(jí)leader
完成業(yè)務(wù)的開發(fā)后,走了一波自測(cè),git push
后,就忙其他的事情去了。
項(xiàng)目的框架是SpringBoot+Mybaits
出問(wèn)題了
阿星負(fù)責(zé)的業(yè)務(wù)也開發(fā)完了,熟練的git pull
,準(zhǔn)備自測(cè),單元測(cè)試run
一下,上個(gè)廁所回來(lái)收工,就是這么自信。
回來(lái)后,看下控制臺(tái),人都傻了,一片紅,內(nèi)心不禁感嘆“如果這是股票基金該多好”。
出了問(wèn)題就要解決,隨著排查深入,我的眉頭一皺發(fā)現(xiàn)事情并不簡(jiǎn)單,怎么以前的一些代碼都報(bào)錯(cuò)了?
隨著排查深入,最后跟到了Mybatis
源碼,發(fā)現(xiàn)罪魁禍?zhǔn)资?code>sharding-jdbc引起的,因?yàn)閿?shù)據(jù)源是sharding-jdbc
的,導(dǎo)致后續(xù)執(zhí)行sql
的是ShardingPreparedStatement
。
這就意味著,sharding-jdbc
影響項(xiàng)目的所有業(yè)務(wù)表,因?yàn)樽罱K數(shù)據(jù)庫(kù)交互都由ShardingPreparedStatement
去做了,歷史的一些sql
語(yǔ)句因?yàn)?code>sql函數(shù)或者其他寫法,使得ShardingPreparedStatement
無(wú)法處理而出現(xiàn)異常。
關(guān)鍵代碼如下
發(fā)現(xiàn)問(wèn)題后,阿星馬上就反饋給leader
了。
唉,本來(lái)還想摸魚的,看來(lái)摸魚的時(shí)間是沒(méi)了,還多了一項(xiàng)任務(wù)。
分析
竟然交給阿星來(lái)做了,就擼起袖子開干吧,先看看分表功能的需求
- 支持自定義分表策略
- 能控制影響范圍
- 通用性
分表會(huì)提前建立好,所以不需要考慮表不存在的問(wèn)題,核心邏輯實(shí)現(xiàn),通過(guò)分表策略得到分表名,再把分表名動(dòng)態(tài)替換到sql
。
分表策略
為了支持分表策略,我們需要先定義分表策略抽象接口,定義如下
/** * @Author 程序猿阿星 * @Description 分表策略接口 * @Date 2021/5/9 */ public interface ITableShardStrategy { /** * @author: 程序猿阿星 * @description: 生成分表名 * @param tableNamePrefix 表前綴名 * @param value 值 * @date: 2021/5/9 * @return: java.lang.String */ String generateTableName(String tableNamePrefix,Object value); /** * 驗(yàn)證tableNamePrefix */ default void verificationTableNamePrefix(String tableNamePrefix){ if (StrUtil.isBlank(tableNamePrefix)) { throw new RuntimeException("tableNamePrefix is null"); } } }
generateTableName
函數(shù)的任務(wù)就是生成分表名,入?yún)⒂?code>tableNamePrefix、value,tableNamePrefix
為分表前綴,value
作為生成分表名的邏輯參數(shù)。
verificationTableNamePrefix
函數(shù)驗(yàn)證tableNamePrefix
必填,提供給實(shí)現(xiàn)類使用。
為了方便理解,下面是id
取模策略代碼,取模兩張表
/** * @Author 程序猿阿星 * @Description 分表策略id * @Date 2021/5/9 */ @Component public class TableShardStrategyId implements ITableShardStrategy { @Override public String generateTableName(String tableNamePrefix, Object value) { verificationTableNamePrefix(tableNamePrefix); if (value == null || StrUtil.isBlank(value.toString())) { throw new RuntimeException("value is null"); } long id = Long.parseLong(value.toString()); //此處可以緩存優(yōu)化 return tableNamePrefix + "_" + (id % 2); } }
傳入進(jìn)來(lái)的value
是id
值,用tableNamePrefix
拼接id
取模后的值,得到分表名返回。
控制影響范圍
分表策略已經(jīng)抽象出來(lái),下面要考慮控制影響范圍,我們都知道Mybatis
規(guī)范中每個(gè)Mapper
類對(duì)應(yīng)一張業(yè)務(wù)主體表,Mapper
類的函數(shù)對(duì)應(yīng)業(yè)務(wù)主體表的相關(guān)sql
。
阿星想著,可以給Mapper
類打上注解,代表該Mpaaer
類對(duì)應(yīng)的業(yè)務(wù)主體表有分表需求,從規(guī)范來(lái)說(shuō)Mapper
類的每個(gè)函數(shù)對(duì)應(yīng)的主體表都是正確的,但是有些同學(xué)可能不會(huì)按規(guī)范來(lái)寫。
假設(shè)Mpaaer
類對(duì)應(yīng)的是B
表,Mpaaer
類的某個(gè)函數(shù)寫著A
表的sql
,甚至是歷史遺留問(wèn)題,所以注解不僅僅可以打在Mapper
類上,同時(shí)還可以打在Mapper
類的任意一個(gè)函數(shù)上,并且保證小粒度覆蓋粗粒度。
阿星這里自定義分表注解,代碼如下
/** * @Author 程序猿阿星 * @Description 分表注解 * @Date 2021/5/9 */ @Target(value = {ElementType.TYPE,ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface TableShard { // 表前綴名 String tableNamePrefix(); //值 String value() default ""; //是否是字段名,如果是需要解析請(qǐng)求參數(shù)改字段名的值(默認(rèn)否) boolean fieldFlag() default false; // 對(duì)應(yīng)的分表策略類 Class<? extends ITableShardStrategy> shardStrategy(); }
注解的作用范圍是類、接口、函數(shù),運(yùn)行時(shí)生效。
tableNamePrefix
與shardStrategy
屬性都好理解,表前綴名和分表策略,剩下的value
與fieldFlag
要怎么理解,分表策略分兩類,第一類依賴表中某個(gè)字段值,第二類則不依賴。
根據(jù)企業(yè)id
取模,屬于第一類,此處的value
設(shè)置企業(yè)id
入?yún)⒆侄蚊?code>fieldFlag為true
,意味著,會(huì)去解析獲取企業(yè)id
字段名對(duì)應(yīng)的值。
根據(jù)日期分表,屬于第二類,直接在分表策略實(shí)現(xiàn)類里面寫就行了,不依賴表字段值,value
與fieldFlag
無(wú)需填寫,當(dāng)然你value
也可以設(shè)置時(shí)間格式,具體看分表策略實(shí)現(xiàn)類的邏輯。
通用性
抽象分表策略與分表注解都搞定了,最后一步就是根據(jù)分表注解信息,去執(zhí)行分表策略得到分表名,再把分表名動(dòng)態(tài)替換到sql
中,同時(shí)具有通用性。
Mybatis
框架中,有攔截器機(jī)制做擴(kuò)展,我們只需要攔截StatementHandler#prepare
函數(shù),即StatementHandle
創(chuàng)建Statement
之前,先把sql
里面的表名動(dòng)態(tài)替換成分表名。
Mybatis
分表攔截器流程圖如下
Mybatis
分表攔截器代碼如下,有點(diǎn)長(zhǎng)哈,主流程看intercept
函數(shù)就好了。
/** * @Author 程序員阿星 * @Description 分表攔截器 * @Date 2021/5/9 */ @Intercepts({ @Signature( type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class} ) }) public class TableShardInterceptor implements Interceptor { private static final ReflectorFactory defaultReflectorFactory = new DefaultReflectorFactory(); @Override public Object intercept(Invocation invocation) throws Throwable { // MetaObject是mybatis里面提供的一個(gè)工具類,類似反射的效果 MetaObject metaObject = getMetaObject(invocation); BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql"); MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement"); //獲取Mapper執(zhí)行方法 Method method = invocation.getMethod(); //獲取分表注解 TableShard tableShard = getTableShard(method,mappedStatement); // 如果method與class都沒(méi)有TableShard注解或執(zhí)行方法不存在,執(zhí)行下一個(gè)插件邏輯 if (tableShard == null) { return invocation.proceed(); } //獲取值 String value = tableShard.value(); //value是否字段名,如果是,需要解析請(qǐng)求參數(shù)字段名的值 boolean fieldFlag = tableShard.fieldFlag(); if (fieldFlag) { //獲取請(qǐng)求參數(shù) Object parameterObject = boundSql.getParameterObject(); if (parameterObject instanceof MapperMethod.ParamMap) { //ParamMap類型邏輯處理 MapperMethod.ParamMap parameterMap = (MapperMethod.ParamMap) parameterObject; //根據(jù)字段名獲取參數(shù)值 Object valueObject = parameterMap.get(value); if (valueObject == null) { throw new RuntimeException(String.format("入?yún)⒆侄?s無(wú)匹配", value)); } //替換sql replaceSql(tableShard, valueObject, metaObject, boundSql); } else { //單參數(shù)邏輯 //如果是基礎(chǔ)類型拋出異常 if (isBaseType(parameterObject)) { throw new RuntimeException("單參數(shù)非法,請(qǐng)使用@Param注解"); } if (parameterObject instanceof Map){ Map<String,Object> parameterMap = (Map<String,Object>)parameterObject; Object valueObject = parameterMap.get(value); //替換sql replaceSql(tableShard, valueObject, metaObject, boundSql); } else { //非基礎(chǔ)類型對(duì)象 Class<?> parameterObjectClass = parameterObject.getClass(); Field declaredField = parameterObjectClass.getDeclaredField(value); declaredField.setAccessible(true); Object valueObject = declaredField.get(parameterObject); //替換sql replaceSql(tableShard, valueObject, metaObject, boundSql); } } } else {//無(wú)需處理parameterField //替換sql replaceSql(tableShard, value, metaObject, boundSql); } //執(zhí)行下一個(gè)插件邏輯 return invocation.proceed(); } @Override public Object plugin(Object target) { // 當(dāng)目標(biāo)類是StatementHandler類型時(shí),才包裝目標(biāo)類,否者直接返回目標(biāo)本身, 減少目標(biāo)被代理的次數(shù) if (target instanceof StatementHandler) { return Plugin.wrap(target, this); } else { return target; } } /** * @param object * @methodName: isBaseType * @author: 程序員阿星 * @description: 基本數(shù)據(jù)類型驗(yàn)證,true是,false否 * @date: 2021/5/9 * @return: boolean */ private boolean isBaseType(Object object) { if (object.getClass().isPrimitive() || object instanceof String || object instanceof Integer || object instanceof Double || object instanceof Float || object instanceof Long || object instanceof Boolean || object instanceof Byte || object instanceof Short) { return true; } else { return false; } } /** * @param tableShard 分表注解 * @param value 值 * @param metaObject mybatis反射對(duì)象 * @param boundSql sql信息對(duì)象 * @author: 程序猿阿星 * @description: 替換sql * @date: 2021/5/9 * @return: void */ private void replaceSql(TableShard tableShard, Object value, MetaObject metaObject, BoundSql boundSql) { String tableNamePrefix = tableShard.tableNamePrefix(); //獲取策略class Class<? extends ITableShardStrategy> strategyClazz = tableShard.shardStrategy(); //從spring ioc容器獲取策略類 ITableShardStrategy tableShardStrategy = SpringUtil.getBean(strategyClazz); //生成分表名 String shardTableName = tableShardStrategy.generateTableName(tableNamePrefix, value); // 獲取sql String sql = boundSql.getSql(); // 完成表名替換 metaObject.setValue("delegate.boundSql.sql", sql.replaceAll(tableNamePrefix, shardTableName)); } /** * @param invocation * @author: 程序猿阿星 * @description: 獲取MetaObject對(duì)象-mybatis里面提供的一個(gè)工具類,類似反射的效果 * @date: 2021/5/9 * @return: org.apache.ibatis.reflection.MetaObject */ private MetaObject getMetaObject(Invocation invocation) { StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); // MetaObject是mybatis里面提供的一個(gè)工具類,類似反射的效果 MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, defaultReflectorFactory ); return metaObject; } /** * @author: 程序猿阿星 * @description: 獲取分表注解 * @param method * @param mappedStatement * @date: 2021/5/9 * @return: com.xing.shard.interceptor.TableShard */ private TableShard getTableShard(Method method, MappedStatement mappedStatement) throws ClassNotFoundException { String id = mappedStatement.getId(); //獲取Class final String className = id.substring(0, id.lastIndexOf(".")); //分表注解 TableShard tableShard = null; //獲取Mapper執(zhí)行方法的TableShard注解 tableShard = method.getAnnotation(TableShard.class); //如果方法沒(méi)有設(shè)置注解,從Mapper接口上面獲取TableShard注解 if (tableShard == null) { // 獲取TableShard注解 tableShard = Class.forName(className).getAnnotation(TableShard.class); } return tableShard; } }
到了這里,其實(shí)分表功能就已經(jīng)完成了,我們只需要把分表策略抽象接口、分表注解、分表攔截器抽成一個(gè)通用jar
包,需要使用的項(xiàng)目引入這個(gè)jar
,然后注冊(cè)分表攔截器,自己根據(jù)業(yè)務(wù)需求實(shí)現(xiàn)分表策略,在給對(duì)應(yīng)的Mpaaer
加上分表注解就好了。
實(shí)踐跑起來(lái)
這里阿星單獨(dú)寫了一套demo
,場(chǎng)景是有兩個(gè)分表策略,表也提前建立好了
- 根據(jù)
id
分表 tb_log_id_0
tb_log_id_1
- 根據(jù)日期分表
tb_log_date_202105
tb_log_date_202106
預(yù)警:后面都是代碼實(shí)操環(huán)節(jié),請(qǐng)各位讀者大大耐心看完(非Java開發(fā)除外)。
TableShardStrategy定義
/** * @Author wx * @Description 分表策略日期 * @Date 2021/5/9 */ @Component public class TableShardStrategyDate implements ITableShardStrategy { private static final String DATE_PATTERN = "yyyyMM"; @Override public String generateTableName(String tableNamePrefix, Object value) { verificationTableNamePrefix(tableNamePrefix); if (value == null || StrUtil.isBlank(value.toString())) { return tableNamePrefix + "_" +DateUtil.format(new Date(), DATE_PATTERN); } else { return tableNamePrefix + "_" +DateUtil.format(new Date(), value.toString()); } } } ** * @Author 程序猿阿星 * @Description 分表策略id * @Date 2021/5/9 */ @Component public class TableShardStrategyId implements ITableShardStrategy { @Override public String generateTableName(String tableNamePrefix, Object value) { verificationTableNamePrefix(tableNamePrefix); if (value == null || StrUtil.isBlank(value.toString())) { throw new RuntimeException("value is null"); } long id = Long.parseLong(value.toString()); //可以加入本地緩存優(yōu)化 return tableNamePrefix + "_" + (id % 2); } }
Mapper定義
Mapper接口
/** * @Author 程序猿阿星 * @Description * @Date 2021/5/8 */ @TableShard(tableNamePrefix = "tb_log_date",shardStrategy = TableShardStrategyDate.class) public interface LogDateMapper { /** * 查詢列表-根據(jù)日期分表 */ List<LogDate> queryList(); /** * 單插入-根據(jù)日期分表 */ void save(LogDate logDate); } ------------------------------------------------------------------------------------------------- /** * @Author 程序猿阿星 * @Description * @Date 2021/5/8 */ @TableShard(tableNamePrefix = "tb_log_id",value = "id",fieldFlag = true,shardStrategy = TableShardStrategyId.class) public interface LogIdMapper { /** * 根據(jù)id查詢-根據(jù)id分片 */ LogId queryOne(@Param("id") long id); /** * 單插入-根據(jù)id分片 */ void save(LogId logId); }
Mapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.xing.shard.mapper.LogDateMapper"> //對(duì)應(yīng)LogDateMapper#queryList函數(shù) <select id="queryList" resultType="com.xing.shard.entity.LogDate"> select id as id, comment as comment, create_date as createDate from tb_log_date </select> //對(duì)應(yīng)LogDateMapper#save函數(shù) <insert id="save" > insert into tb_log_date(id, comment,create_date) values (#{id}, #{comment},#{createDate}) </insert> </mapper> ------------------------------------------------------------------------------------------------- <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.xing.shard.mapper.LogIdMapper"> //對(duì)應(yīng)LogIdMapper#queryOne函數(shù) <select id="queryOne" resultType="com.xing.shard.entity.LogId"> select id as id, comment as comment, create_date as createDate from tb_log_id where id = #{id} </select> //對(duì)應(yīng)save函數(shù) <insert id="save" > insert into tb_log_id(id, comment,create_date) values (#{id}, #{comment},#{createDate}) </insert> </mapper>
執(zhí)行下單元測(cè)試
日期分表單元測(cè)試執(zhí)行
@Test void test() { LogDate logDate = new LogDate(); logDate.setId(snowflake.nextId()); logDate.setComment("測(cè)試內(nèi)容"); logDate.setCreateDate(new Date()); //插入 logDateMapper.save(logDate); //查詢 List<LogDate> logDates = logDateMapper.queryList(); System.out.println(JSONUtil.toJsonPrettyStr(logDates)); }
輸出結(jié)果
id
分表單元測(cè)試執(zhí)行
@Test void test() { LogId logId = new LogId(); long id = snowflake.nextId(); logId.setId(id); logId.setComment("測(cè)試"); logId.setCreateDate(new Date()); //插入 logIdMapper.save(logId); //查詢 LogId logIdObject = logIdMapper.queryOne(id); System.out.println(JSONUtil.toJsonPrettyStr(logIdObject)); }
輸出結(jié)果
小結(jié)一下
本文可以當(dāng)做對(duì)Mybatis
進(jìn)階的使用教程,通過(guò)Mybatis
攔截器實(shí)現(xiàn)分表的功能,滿足基本的業(yè)務(wù)需求,雖然比較簡(jiǎn)陋,但是Mybatis
這種擴(kuò)展機(jī)制與設(shè)計(jì)值得學(xué)習(xí)思考。
有興趣的讀者也可以自己寫一個(gè),或基于阿星的做改造,畢竟是簡(jiǎn)陋版本,還是有很多場(chǎng)景沒(méi)有考慮到。
另外分表的demo
項(xiàng)目,阿星放到了Gitee
,大家按需自取
Gitee地址: https://gitee.com/jxncwx/shard
項(xiàng)目結(jié)構(gòu):
到此這篇關(guān)于Mybatis實(shí)現(xiàn)分表插件的文章就介紹到這了,更多相關(guān)Mybatis 分表插件內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- MyBatis實(shí)現(xiàn)Mysql數(shù)據(jù)庫(kù)分庫(kù)分表操作和總結(jié)(推薦)
- 簡(jiǎn)單易懂的MyBatis分庫(kù)分表方案分享
- Spring Boot 集成 Sharding-JDBC + Mybatis-Plus 實(shí)現(xiàn)分庫(kù)分表功能
- Mybatis-plus使用TableNameHandler分表詳解(附完整示例源碼)
- springboot+mybatis-plus基于攔截器實(shí)現(xiàn)分表的示例代碼
- SQL數(shù)據(jù)分表Mybatis?Plus動(dòng)態(tài)表名優(yōu)方案
- 詳解mybatis如何實(shí)現(xiàn)進(jìn)行分表
- Mybatis-Plus集成Sharding-JDBC與Flyway實(shí)現(xiàn)多租戶分庫(kù)分表實(shí)戰(zhàn)
- Mybatis攔截器實(shí)現(xiàn)一種百萬(wàn)級(jí)輕量分表方案
相關(guān)文章
Java的中l(wèi)ombok下的@Builder注解用法詳解
這篇文章主要介紹了Java的中l(wèi)ombok下的@Builder注解用法詳解,lombok注解在java進(jìn)行編譯時(shí)進(jìn)行代碼的構(gòu)建,對(duì)于java對(duì)象的創(chuàng)建工作它可以更優(yōu)雅,不需要寫多余的重復(fù)的代碼,在出現(xiàn)lombok之后,對(duì)象的創(chuàng)建工作更提供Builder方法,需要的朋友可以參考下2023-11-11如何在 Linux 上搭建 java 部署環(huán)境(安裝jdk/tomcat/mys
這篇文章主要介紹了如何在 Linux 上搭建 java 部署環(huán)境(安裝jdk/tomcat/mysql) + 將程序部署到云服務(wù)器上的操作),本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-01-01Java基于Socket實(shí)現(xiàn)簡(jiǎn)單的多線程回顯服務(wù)器功能示例
這篇文章主要介紹了Java基于Socket實(shí)現(xiàn)簡(jiǎn)單的多線程回顯服務(wù)器功能,結(jié)合實(shí)例形式分析了java使用socket進(jìn)行多線程數(shù)據(jù)傳輸?shù)南嚓P(guān)操作技巧,需要的朋友可以參考下2017-08-08使用Java servlet實(shí)現(xiàn)自動(dòng)登錄退出功能
這篇文章主要介紹了使用Java servlet實(shí)現(xiàn)自動(dòng)登錄退出功能,,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-11-11maven坐標(biāo)Dependencies和Exclusions的使用
這篇文章主要介紹了maven坐標(biāo)Dependencies和Exclusions的使用,很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12關(guān)于HashMap 并發(fā)時(shí)會(huì)引起死循環(huán)的問(wèn)題解析
JDK1.8之前采用頭插,即在鏈表結(jié)構(gòu)上每次都把數(shù)據(jù)放在鏈表頭部。JDK1.8采用尾插方法,很多朋友在學(xué)習(xí)Java并發(fā)容器和框架時(shí),看到為什么要使用ConcurrentHashMap時(shí)不知道究其原因,今天小編通過(guò)本文給大家介紹下HashMap 并發(fā)死循環(huán)問(wèn)題,一起看看吧2021-05-05淺析SpringBoot統(tǒng)一返回結(jié)果的實(shí)現(xiàn)
前后端開發(fā)過(guò)程中數(shù)據(jù)交互規(guī)范化是一件非常重要的事情,不僅可以減少前后端交互過(guò)程中出現(xiàn)的問(wèn)題,也讓代碼邏輯更加具有條理,下面小編就和大家講講SpringBoot如何統(tǒng)一返回結(jié)果的吧2023-07-07Java14對(duì)于NullPointerException的新處理方式示例解析
這篇文章主要為大家介紹了Java14對(duì)于NullPointerException的新處理方式示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09