这篇文章主要介绍“用Mybatis手写一个分表插件”,在日常操作中,相信很多人在用Mybatis手写一个分表插件问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”用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分表策略接口*@Date2021/5/9*/publicinterfaceITableShardStrategy{/***@author:程序猿阿星*@description:生成分表名*@paramtableNamePrefix表前缀名*@paramvalue值*@date:2021/5/9*@return:java.lang.String*/StringgenerateTableName(StringtableNamePrefix,Objectvalue);/***验证tableNamePrefix*/defaultvoidverificationTableNamePrefix(StringtableNamePrefix){if(StrUtil.isBlank(tableNamePrefix)){thrownewRuntimeException("tableNamePrefixisnull");}}}

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

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

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

/***@Author程序猿阿星*@Description分表策略id*@Date2021/5/9*/@ComponentpublicclassTableShardStrategyIdimplementsITableShardStrategy{@OverridepublicStringgenerateTableName(StringtableNamePrefix,Objectvalue){verificationTableNamePrefix(tableNamePrefix);if(value==null||StrUtil.isBlank(value.toString())){thrownewRuntimeException("valueisnull");}longid=Long.parseLong(value.toString());//此处可以缓存优化returntableNamePrefix+"_"+(id%2);}}

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

控制影响范围

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

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

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

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

/***@Author程序猿阿星*@Description分表注解*@Date2021/5/9*/@Target(value={ElementType.TYPE,ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)public@interfaceTableShard{//表前缀名StringtableNamePrefix();//值Stringvalue()default"";//是否是字段名,如果是需要解析请求参数改字段名的值(默认否)booleanfieldFlag()defaultfalse;//对应的分表策略类Class<?extendsITableShardStrategy>shardStrategy();}

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

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

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

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

通用性

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

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

Mybatis分表拦截器流程图如下

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

/***@Author程序员阿星*@Description分表拦截器*@Date2021/5/9*/@Intercepts({@Signature(type=StatementHandler.class,method="prepare",args={Connection.class,Integer.class})})publicclassTableShardInterceptorimplementsInterceptor{privatestaticfinalReflectorFactorydefaultReflectorFactory=newDefaultReflectorFactory();@OverridepublicObjectintercept(Invocationinvocation)throwsThrowable{//MetaObject是mybatis里面提供的一个工具类,类似反射的效果MetaObjectmetaObject=getMetaObject(invocation);BoundSqlboundSql=(BoundSql)metaObject.getValue("delegate.boundSql");MappedStatementmappedStatement=(MappedStatement)metaObject.getValue("delegate.mappedStatement");//获取Mapper执行方法Methodmethod=invocation.getMethod();//获取分表注解TableShardtableShard=getTableShard(method,mappedStatement);//如果method与class都没有TableShard注解或执行方法不存在,执行下一个插件逻辑if(tableShard==null){returninvocation.proceed();}//获取值Stringvalue=tableShard.value();//value是否字段名,如果是,需要解析请求参数字段名的值booleanfieldFlag=tableShard.fieldFlag();if(fieldFlag){//获取请求参数ObjectparameterObject=boundSql.getParameterObject();if(parameterObjectinstanceofMapperMethod.ParamMap){//ParamMap类型逻辑处理MapperMethod.ParamMapparameterMap=(MapperMethod.ParamMap)parameterObject;//根据字段名获取参数值ObjectvalueObject=parameterMap.get(value);if(valueObject==null){thrownewRuntimeException(String.format("入参字段%s无匹配",value));}//替换sqlreplaceSql(tableShard,valueObject,metaObject,boundSql);}else{//单参数逻辑//如果是基础类型抛出异常if(isBaseType(parameterObject)){thrownewRuntimeException("单参数非法,请使用@Param注解");}if(parameterObjectinstanceofMap){Map<String,Object>parameterMap=(Map<String,Object>)parameterObject;ObjectvalueObject=parameterMap.get(value);//替换sqlreplaceSql(tableShard,valueObject,metaObject,boundSql);}else{//非基础类型对象Class<?>parameterObjectClass=parameterObject.getClass();FielddeclaredField=parameterObjectClass.getDeclaredField(value);declaredField.setAccessible(true);ObjectvalueObject=declaredField.get(parameterObject);//替换sqlreplaceSql(tableShard,valueObject,metaObject,boundSql);}}}else{//无需处理parameterField//替换sqlreplaceSql(tableShard,value,metaObject,boundSql);}//执行下一个插件逻辑returninvocation.proceed();}@OverridepublicObjectplugin(Objecttarget){//当目标类是StatementHandler类型时,才包装目标类,否者直接返回目标本身,减少目标被代理的次数if(targetinstanceofStatementHandler){returnPlugin.wrap(target,this);}else{returntarget;}}/***@paramobject*@methodName:isBaseType*@author:程序员阿星*@description:基本数据类型验证,true是,false否*@date:2021/5/9*@return:boolean*/privatebooleanisBaseType(Objectobject){if(object.getClass().isPrimitive()||objectinstanceofString||objectinstanceofInteger||objectinstanceofDouble||objectinstanceofFloat||objectinstanceofLong||objectinstanceofBoolean||objectinstanceofByte||objectinstanceofShort){returntrue;}else{returnfalse;}}/***@paramtableShard分表注解*@paramvalue值*@parammetaObjectmybatis反射对象*@paramboundSqlsql信息对象*@author:程序猿阿星*@description:替换sql*@date:2021/5/9*@return:void*/privatevoidreplaceSql(TableShardtableShard,Objectvalue,MetaObjectmetaObject,BoundSqlboundSql){StringtableNamePrefix=tableShard.tableNamePrefix();//获取策略classClass<?extendsITableShardStrategy>strategyClazz=tableShard.shardStrategy();//从springioc容器获取策略类ITableShardStrategytableShardStrategy=SpringUtil.getBean(strategyClazz);//生成分表名StringshardTableName=tableShardStrategy.generateTableName(tableNamePrefix,value);//获取sqlStringsql=boundSql.getSql();//完成表名替换metaObject.setValue("delegate.boundSql.sql",sql.replaceAll(tableNamePrefix,shardTableName));}/***@paraminvocation*@author:程序猿阿星*@description:获取MetaObject对象-mybatis里面提供的一个工具类,类似反射的效果*@date:2021/5/9*@return:org.apache.ibatis.reflection.MetaObject*/privateMetaObjectgetMetaObject(Invocationinvocation){StatementHandlerstatementHandler=(StatementHandler)invocation.getTarget();//MetaObject是mybatis里面提供的一个工具类,类似反射的效果MetaObjectmetaObject=MetaObject.forObject(statementHandler,SystemMetaObject.DEFAULT_OBJECT_FACTORY,SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,defaultReflectorFactory);returnmetaObject;}/***@author:程序猿阿星*@description:获取分表注解*@parammethod*@parammappedStatement*@date:2021/5/9*@return:com.xing.shard.interceptor.TableShard*/privateTableShardgetTableShard(Methodmethod,MappedStatementmappedStatement)throwsClassNotFoundException{Stringid=mappedStatement.getId();//获取ClassfinalStringclassName=id.substring(0,id.lastIndexOf("."));//分表注解TableShardtableShard=null;//获取Mapper执行方法的TableShard注解tableShard=method.getAnnotation(TableShard.class);//如果方法没有设置注解,从Mapper接口上面获取TableShard注解if(tableShard==null){//获取TableShard注解tableShard=Class.forName(className).getAnnotation(TableShard.class);}returntableShard;}}

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

实践跑起来

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

根据id分表

tb_log_id_0

tb_log_id_1

根据日期分表

tb_log_date_202105

tb_log_date_202106

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

TableShardStrategy定义

/***@Authorwx*@Description分表策略日期*@Date2021/5/9*/@ComponentpublicclassTableShardStrategyDateimplementsITableShardStrategy{privatestaticfinalStringDATE_PATTERN="yyyyMM";@OverridepublicStringgenerateTableName(StringtableNamePrefix,Objectvalue){verificationTableNamePrefix(tableNamePrefix);if(value==null||StrUtil.isBlank(value.toString())){returntableNamePrefix+"_"+DateUtil.format(newDate(),DATE_PATTERN);}else{returntableNamePrefix+"_"+DateUtil.format(newDate(),value.toString());}}}***@Author程序猿阿星*@Description分表策略id*@Date2021/5/9*/@ComponentpublicclassTableShardStrategyIdimplementsITableShardStrategy{@OverridepublicStringgenerateTableName(StringtableNamePrefix,Objectvalue){verificationTableNamePrefix(tableNamePrefix);if(value==null||StrUtil.isBlank(value.toString())){thrownewRuntimeException("valueisnull");}longid=Long.parseLong(value.toString());//可以加入本地缓存优化returntableNamePrefix+"_"+(id%2);}}Mapper定义

Mapper接口

/***@Author程序猿阿星*@Description*@Date2021/5/8*/@TableShard(tableNamePrefix="tb_log_date",shardStrategy=TableShardStrategyDate.class)publicinterfaceLogDateMapper{/***查询列表-根据日期分表*/List<LogDate>queryList();/***单插入-根据日期分表*/voidsave(LogDatelogDate);}-------------------------------------------------------------------------------------------------/***@Author程序猿阿星*@Description*@Date2021/5/8*/@TableShard(tableNamePrefix="tb_log_id",value="id",fieldFlag=true,shardStrategy=TableShardStrategyId.class)publicinterfaceLogIdMapper{/***根据id查询-根据id分片*/LogIdqueryOne(@Param("id")longid);/***单插入-根据id分片*/voidsave(LogIdlogId);}

Mapper.xml

<?xmlversion="1.0"encoding="UTF-8"?><!DOCTYPEmapperPUBLIC"-//mybatis.org//DTDMapper3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mappernamespace="com.xing.shard.mapper.LogDateMapper">//对应LogDateMapper#queryList函数<selectid="queryList"resultType="com.xing.shard.entity.LogDate">selectidasid,commentascomment,create_dateascreateDatefromtb_log_date</select>//对应LogDateMapper#save函数<insertid="save">insertintotb_log_date(id,comment,create_date)values(#{id},#{comment},#{createDate})</insert></mapper>-------------------------------------------------------------------------------------------------<?xmlversion="1.0"encoding="UTF-8"?><!DOCTYPEmapperPUBLIC"-//mybatis.org//DTDMapper3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mappernamespace="com.xing.shard.mapper.LogIdMapper">//对应LogIdMapper#queryOne函数<selectid="queryOne"resultType="com.xing.shard.entity.LogId">selectidasid,commentascomment,create_dateascreateDatefromtb_log_idwhereid=#{id}</select>//对应save函数<insertid="save">insertintotb_log_id(id,comment,create_date)values(#{id},#{comment},#{createDate})</insert></mapper>执行下单元测试

日期分表单元测试执行

@Testvoidtest(){LogDatelogDate=newLogDate();logDate.setId(snowflake.nextId());logDate.setComment("测试内容");logDate.setCreateDate(newDate());//插入logDateMapper.save(logDate);//查询List<LogDate>logDates=logDateMapper.queryList();System.out.println(JSONUtil.toJsonPrettyStr(logDates));}

输出结果

id分表单元测试执行

@Testvoidtest(){LogIdlogId=newLogId();longid=snowflake.nextId();logId.setId(id);logId.setComment("测试");logId.setCreateDate(newDate());//插入logIdMapper.save(logId);//查询LogIdlogIdObject=logIdMapper.queryOne(id);System.out.println(JSONUtil.toJsonPrettyStr(logIdObject));}

输出结果

小结一下

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

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