共計 12087 個字符,預計需要花費 31 分鐘才能閱讀完成。
自動寫代碼機器人,免費開通
這篇文章主要介紹“用 Mybatis 手寫一個分表插件”,在日常操作中,相信很多人在用 Mybatis 手寫一個分表插件問題上存在疑惑,丸趣 TV 小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”用 Mybatis 手寫一個分表插件”的疑惑有所幫助!接下來,請跟著丸趣 TV 小編一起來學習吧!
背景
事情是醬紫的,阿星的上級 leader 負責記錄信息的業(yè)務,每日預估數據量是 15 萬左右,所以引入 sharding-jdbc 做分表。
上級 leader 完成業(yè)務的開發(fā)后,走了一波自測,git push 后,就忙其他的事情去了。
項目的框架是 SpringBoot+Mybaits
出問題了
阿星負責的業(yè)務也開發(fā)完了,熟練的 git pull,準備自測,單元測試 run 一下,上個廁所回來收工,就是這么自信。
回來后,看下控制臺,人都傻了,一片紅,內心不禁感嘆“如果這是股票基金該多好”。
出了問題就要解決,隨著排查深入,我的眉頭一皺發(fā)現事情并不簡單,怎么以前的一些代碼都報錯了?
隨著排查深入,最后跟到了 Mybatis 源碼,發(fā)現罪魁禍首是 sharding-jdbc 引起的,因為數據源是 sharding-jdbc 的,導致后續(xù)執(zhí)行 sql 的是 ShardingPreparedStatement。
這就意味著,sharding-jdbc 影響項目的所有業(yè)務表,因為最終數據庫交互都由 ShardingPreparedStatement 去做了,歷史的一些 sql 語句因為 sql 函數或者其他寫法,使得 ShardingPreparedStatement 無法處理而出現異常。
關鍵代碼如下
發(fā)現問題后,阿星馬上就反饋給 leader 了。
唉,本來還想摸魚的,看來摸魚的時間是沒了,還多了一項任務。
分析
竟然交給阿星來做了,就擼起袖子開干吧,先看看分表功能的需求
支持自定義分表策略
能控制影響范圍
通用性
分表會提前建立好,所以不需要考慮表不存在的問題,核心邏輯實現,通過分表策略得到分表名,再把分表名動態(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 函數的任務就是生成分表名,入參有 tableNamePrefix、value,tableNamePrefix 為分表前綴,value 作為生成分表名的邏輯參數。
verificationTableNamePrefix 函數驗證 tableNamePrefix 必填,提供給實現類使用。
為了方便理解,下面是 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); } }
傳入進來的 value 是 id 值,用 tableNamePrefix 拼接 id 取模后的值,得到分表名返回。
控制影響范圍
分表策略已經抽象出來,下面要考慮控制影響范圍,我們都知道 Mybatis 規(guī)范中每個 Mapper 類對應一張業(yè)務主體表,Mapper 類的函數對應業(yè)務主體表的相關 sql。
阿星想著,可以給 Mapper 類打上注解,代表該 Mpaaer 類對應的業(yè)務主體表有分表需求,從規(guī)范來說 Mapper 類的每個函數對應的主體表都是正確的,但是有些同學可能不會按規(guī)范來寫。
假設 Mpaaer 類對應的是 B 表,Mpaaer 類的某個函數寫著 A 表的 sql,甚至是歷史遺留問題,所以注解不僅僅可以打在 Mapper 類上,同時還可以打在 Mapper 類的任意一個函數上,并且保證小粒度覆蓋粗粒度。
阿星這里自定義分表注解,代碼如下
/** * @Author 程序猿阿星 * @Description 分表注解 * @Date 2021/5/9 */ @Target(value = {ElementType.TYPE,ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface TableShard { // 表前綴名 String tableNamePrefix(); // 值 String value() default // 是否是字段名,如果是需要解析請求參數改字段名的值(默認否) boolean fieldFlag() default false; // 對應的分表策略類 Class ? extends ITableShardStrategy shardStrategy(); }
注解的作用范圍是類、接口、函數,運行時生效。
tableNamePrefix 與 shardStrategy 屬性都好理解,表前綴名和分表策略,剩下的 value 與 fieldFlag 要怎么理解,分表策略分兩類,第一類依賴表中某個字段值,第二類則不依賴。
根據企業(yè) id 取模,屬于第一類,此處的 value 設置企業(yè) id 入參字段名,fieldFlag 為 true,意味著,會去解析獲取企業(yè) id 字段名對應的值。
根據日期分表,屬于第二類,直接在分表策略實現類里面寫就行了,不依賴表字段值,value 與 fieldFlag 無需填寫,當然你 value 也可以設置時間格式,具體看分表策略實現類的邏輯。
通用性
抽象分表策略與分表注解都搞定了,最后一步就是根據分表注解信息,去執(zhí)行分表策略得到分表名,再把分表名動態(tài)替換到 sql 中,同時具有通用性。
Mybatis 框架中,有攔截器機制做擴展,我們只需要攔截 StatementHandler#prepare 函數,即 StatementHandle 創(chuàng)建 Statement 之前,先把 sql 里面的表名動態(tài)替換成分表名。
Mybatis 分表攔截器流程圖如下
Mybatis 分表攔截器代碼如下,有點長哈,主流程看 intercept 函數就好了。
/** * @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 里面提供的一個工具類,類似反射的效果 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 是否字段名,如果是,需要解析請求參數字段名的值 boolean fieldFlag = tableShard.fieldFlag(); if (fieldFlag) { // 獲取請求參數 Object parameterObject = boundSql.getParameterObject(); if (parameterObject instanceof MapperMethod.ParamMap) { //ParamMap 類型邏輯處理 MapperMethod.ParamMap parameterMap = (MapperMethod.ParamMap) parameterObject; // 根據字段名獲取參數值 Object valueObject = parameterMap.get(value); if (valueObject == null) { throw new RuntimeException(String.format( 入參字段 %s 無匹配 , value)); } // 替換 sql replaceSql(tableShard, valueObject, metaObject, boundSql); } else { // 單參數邏輯 // 如果是基礎類型拋出異常 if (isBaseType(parameterObject)) { throw new RuntimeException( 單參數非法,請使用 @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 { // 非基礎類型對象 Class ? parameterObjectClass = parameterObject.getClass(); Field declaredField = parameterObjectClass.getDeclaredField(value); declaredField.setAccessible(true); Object valueObject = declaredField.get(parameterObject); // 替換 sql replaceSql(tableShard, valueObject, metaObject, boundSql); } } } else {// 無需處理 parameterField // 替換 sql replaceSql(tableShard, value, metaObject, boundSql); } // 執(zhí)行下一個插件邏輯 return invocation.proceed(); } @Override public Object plugin(Object target) { // 當目標類是 StatementHandler 類型時,才包裝目標類,否者直接返回目標本身, 減少目標被代理的次數 if (target instanceof StatementHandler) { return Plugin.wrap(target, this); } else { return target; } } /** * @param object * @methodName: isBaseType * @author: 程序員阿星 * @description: 基本數據類型驗證,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(); // 獲取策略 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 對象 -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(); // 獲取 Class final String className = id.substring(0, id.lastIndexOf( .)); // 分表注解 TableShard tableShard = null; // 獲取 Mapper 執(zhí)行方法的 TableShard 注解 tableShard = method.getAnnotation(TableShard.class); // 如果方法沒有設置注解,從 Mapper 接口上面獲取 TableShard 注解 if (tableShard == null) { // 獲取 TableShard 注解 tableShard = Class.forName(className).getAnnotation(TableShard.class); } return tableShard; } }
到了這里,其實分表功能就已經完成了,我們只需要把分表策略抽象接口、分表注解、分表攔截器抽成一個通用 jar 包,需要使用的項目引入這個 jar,然后注冊分表攔截器,自己根據業(yè)務需求實現分表策略,在給對應的 Mpaaer 加上分表注解就好了。
實踐跑起來
這里阿星單獨寫了一套 demo,場景是有兩個分表策略,表也提前建立好了
根據 id 分表
tb_log_id_0
tb_log_id_1
根據日期分表
tb_log_date_202105
tb_log_date_202106
預警:后面都是代碼實操環(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 @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 { /** * 查詢列表 - 根據日期分表 */ List LogDate queryList(); /** * 單插入 - 根據日期分表 */ 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 { /** * 根據 id 查詢 - 根據 id 分片 */ LogId queryOne(@Param( id) long id); /** * 單插入 - 根據 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 // 對應 LogDateMapper#queryList 函數 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 // 對應 LogDateMapper#save 函數 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 // 對應 LogIdMapper#queryOne 函數 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 // 對應 save 函數 insert id= save insert into tb_log_id(id, comment,create_date) values (#{id}, #{comment},#{createDate}) /insert /mapper
執(zhí)行下單元測試
日期分表單元測試執(zhí)行
@Test void test() { LogDate logDate = new LogDate(); logDate.setId(snowflake.nextId()); logDate.setComment(測試內容 logDate.setCreateDate(new Date()); // 插入 logDateMapper.save(logDate); // 查詢 List LogDate logDates = logDateMapper.queryList(); System.out.println(JSONUtil.toJsonPrettyStr(logDates)); }
輸出結果
id 分表單元測試執(zhí)行
@Test void 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)); }
輸出結果
小結一下
本文可以當做對 Mybatis 進階的使用教程,通過 Mybatis 攔截器實現分表的功能,滿足基本的業(yè)務需求,雖然比較簡陋,但是 Mybatis 這種擴展機制與設計值得學習思考。
到此,關于“用 Mybatis 手寫一個分表插件”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續(xù)學習更多相關知識,請繼續(xù)關注丸趣 TV 網站,丸趣 TV 小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p/> 向 AI 問一下細節(jié)