当前位置:   article > 正文

基于Mybatis手撸一个分表插件

parameterobject instanceof mappermethod.parammap

大家好,我是摸鱼失败的阿星

背景

事情是酱紫的,阿星的上级leader负责记录信息的业务,每日预估数据量是15万左右,所以引入sharding-jdbc做分表。

上级leader完成业务的开发后,走了一波自测,git push后,就忙其他的事情去了。

项目的框架是SpringBoot+Mybaits

出问题了

阿星负责的业务也开发完了,熟练的git pull,准备自测,单元测试run一下,上个厕所回来收工,就是这么自信。

回来后,看下控制台,人都傻了,一片红,内心不禁感叹“如果这是股票基金该多好”。

出了问题就要解决,随着排查深入,我的眉头一皱发现事情并不简单,怎么以前的一些代码都报错了?

随着排查深入,最后跟到了Mybatis源码,发现罪魁祸首是sharding-jdbc引起的,因为数据源是sharding-jdbc的,导致后续执行sql的是ShardingPreparedStatement

这就意味着,sharding-jdbc影响项目的所有业务表,因为最终数据库交互都由ShardingPreparedStatement去做了,历史的一些sql语句因为sql函数或者其他写法,使得ShardingPreparedStatement无法处理而出现异常。

关键代码如下

发现问题后,阿星马上就反馈给leader了。

唉,本来还想摸鱼的,看来摸鱼的时间是没了,还多了一项任务。

分析

竟然交给阿星来做了,就撸起袖子开干吧,先看看分表功能的需求

  • 支持自定义分表策略

  • 能控制影响范围

  • 通用性

分表会提前建立好,所以不需要考虑表不存在的问题,核心逻辑实现,通过分表策略得到分表名,再把分表名动态替换到sql

分表策略

为了支持分表策略,我们需要先定义分表策略抽象接口,定义如下

  1. /**
  2.  * @Author 程序猿阿星
  3.  * @Description 分表策略接口
  4.  * @Date 2021/5/9
  5.  */
  6. public interface ITableShardStrategy {
  7.     /**
  8.      * @author: 程序猿阿星
  9.      * @description: 生成分表名
  10.      * @param tableNamePrefix 表前缀名
  11.      * @param value 值
  12.      * @date: 2021/5/9
  13.      * @return: java.lang.String
  14.      */
  15.     String generateTableName(String tableNamePrefix,Object value);
  16.     /**
  17.      * 验证tableNamePrefix
  18.      */
  19.     default void verificationTableNamePrefix(String tableNamePrefix){
  20.         if (StrUtil.isBlank(tableNamePrefix)) {
  21.             throw new RuntimeException("tableNamePrefix is null");
  22.         }
  23.     }
  24. }

generateTableName函数的任务就是生成分表名,入参有tableNamePrefix、valuetableNamePrefix为分表前缀,value作为生成分表名的逻辑参数。

verificationTableNamePrefix函数验证tableNamePrefix必填,提供给实现类使用。

为了方便理解,下面是id取模策略代码,取模两张表

  1. /**
  2.  * @Author 程序猿阿星
  3.  * @Description 分表策略id
  4.  * @Date 2021/5/9
  5.  */
  6. @Component
  7. public class TableShardStrategyId implements ITableShardStrategy {
  8.     @Override
  9.     public String generateTableName(String tableNamePrefix, Object value) {
  10.         verificationTableNamePrefix(tableNamePrefix);
  11.         if (value == null || StrUtil.isBlank(value.toString())) {
  12.             throw new RuntimeException("value is null");
  13.         }
  14.         long id = Long.parseLong(value.toString());
  15.         //此处可以缓存优化
  16.         return tableNamePrefix + "_" + (id % 2);
  17.     }
  18. }

传入进来的valueid值,用tableNamePrefix拼接id取模后的值,得到分表名返回。

控制影响范围

分表策略已经抽象出来,下面要考虑控制影响范围,我们都知道Mybatis规范中每个Mapper类对应一张业务主体表,Mapper类的函数对应业务主体表的相关sql

阿星想着,可以给Mapper类打上注解,代表该Mpaaer类对应的业务主体表有分表需求,从规范来说Mapper类的每个函数对应的主体表都是正确的,但是有些同学可能不会按规范来写。

假设Mpaaer类对应的是B表,Mpaaer类的某个函数写着A表的sql,甚至是历史遗留问题,所以注解不仅仅可以打在Mapper类上,同时还可以打在Mapper类的任意一个函数上,并且保证小粒度覆盖粗粒度。

阿星这里自定义分表注解,代码如下

  1. /**
  2.  * @Author 程序猿阿星
  3.  * @Description 分表注解
  4.  * @Date 2021/5/9
  5.  */
  6. @Target(value = {ElementType.TYPE,ElementType.METHOD})
  7. @Retention(RetentionPolicy.RUNTIME)
  8. public @interface TableShard {
  9.     // 表前缀名
  10.     String tableNamePrefix();
  11.     //值
  12.     String value() default "";
  13.     //是否是字段名,如果是需要解析请求参数改字段名的值(默认否)
  14.     boolean fieldFlag() default false;
  15.     // 对应的分表策略类
  16.     Class<? extends ITableShardStrategy> shardStrategy();
  17. }

注解的作用范围是类、接口、函数,运行时生效。

tableNamePrefixshardStrategy属性都好理解,表前缀名和分表策略,剩下的valuefieldFlag要怎么理解,分表策略分两类,第一类依赖表中某个字段值,第二类则不依赖。

根据企业id取模,属于第一类,此处的value设置企业id入参字段名,fieldFlagtrue,意味着,会去解析获取企业id字段名对应的值。

根据日期分表,属于第二类,直接在分表策略实现类里面写就行了,不依赖表字段值,valuefieldFlag无需填写,当然你value也可以设置时间格式,具体看分表策略实现类的逻辑。

通用性

抽象分表策略与分表注解都搞定了,最后一步就是根据分表注解信息,去执行分表策略得到分表名,再把分表名动态替换到sql中,同时具有通用性。

Mybatis框架中,有拦截器机制做扩展,我们只需要拦截StatementHandler#prepare函数,即StatementHandle创建Statement之前,先把sql里面的表名动态替换成分表名。

Mybatis分表拦截器流程图如下

Mybatis分表拦截器代码如下,有点长哈,主流程看intercept函数就好了。

  1. /**
  2.  * @Author 程序员阿星
  3.  * @Description 分表拦截器
  4.  * @Date 2021/5/9
  5.  */
  6. @Intercepts({
  7.         @Signature(
  8.                 type = StatementHandler.class,
  9.                 method = "prepare",
  10.                 args = {Connection.class, Integer.class}
  11.         )
  12. })
  13. public class TableShardInterceptor implements Interceptor {
  14.     private static final ReflectorFactory defaultReflectorFactory = new DefaultReflectorFactory();
  15.     @Override
  16.     public Object intercept(Invocation invocation) throws Throwable {
  17.         // MetaObject是mybatis里面提供的一个工具类,类似反射的效果
  18.         MetaObject metaObject = getMetaObject(invocation);
  19.         BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
  20.         MappedStatement mappedStatement = (MappedStatement)
  21.                 metaObject.getValue("delegate.mappedStatement");
  22.         //获取Mapper执行方法
  23.         Method method = invocation.getMethod();
  24.         //获取分表注解
  25.         TableShard tableShard = getTableShard(method,mappedStatement);
  26.         // 如果method与class都没有TableShard注解或执行方法不存在,执行下一个插件逻辑
  27.         if (tableShard == null) {
  28.             return invocation.proceed();
  29.         }
  30.         //获取值
  31.         String value = tableShard.value();
  32.         //value是否字段名,如果是,需要解析请求参数字段名的值
  33.         boolean fieldFlag = tableShard.fieldFlag();
  34.         if (fieldFlag) {
  35.             //获取请求参数
  36.             Object parameterObject = boundSql.getParameterObject();
  37.             if (parameterObject instanceof MapperMethod.ParamMap) { //ParamMap类型逻辑处理
  38.                 MapperMethod.ParamMap parameterMap = (MapperMethod.ParamMap) parameterObject;
  39.                 //根据字段名获取参数值
  40.                 Object valueObject = parameterMap.get(value);
  41.                 if (valueObject == null) {
  42.                     throw new RuntimeException(String.format("入参字段%s无匹配", value));
  43.                 }
  44.                 //替换sql
  45.                 replaceSql(tableShard, valueObject, metaObject, boundSql);
  46.             } else { //单参数逻辑
  47.                 //如果是基础类型抛出异常
  48.                 if (isBaseType(parameterObject)) {
  49.                     throw new RuntimeException("单参数非法,请使用@Param注解");
  50.                 }
  51.                 if (parameterObject instanceof Map){
  52.                     Map<String,Object>  parameterMap =  (Map<String,Object>)parameterObject;
  53.                     Object valueObject = parameterMap.get(value);
  54.                     //替换sql
  55.                     replaceSql(tableShard, valueObject, metaObject, boundSql);
  56.                 } else {
  57.                     //非基础类型对象
  58.                     Class<?> parameterObjectClass = parameterObject.getClass();
  59.                     Field declaredField = parameterObjectClass.getDeclaredField(value);
  60.                     declaredField.setAccessible(true);
  61.                     Object valueObject = declaredField.get(parameterObject);
  62.                     //替换sql
  63.                     replaceSql(tableShard, valueObject, metaObject, boundSql);
  64.                 }
  65.             }
  66.         } else {//无需处理parameterField
  67.             //替换sql
  68.             replaceSql(tableShard, value, metaObject, boundSql);
  69.         }
  70.         //执行下一个插件逻辑
  71.         return invocation.proceed();
  72.     }
  73.     @Override
  74.     public Object plugin(Object target) {
  75.         // 当目标类是StatementHandler类型时,才包装目标类,否者直接返回目标本身, 减少目标被代理的次数
  76.         if (target instanceof StatementHandler) {
  77.             return Plugin.wrap(target, this);
  78.         } else {
  79.             return target;
  80.         }
  81.     }
  82.     /**
  83.      * @param object
  84.      * @methodName: isBaseType
  85.      * @author: 程序员阿星
  86.      * @description: 基本数据类型验证,true是,false否
  87.      * @date: 2021/5/9
  88.      * @return: boolean
  89.      */
  90.     private boolean isBaseType(Object object) {
  91.         if (object.getClass().isPrimitive()
  92.                 || object instanceof String
  93.                 || object instanceof Integer
  94.                 || object instanceof Double
  95.                 || object instanceof Float
  96.                 || object instanceof Long
  97.                 || object instanceof Boolean
  98.                 || object instanceof Byte
  99.                 || object instanceof Short) {
  100.             return true;
  101.         } else {
  102.             return false;
  103.         }
  104.     }
  105.     /**
  106.      * @param tableShard 分表注解
  107.      * @param value      值
  108.      * @param metaObject mybatis反射对象
  109.      * @param boundSql   sql信息对象
  110.      * @author: 程序猿阿星
  111.      * @description: 替换sql
  112.      * @date: 2021/5/9
  113.      * @return: void
  114.      */
  115.     private void replaceSql(TableShard tableShard, Object value, MetaObject metaObject, BoundSql boundSql) {
  116.         String tableNamePrefix = tableShard.tableNamePrefix();
  117.         //获取策略class
  118.         Class<? extends ITableShardStrategy> strategyClazz = tableShard.shardStrategy();
  119.         //从spring ioc容器获取策略类
  120.         ITableShardStrategy tableShardStrategy = SpringUtil.getBean(strategyClazz);
  121.         //生成分表名
  122.         String shardTableName = tableShardStrategy.generateTableName(tableNamePrefix, value);
  123.         // 获取sql
  124.         String sql = boundSql.getSql();
  125.         // 完成表名替换
  126.         metaObject.setValue("delegate.boundSql.sql", sql.replaceAll(tableNamePrefix, shardTableName));
  127.     }
  128.     /**
  129.      * @param invocation
  130.      * @author: 程序猿阿星
  131.      * @description: 获取MetaObject对象-mybatis里面提供的一个工具类,类似反射的效果
  132.      * @date: 2021/5/9
  133.      * @return: org.apache.ibatis.reflection.MetaObject
  134.      */
  135.     private MetaObject getMetaObject(Invocation invocation) {
  136.         StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
  137.         // MetaObject是mybatis里面提供的一个工具类,类似反射的效果
  138.         MetaObject metaObject = MetaObject.forObject(statementHandler,
  139.                 SystemMetaObject.DEFAULT_OBJECT_FACTORY,
  140.                 SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,
  141.                 defaultReflectorFactory
  142.         );
  143.         return metaObject;
  144.     }
  145.     /**
  146.      * @author: 程序猿阿星
  147.      * @description: 获取分表注解
  148.      * @param method
  149.      * @param mappedStatement
  150.      * @date: 2021/5/9
  151.      * @return: com.xing.shard.interceptor.TableShard
  152.      */
  153.     private TableShard getTableShard(Method method, MappedStatement mappedStatement) throws ClassNotFoundException {
  154.         String id = mappedStatement.getId();
  155.         //获取Class
  156.         final String className = id.substring(0, id.lastIndexOf("."));
  157.         //分表注解
  158.         TableShard tableShard = null;
  159.         //获取Mapper执行方法的TableShard注解
  160.         tableShard = method.getAnnotation(TableShard.class);
  161.         //如果方法没有设置注解,从Mapper接口上面获取TableShard注解
  162.         if (tableShard == null) {
  163.             // 获取TableShard注解
  164.             tableShard = Class.forName(className).getAnnotation(TableShard.class);
  165.         }
  166.         return tableShard;
  167.     }
  168. }

到了这里,其实分表功能就已经完成了,我们只需要把分表策略抽象接口、分表注解、分表拦截器抽成一个通用jar包,需要使用的项目引入这个jar,然后注册分表拦截器,自己根据业务需求实现分表策略,在给对应的Mpaaer加上分表注解就好了。

实践跑起来

这里阿星单独写了一套demo,场景是有两个分表策略,表也提前建立好了

  • 根据id分表

    • tb_log_id_0

    • tb_log_id_1

  • 根据日期分表

    • tb_log_date_202105

    • tb_log_date_202106

预警:后面都是代码实操环节,请各位读者大大耐心看完(非Java开发除外)

TableShardStrategy定义

  1. /**
  2.  * @Author wx
  3.  * @Description 分表策略日期
  4.  * @Date 2021/5/9
  5.  */
  6. @Component
  7. public class TableShardStrategyDate implements ITableShardStrategy {
  8.     private static final String DATE_PATTERN = "yyyyMM";
  9.     @Override
  10.     public String generateTableName(String tableNamePrefix, Object value) {
  11.         verificationTableNamePrefix(tableNamePrefix);
  12.         if (value == null || StrUtil.isBlank(value.toString())) {
  13.             return tableNamePrefix + "_" +DateUtil.format(new Date(), DATE_PATTERN);
  14.         } else {
  15.             return tableNamePrefix + "_" +DateUtil.format(new Date(), value.toString());
  16.         }
  17.     }
  18. }
  19. **
  20.  * @Author 程序猿阿星
  21.  * @Description 分表策略id
  22.  * @Date 2021/5/9
  23.  */
  24. @Component
  25. public class TableShardStrategyId implements ITableShardStrategy {
  26.     @Override
  27.     public String generateTableName(String tableNamePrefix, Object value) {
  28.         verificationTableNamePrefix(tableNamePrefix);
  29.         if (value == null || StrUtil.isBlank(value.toString())) {
  30.             throw new RuntimeException("value is null");
  31.         }
  32.         long id = Long.parseLong(value.toString());
  33.         //可以加入本地缓存优化
  34.         return tableNamePrefix + "_" + (id % 2);
  35.     }
  36. }

Mapper定义

Mapper接口

  1. /**
  2.  * @Author 程序猿阿星
  3.  * @Description
  4.  * @Date 2021/5/8
  5.  */
  6. @TableShard(tableNamePrefix = "tb_log_date",shardStrategy = TableShardStrategyDate.class)
  7. public interface LogDateMapper {
  8.     /**
  9.      * 查询列表-根据日期分表
  10.      */
  11.     List<LogDate> queryList();
  12.     /**
  13.      * 单插入-根据日期分表
  14.      */
  15.     void  save(LogDate logDate);
  16. }
  17. -------------------------------------------------------------------------------------------------
  18. /**
  19.  * @Author 程序猿阿星
  20.  * @Description
  21.  * @Date 2021/5/8
  22.  */
  23. @TableShard(tableNamePrefix = "tb_log_id",value = "id",fieldFlag = true,shardStrategy = TableShardStrategyId.class)
  24. public interface LogIdMapper {
  25.     /**
  26.      * 根据id查询-根据id分片
  27.      */
  28.     LogId queryOne(@Param("id") long id);
  29.     /**
  30.      * 单插入-根据id分片
  31.      */
  32.     void save(LogId logId);
  33. }

Mapper.xml

  1. <?xml version="1.0" encoding="UTF-8" ?>
  2. <!DOCTYPE mapper
  3.         PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  4.         "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  5. <mapper namespace="com.xing.shard.mapper.LogDateMapper">
  6.     
  7.     //对应LogDateMapper#queryList函数
  8.     <select id="queryList" resultType="com.xing.shard.entity.LogDate">
  9.         select
  10.         id as id,
  11.         comment as comment,
  12.         create_date as createDate
  13.         from
  14.         tb_log_date
  15.     </select>
  16.     
  17.     //对应LogDateMapper#save函数
  18.     <insert id="save" >
  19.         insert into tb_log_date(id, comment,create_date)
  20.         values (#{id}, #{comment},#{createDate})
  21.     </insert>
  22. </mapper>
  23. -------------------------------------------------------------------------------------------------
  24. <?xml version="1.0" encoding="UTF-8" ?>
  25. <!DOCTYPE mapper
  26.         PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  27.         "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  28. <mapper namespace="com.xing.shard.mapper.LogIdMapper">
  29.     
  30.     //对应LogIdMapper#queryOne函数
  31.     <select id="queryOne" resultType="com.xing.shard.entity.LogId">
  32.         select
  33.         id as id,
  34.         comment as comment,
  35.         create_date as createDate
  36.         from
  37.         tb_log_id
  38.         where
  39.         id = #{id}
  40.     </select>
  41.     
  42.     //对应save函数
  43.     <insert id="save" >
  44.         insert into tb_log_id(id, comment,create_date)
  45.         values (#{id}, #{comment},#{createDate})
  46.     </insert>
  47. </mapper>

执行下单元测试

日期分表单元测试执行

  1.     @Test
  2.     void test() {
  3.         LogDate logDate = new LogDate();
  4.         logDate.setId(snowflake.nextId());
  5.         logDate.setComment("测试内容");
  6.         logDate.setCreateDate(new Date());
  7.         //插入
  8.         logDateMapper.save(logDate);
  9.         //查询
  10.         List<LogDate> logDates = logDateMapper.queryList();
  11.         System.out.println(JSONUtil.toJsonPrettyStr(logDates));
  12.     }

输出结果


id分表单元测试执行

  1.     @Test
  2.     void test() {
  3.         LogId logId = new LogId();
  4.         long id = snowflake.nextId();
  5.         logId.setId(id);
  6.         logId.setComment("测试");
  7.         logId.setCreateDate(new Date());
  8.         //插入
  9.         logIdMapper.save(logId);
  10.         //查询
  11.         LogId logIdObject = logIdMapper.queryOne(id);
  12.         System.out.println(JSONUtil.toJsonPrettyStr(logIdObject));
  13.     }

输出结果

小结一下

本文可以当做对Mybatis进阶的使用教程,通过Mybatis拦截器实现分表的功能,满足基本的业务需求,虽然比较简陋,但是Mybatis这种扩展机制与设计值得学习思考。

有兴趣的读者也可以自己写一个,或基于阿星的做改造,毕竟是简陋版本,还是有很多场景没有考虑到。

另外分表的demo项目,阿星放到了Gitee和公众号,大家按需自取

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

  • 公众号回复 fb

项目结构:

后续计划

给各位读者大大们报告2021-5月的安排。

如果各位读者大大们,觉得有什么问题,随时评论指出,阿星马上调整~

  1. 最近给大家找了  百万级电商
  2. 资源,怎么领取?
  3. 扫二维码,加我微信,回复:百万级电商
  4.  注意,不要乱回复 
  5. 没错,不是机器人记得一定要等待,等待才有好东西
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/643285
推荐阅读
相关标签
  

闽ICP备14008679号