日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

基于 MyBatis 手撸一个分表插件

發(fā)布時間:2025/3/11 编程问答 34 豆豆
生活随笔 收集整理的這篇文章主要介紹了 基于 MyBatis 手撸一个分表插件 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

背景

事情是醬紫的,上級leader負責(zé)記錄信息的業(yè)務(wù),每日預(yù)估數(shù)據(jù)量是15萬左右,所以引入sharding-jdbc做分表。

上級leader完成業(yè)務(wù)的開發(fā)后,走了一波自測,git push后,就忙其他的事情去了。

項目的框架是SpringBoot+Mybaits

出問題了

因為負責(zé)的業(yè)務(wù)也開發(fā)完了,熟練的git pull,準(zhǔn)備自測,單元測試run一下,上個廁所回來收工,就是這么自信。

回來后,看下控制臺,人都傻了,一片紅,內(nèi)心不禁感嘆“如果這是股票基金該多好”。

出了問題就要解決,隨著排查深入,我的眉頭一皺發(fā)現(xiàn)事情并不簡單,怎么以前的一些代碼都報錯了?

隨著排查深入,最后跟到了Mybatis源碼,發(fā)現(xiàn)罪魁禍?zhǔn)资莝harding-jdbc引起的,因為數(shù)據(jù)源是sharding-jdbc的,導(dǎo)致后續(xù)執(zhí)行sql的是ShardingPreparedStatement。

這就意味著,sharding-jdbc影響項目的所有業(yè)務(wù)表,因為最終數(shù)據(jù)庫交互都由ShardingPreparedStatement去做了,歷史的一些sql語句因為sql函數(shù)或者其他寫法,使得ShardingPreparedStatement無法處理而出現(xiàn)異常。

關(guān)鍵代碼如下

發(fā)現(xiàn)問題后,阿星馬上就反饋給leader了。

唉,本來還想摸魚的,看來摸魚的時間是沒了,還多了一項任務(wù)。

分析

竟然交給阿星來做了,就擼起袖子開干吧,先看看分表功能的需求

  • 支持自定義分表策略

  • 能控制影響范圍

  • 通用性

分表會提前建立好,所以不需要考慮表不存在的問題,核心邏輯實現(xiàn),通過分表策略得到分表名,再把分表名動態(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);/***?驗證tableNamePrefix*/default?void?verificationTableNamePrefix(String?tableNamePrefix){if?(StrUtil.isBlank(tableNamePrefix))?{throw?new?RuntimeException("tableNamePrefix?is?null");}} }

generateTableName函數(shù)的任務(wù)就是生成分表名,入?yún)⒂衪ableNamePrefix、value,tableNamePrefix為分表前綴,value作為生成分表名的邏輯參數(shù)。

verificationTableNamePrefix函數(shù)驗證tableNamePrefix必填,提供給實現(xiàn)類使用。

為了方便理解,下面是id取模策略代碼,取模兩張表

/***?@Author?程序猿阿星*?@Description?分表策略id*?@Date?2021/5/9*/ @Component public?class?TableShardStrategyId?implements?ITableShardStrategy?{@Overridepublic?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);} }

傳入進來的value是id值,用tableNamePrefix拼接id取模后的值,得到分表名返回。

控制影響范圍

分表策略已經(jīng)抽象出來,下面要考慮控制影響范圍,我們都知道Mybatis規(guī)范中每個Mapper類對應(yīng)一張業(yè)務(wù)主體表,Mapper類的函數(shù)對應(yīng)業(yè)務(wù)主體表的相關(guān)sql。

阿星想著,可以給Mapper類打上注解,代表該Mpaaer類對應(yīng)的業(yè)務(wù)主體表有分表需求,從規(guī)范來說Mapper類的每個函數(shù)對應(yīng)的主體表都是正確的,但是有些同學(xué)可能不會按規(guī)范來寫。

假設(shè)Mpaaer類對應(yīng)的是B表,Mpaaer類的某個函數(shù)寫著A表的sql,甚至是歷史遺留問題,所以注解不僅僅可以打在Mapper類上,同時還可以打在Mapper類的任意一個函數(shù)上,并且保證小粒度覆蓋粗粒度。

阿星這里自定義分表注解,代碼如下

/***?@Author?程序猿阿星*?@Description?分表注解*?@Date?2021/5/9*/ @Target(value?=?{ElementType.TYPE,ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public?@interface?TableShard?{//?表前綴名String?tableNamePrefix();//值String?value()?default?"";//是否是字段名,如果是需要解析請求參數(shù)改字段名的值(默認否)boolean?fieldFlag()?default?false;//?對應(yīng)的分表策略類Class<??extends?ITableShardStrategy>?shardStrategy();}

注解的作用范圍是類、接口、函數(shù),運行時生效。

tableNamePrefix與shardStrategy屬性都好理解,表前綴名和分表策略,剩下的value與fieldFlag要怎么理解,分表策略分兩類,第一類依賴表中某個字段值,第二類則不依賴。

根據(jù)企業(yè)id取模,屬于第一類,此處的value設(shè)置企業(yè)id入?yún)⒆侄蚊?#xff0c;fieldFlag為true,意味著,會去解析獲取企業(yè)id字段名對應(yīng)的值。

根據(jù)日期分表,屬于第二類,直接在分表策略實現(xiàn)類里面寫就行了,不依賴表字段值,value與fieldFlag無需填寫,當(dāng)然你value也可以設(shè)置時間格式,具體看分表策略實現(xiàn)類的邏輯。

通用性

抽象分表策略與分表注解都搞定了,最后一步就是根據(jù)分表注解信息,去執(zhí)行分表策略得到分表名,再把分表名動態(tài)替換到sql中,同時具有通用性。

Mybatis框架中,有攔截器機制做擴展,我們只需要攔截StatementHandler#prepare函數(shù),即StatementHandle創(chuàng)建Statement之前,先把sql里面的表名動態(tài)替換成分表名。

Mybatis分表攔截器流程圖如下

Mybatis分表攔截器代碼如下,有點長哈,主流程看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();@Overridepublic?Object?intercept(Invocation?invocation)?throws?Throwable?{//?MetaObject是mybatis里面提供的一個工具類,類似反射的效果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都沒有TableShard注解或執(zhí)行方法不存在,執(zhí)行下一個插件邏輯if?(tableShard?==?null)?{return?invocation.proceed();}//獲取值String?value?=?tableShard.value();//value是否字段名,如果是,需要解析請求參數(shù)字段名的值boolean?fieldFlag?=?tableShard.fieldFlag();if?(fieldFlag)?{//獲取請求參數(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無匹配",?value));}//替換sqlreplaceSql(tableShard,?valueObject,?metaObject,?boundSql);}?else?{?//單參數(shù)邏輯//如果是基礎(chǔ)類型拋出異常if?(isBaseType(parameterObject))?{throw?new?RuntimeException("單參數(shù)非法,請使用@Param注解");}if?(parameterObject?instanceof?Map){Map<String,Object>??parameterMap?=??(Map<String,Object>)parameterObject;Object?valueObject?=?parameterMap.get(value);//替換sqlreplaceSql(tableShard,?valueObject,?metaObject,?boundSql);}?else?{//非基礎(chǔ)類型對象Class<?>?parameterObjectClass?=?parameterObject.getClass();Field?declaredField?=?parameterObjectClass.getDeclaredField(value);declaredField.setAccessible(true);Object?valueObject?=?declaredField.get(parameterObject);//替換sqlreplaceSql(tableShard,?valueObject,?metaObject,?boundSql);}}}?else?{//無需處理parameterField//替換sqlreplaceSql(tableShard,?value,?metaObject,?boundSql);}//執(zhí)行下一個插件邏輯return?invocation.proceed();}@Overridepublic?Object?plugin(Object?target)?{//?當(dāng)目標(biāo)類是StatementHandler類型時,才包裝目標(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ù)類型驗證,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反射對象*?@param?boundSql???sql信息對象*?@author:?程序猿阿星*?@description:?替換sql*?@date:?2021/5/9*?@return:?void*/private?void?replaceSql(TableShard?tableShard,?Object?value,?MetaObject?metaObject,?BoundSql?boundSql)?{String?tableNamePrefix?=?tableShard.tableNamePrefix();//獲取策略classClass<??extends?ITableShardStrategy>?strategyClazz?=?tableShard.shardStrategy();//從spring?ioc容器獲取策略類ITableShardStrategy?tableShardStrategy?=?SpringUtil.getBean(strategyClazz);//生成分表名String?shardTableName?=?tableShardStrategy.generateTableName(tableNamePrefix,?value);//?獲取sqlString?sql?=?boundSql.getSql();//?完成表名替換metaObject.setValue("delegate.boundSql.sql",?sql.replaceAll(tableNamePrefix,?shardTableName));}/***?@param?invocation*?@author:?程序猿阿星*?@description:?獲取MetaObject對象-mybatis里面提供的一個工具類,類似反射的效果*?@date:?2021/5/9*?@return:?org.apache.ibatis.reflection.MetaObject*/private?MetaObject?getMetaObject(Invocation?invocation)?{StatementHandler?statementHandler?=?(StatementHandler)?invocation.getTarget();//?MetaObject是mybatis里面提供的一個工具類,類似反射的效果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();//獲取Classfinal?String?className?=?id.substring(0,?id.lastIndexOf("."));//分表注解TableShard?tableShard?=?null;//獲取Mapper執(zhí)行方法的TableShard注解tableShard?=?method.getAnnotation(TableShard.class);//如果方法沒有設(shè)置注解,從Mapper接口上面獲取TableShard注解if?(tableShard?==?null)?{//?獲取TableShard注解tableShard?=?Class.forName(className).getAnnotation(TableShard.class);}return?tableShard;}}

到了這里,其實分表功能就已經(jīng)完成了,我們只需要把分表策略抽象接口、分表注解、分表攔截器抽成一個通用jar包,需要使用的項目引入這個jar,然后注冊分表攔截器,自己根據(jù)業(yè)務(wù)需求實現(xiàn)分表策略,在給對應(yīng)的Mpaaer加上分表注解就好了。

實踐跑起來

這里阿星單獨寫了一套demo,場景是有兩個分表策略,表也提前建立好了

  • 根據(jù)id分表

    • tb_log_id_0

    • tb_log_id_1

  • 根據(jù)日期分表

    • tb_log_date_202105

    • tb_log_date_202106

預(yù)警:后面都是代碼實操環(huán)節(jié),請各位讀者大大耐心看完(非Java開發(fā)除外)

TableShardStrategy定義

/***?@Author?wx*?@Description?分表策略日期*?@Date?2021/5/9*/ @Component public?class?TableShardStrategyDate?implements?ITableShardStrategy?{private?static?final?String?DATE_PATTERN?=?"yyyyMM";@Overridepublic?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?{@Overridepublic?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?mapperPUBLIC?"-//mybatis.org//DTD?Mapper?3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper?namespace="com.xing.shard.mapper.LogDateMapper">//對應(yīng)LogDateMapper#queryList函數(shù)<select?id="queryList"?resultType="com.xing.shard.entity.LogDate">selectid?as?id,comment?as?comment,create_date?as?createDatefromtb_log_date</select>//對應(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?mapperPUBLIC?"-//mybatis.org//DTD?Mapper?3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper?namespace="com.xing.shard.mapper.LogIdMapper">//對應(yīng)LogIdMapper#queryOne函數(shù)<select?id="queryOne"?resultType="com.xing.shard.entity.LogId">selectid?as?id,comment?as?comment,create_date?as?createDatefromtb_log_idwhereid?=?#{id}</select>//對應(yīng)save函數(shù)<insert?id="save"?>insert?into?tb_log_id(id,?comment,create_date)values?(#{id},?#{comment},#{createDate})</insert></mapper>

執(zhí)行下單元測試

日期分表單元測試執(zhí)行

@Testvoid?test()?{LogDate?logDate?=?new?LogDate();logDate.setId(snowflake.nextId());logDate.setComment("測試內(nèi)容");logDate.setCreateDate(new?Date());//插入logDateMapper.save(logDate);//查詢List<LogDate>?logDates?=?logDateMapper.queryList();System.out.println(JSONUtil.toJsonPrettyStr(logDates));}

輸出結(jié)果


id分表單元測試執(zhí)行

@Testvoid?test()?{LogId?logId?=?new?LogId();long?id?=?snowflake.nextId();logId.setId(id);logId.setComment("測試");logId.setCreateDate(new?Date());//插入logIdMapper.save(logId);//查詢LogId?logIdObject?=?logIdMapper.queryOne(id);System.out.println(JSONUtil.toJsonPrettyStr(logIdObject));}

輸出結(jié)果

小結(jié)一下

本文可以當(dāng)做對Mybatis進階的使用教程,通過Mybatis攔截器實現(xiàn)分表的功能,滿足基本的業(yè)務(wù)需求,雖然比較簡陋,但是Mybatis這種擴展機制與設(shè)計值得學(xué)習(xí)思考。

有興趣的讀者也可以自己寫一個,或基于阿星的做改造,畢竟是簡陋版本,還是有很多場景沒有考慮到。

另外分表的demo項目,放到了Gitee和公眾號,大家按需自取

- Gitee地址: https://gitee.com/jxncwx/shard

項目結(jié)構(gòu):

往期推薦

MyBatis 中為什么不建議使用 where 1=1?


SpringBoot 使用注解實現(xiàn)消息廣播功能


聊聊接口性能優(yōu)化的11個小技巧


總結(jié)

以上是生活随笔為你收集整理的基于 MyBatis 手撸一个分表插件的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。