用Mybatis手写一个分表插件

72次阅读
没有评论

共计 12087 个字符,预计需要花费 31 分钟才能阅读完成。

自动写代码机器人,免费开通

这篇文章主要介绍“用 Mybatis 手写一个分表插件”,在日常操作中,相信很多人在用 Mybatis 手写一个分表插件问题上存在疑惑,丸趣 TV 小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”用 Mybatis 手写一个分表插件”的疑惑有所帮助!接下来,请跟着丸趣 TV 小编一起来学习吧!

背景

事情是酱紫的,阿星的上级 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 无法处理而出现异常。

关键代码如下

用 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、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()); // 此处可以缓存优化  return tableNamePrefix +  _  + (id % 2); } }

传入进来的 value 是 id 值,用 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(); }

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

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

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

根据日期分表,属于第二类,直接在分表策略实现类里面写就行了,不依赖表字段值,value 与 fieldFlag 无需填写,当然你 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 这种扩展机制与设计值得学习思考。

到此,关于“用 Mybatis 手写一个分表插件”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注丸趣 TV 网站,丸趣 TV 小编会继续努力为大家带来更多实用的文章!

向 AI 问一下细节

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-12-04发表,共计12087字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)