背景

事情是酱紫的,上级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、valuetableNamePrefix为分表前缀,value作为生成分表名的逻辑参数。

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

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

/*** @Author 程序猿阿星* @Description 分表策略id* @Date 2021/5/9*/
@Component
public class TableShardStrategyId implements ITableShardStrategy {@Overridepublic 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分表拦截器代码如下,有点长哈,主流程看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();@Overridepublic 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));}//替换sqlreplaceSql(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);//替换sqlreplaceSql(tableShard, valueObject, metaObject, boundSql);} else {//非基础类型对象Class<?> parameterObjectClass = parameterObject.getClass();Field declaredField = parameterObjectClass.getDeclaredField(value);declaredField.setAccessible(true);Object valueObject = declaredField.get(parameterObject);//替换sqlreplaceSql(tableShard, valueObject, metaObject, boundSql);}}} else {//无需处理parameterField//替换sqlreplaceSql(tableShard, value, metaObject, boundSql);}//执行下一个插件逻辑return invocation.proceed();}@Overridepublic 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();//获取策略classClass<? extends ITableShardStrategy> strategyClazz = tableShard.shardStrategy();//从spring ioc容器获取策略类ITableShardStrategy tableShardStrategy = SpringUtil.getBean(strategyClazz);//生成分表名String shardTableName = tableShardStrategy.generateTableName(tableNamePrefix, value);// 获取sqlString 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();//获取Classfinal 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加上分表注解就好了。

实践跑起来

这里阿星单独写了一套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";@Overridepublic 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 {@Overridepublic 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 mapperPUBLIC "-//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">selectid as id,comment as comment,create_date as createDatefromtb_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 mapperPUBLIC "-//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">selectid as id,comment as comment,create_date as createDatefromtb_log_idwhereid = #{id}</select>//对应save函数<insert id="save" >insert into tb_log_id(id, comment,create_date)values (#{id}, #{comment},#{createDate})</insert></mapper>

执行下单元测试

日期分表单元测试执行

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

输出结果


id分表单元测试执行

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

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

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

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

项目结构:

往期推荐

MyBatis 中为什么不建议使用 where 1=1?

SpringBoot 使用注解实现消息广播功能

聊聊接口性能优化的11个小技巧

基于 MyBatis 手撸一个分表插件相关推荐

  1. 分表需要解决的问题 基于MyBatis 的轻量分表落地方案

    分表:垂直拆分.水平拆分 垂直拆分:根据业务将一个表拆分为多个表. 如:将经常和不常访问的字段拆分至不同的表中.由于与业务关系密切,目前的分库分表产品均使用水平拆分方式. 水平拆分:根据分片算法将一个 ...

  2. php 六边形 属性图 能力数值图,详解基于 Canvas 手撸一个六边形能力图

    一.前言 六边形能力图如下,由 6 个 六边形组成,每一个顶点代表其在某一方面的能力.这篇文章我们就来看看如何基于 canvas 去绘制这么一个六边形能力图.当然,你也可以基于其他开源的 js 方案来 ...

  3. 基于vue手写一个分屏器,通过鼠标控制屏幕宽度。

    基于vue手写一个分屏器,通过鼠标控制屏幕宽度. 先来看看实现效果: QQ录屏20220403095856 下面是实现代码: <template><section class=&qu ...

  4. mybatis拦截器开发-分表插件

    相关源码已上传至我的github,对应的插件代码在src/main/java/net/dwade/plugins/mybatis目录 https://github.com/huangxfchn/dwa ...

  5. Netty游戏服务器实战开发(11):Spring+mybatis 手写分库分表策略(续)

    在大型网络游戏中,传统的游戏服务器无法满足性能上的需求.所以有了分布式和微服务新起,在传统web服务器中,我们保存用户等信息基本都是利用一张单表搞定,但是在游戏服务器中,由于要求比较高,我们不能存在大 ...

  6. html5如何编写六角形,详解基于 Canvas 手撸一个六边形能力图

    一.前言 六边形能力图如下,由 6 个 六边形组成,每一个顶点代表其在某一方面的能力.这篇文章我们就来看看如何基于 canvas 去绘制这么一个六边形能力图.当然,你也可以基于其他开源的 js 方案来 ...

  7. java分表插件_fastmybatis编写分表插件

    fastmybatis支持原生的插件,将写好的插件配置到mybatis配置文件中即可 这里演示编写一个分表插件 假设有4张分表,user_log0~3,记录用户的日志情况 user_log0 user ...

  8. .Net Core手撸一个基于Token的权限认证

    说明 权限认证是确定用户身份的过程.可确定用户是否有访问资源的权力 今天给大家分享一下类似JWT这种基于token的鉴权机制 基于token的鉴权机制,它不需要在服务端去保留用户的认证信息或者会话信息 ...

  9. 手撸一个动态数据源的Starter 完整编写一个Starter及融合项目的过程 保姆级教程

    手撸一个动态数据源的Starter! 文章目录 手撸一个动态数据源的Starter! 前言 一.准备工作 1,演示 2,项目目录结构 3,POM文件 二.思路 三.编写代码 1,定义核心注解 Ds 2 ...

最新文章

  1. JavaScript面向对象编程
  2. Xcode中通过删除原先版本的程序来复位App
  3. 用WSDL定义Web服务
  4. 使用PyCharm创建Django项目及基本配置
  5. zabbix监控ntpd服务
  6. 更改微软更新服务器地址,更新服务 | Microsoft Docs
  7. java学习(122):treeset自定义排序
  8. typecho一个简洁轻量适合写作,技术类的主题-AirCloud
  9. securecrt使用_SecureCRT会话丢失原因及解决方法
  10. 深入理解JVM--类的执行机制
  11. 什么是validationQuery
  12. 提升开发者安全的七大可行实践
  13. SQL与Excel数据交互
  14. 30. Substring with Concatenation of All Words
  15. 具体数学-第13课(组合数各种性质)
  16. Scikit-Learn (浅谈Kmeans聚类算法)
  17. Swift 模式匹配
  18. JavaScript实现拖动滑块拼图验证(html5、canvas)
  19. 扩展欧几里得算法的实现
  20. acer软件保护卡怎么解除_Acer软件保护卡使用说明资料

热门文章

  1. 四因素三水平正交表_做论文要用正交表?我打包送给你
  2. 天涯python_python 網絡爬蟲(一)爬取天涯論壇評論
  3. HashiCorp Vault 1.0开源自动解封特性,新增Batch令牌
  4. 学习笔记-JMeter 进行接口压力测试
  5. linux扫描工具之nmap
  6. sencha touch调试时Please close other application using ADB: Monitor, DDMS, Eclipse
  7. linux学习笔记-chkconfig
  8. libsvm数据缩放方法
  9. mysql查询很慢优化方法1
  10. SpringMvc+Tomcat+Angular4 部署运行