<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          基于Mybatis手?jǐn)]一個(gè)分表插件

          共 29593字,需瀏覽 60分鐘

           ·

          2021-05-20 23:17

          大家好,我是摸魚失敗的阿星

          背景

          事情是醬紫的,阿星的上級(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 style="font-size: 14px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;color: rgb(19, 148, 216);padding: 2px 6px;word-break: normal;">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 style="font-size: 14px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;color: rgb(19, 148, 216);padding: 2px 6px;word-break: normal;">sql函數(shù)或者其他寫法,使得ShardingPreparedStatement無(wú)法處理而出現(xiàn)異常。

          關(guān)鍵代碼如下

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

          唉,本來(lái)還想摸魚的,看來(lái)摸魚的時(shí)間是沒了,還多了一項(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 style="font-size: 14px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;color: rgb(19, 148, 216);padding: 2px 6px;word-break: normal;">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)的valueid值,用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í)生效。

          tableNamePrefixshardStrategy屬性都好理解,表前綴名和分表策略,剩下的valuefieldFlag要怎么理解,分表策略分兩類,第一類依賴表中某個(gè)字段值,第二類則不依賴。

          根據(jù)企業(yè)id取模,屬于第一類,此處的value設(shè)置企業(yè)id入?yún)⒆侄蚊?code style="font-size: 14px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;color: rgb(19, 148, 216);padding: 2px 6px;word-break: normal;">fieldFlag為true,意味著,會(huì)去解析獲取企業(yè)id字段名對(duì)應(yīng)的值。

          根據(jù)日期分表,屬于第二類,直接在分表策略實(shí)現(xiàn)類里面寫就行了,不依賴表字段值,valuefieldFlag無(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.classInteger.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都沒有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);
                  //如果方法沒有設(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)景沒有考慮到。

          另外分表的demo項(xiàng)目,阿星放到了Gitee和公眾號(hào),大家按需自取

          • Gitee地址: https://gitee.com/jxncwx/shard
          • 公眾號(hào)回復(fù) fb

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

          后續(xù)計(jì)劃

          給各位讀者大大們報(bào)告2021-5月的安排。


          如果各位讀者大大們,覺得有什么問(wèn)題,隨時(shí)評(píng)論指出,阿星馬上調(diào)整~

          最近給大家找了  百萬(wàn)級(jí)電商


          資源,怎么領(lǐng)取?


          掃二維碼,加我微信,回復(fù):百萬(wàn)級(jí)電商

           注意,不要亂回復(fù) 

          沒錯(cuò),不是機(jī)器人
          記得一定要等待,等待才有好東西


          瀏覽 36
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产女人高潮了视频 | 五月天婷婷激情综合网 | 私蜜免费啪啪视频 | 日韩色情无码 | 美女被暴草视频在线看 |