Mybatis实现分表插件

作者:程序猿阿星 时间:2023-07-23 12:41:54 

背景

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

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

项目的框架是SpringBoot+Mybaits

出问题了

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

Mybatis实现分表插件

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

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

Mybatis实现分表插件

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

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

关键代码如下

Mybatis实现分表插件

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

Mybatis实现分表插件

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

分析

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

  • 支持自定义分表策略

  • 能控制影响范围

  • 通用性

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

Mybatis实现分表插件

分表策略

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


/**
* @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、valuetableNamePrefix为分表前缀,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());
       //此处可以缓存优化
       return tableNamePrefix + "_" + (id % 2);
   }
}

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

控制影响范围

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

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

假设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();

}

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

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

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

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

通用性

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

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

Mybatis分表 * 流程图如下

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执行方法
       Method method = invocation.getMethod();

//获取分表注解
       TableShard tableShard = getTableShard(method,mappedStatement);

// 如果method与class都没有TableShard注解或执行方法不存在,执行下一个插件逻辑
       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);
       }
       //执行下一个插件逻辑
       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执行方法的TableShard注解
       tableShard = method.getAnnotation(TableShard.class);
       //如果方法没有设置注解,从Mapper接口上面获取TableShard注解
       if (tableShard == null) {
           // 获取TableShard注解
           tableShard = Class.forName(className).getAnnotation(TableShard.class);
       }
       return tableShard;
   }

}

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

Mybatis实现分表插件

实践跑起来

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

  • 根据id分表

  • tb_log_id_0

  • tb_log_id_1

  • 根据日期分表

  • tb_log_date_202105

  • tb_log_date_202106

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

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());
       //可以加入本地缓存优化
       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>

执行下单元测试

日期分表单元测试执行


   @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));
   }

输出结果

Mybatis实现分表插件

id分表单元测试执行


   @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进阶的使用教程,通过Mybatis * 实现分表的功能,满足基本的业务需求,虽然比较简陋,但是Mybatis这种扩展机制与设计值得学习思考。

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

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

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

项目结构:

Mybatis实现分表插件

来源:https://blog.csdn.net/m0_37199770/article/details/116789071

标签:Mybatis,分表
0
投稿

猜你喜欢

  • Java内存模型与JVM运行时数据区的区别详解

    2023-11-24 13:29:08
  • SpringBoot集成Swagger2的方法

    2023-11-26 13:15:42
  • 利用javaFX实现移动一个小球的示例代码

    2022-06-18 19:04:51
  • C++数组指针和二维数组详情

    2022-03-31 21:11:07
  • SpringBoot创建WebService方法详解

    2022-02-08 10:29:31
  • Java求解两个非负整数最大公约数算法【循环法与递归法】

    2021-10-15 13:53:48
  • Android编程之消息机制实例分析

    2023-07-28 07:24:38
  • java实现发送邮箱验证码

    2022-07-05 07:40:07
  • Java JDK11基于嵌套的访问控制的实现

    2021-07-11 10:02:05
  • Groovy的规则脚本引擎实例解读

    2023-07-11 21:24:04
  • 详细解读java同步之synchronized解析

    2022-08-01 15:27:05
  • 探讨Java中的深浅拷贝问题

    2023-01-07 01:12:44
  • 关于gradle你应该知道的一些小事

    2021-09-15 10:26:35
  • Android之RecyclerView轻松实现下拉刷新和加载更多示例

    2021-09-06 06:14:40
  • Android Studio连接SQLite数据库的登录注册实现

    2023-08-14 11:27:49
  • SpringBoot中shiro过滤器的重写与配置详解

    2021-07-28 23:40:09
  • Java数据结构之顺序表的实现

    2023-06-22 00:47:26
  • maven中下载jar包源码和javadoc的命令介绍

    2023-07-27 04:41:01
  • Android SDK在线更新镜像服务器大全

    2023-03-14 05:44:27
  • C#开启线程的四种示例

    2022-06-01 13:04:37
  • asp之家 软件编程 m.aspxhome.com