基于Mybatis手撸一个分表插件

网友投稿 493 2023-04-03

本站部分文章、图片属于网络上可搜索到的公开信息,均用于学习和交流用途,不能代表睿象云的观点、立场或意见。我们接受网民的监督,如发现任何违法内容或侵犯了您的权益,请第一时间联系小编邮箱jiasou666@gmail.com 处理。

基于Mybatis手撸一个分表插件

背景

事情是酱紫的,阿星的上级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。

分表策略

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

/**  * @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 shardStrategy();   }

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

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

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

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

通用性

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

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

Mybatis分表拦截器流程图如下

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

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

实践跑起来

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

根据id分表 tb_log_id_0tb_log_id_1根据日期分表 tb_log_date_202105tb_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 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

执行下单元测试

日期分表单元测试执行

@Test   void test() {       LogDate logDate = new LogDate();       logDate.setId(snowflake.nextId());       logDate.setComment("测试内容");       logDate.setCreateDate(new Date());       //插入       logDateMapper.save(logDate);       //查询       List logDates = logDateMapper.queryList();       System.out.println(JSONUtil.toJsonPrettyStr(logDates));   }

输出结果

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这种扩展机制与设计值得学习思考。

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

上一篇:如何使用 cron 调度任务
下一篇:2021年你可能错过的DevOps趋势
相关文章

 发表评论

暂时没有评论,来抢沙发吧~