Mybatis实现分表插件

 更新时间:2021年5月16日 20:01  点击:1521

背景

事情是酱紫的,阿星的上级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 {
    @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分表拦截器代码如下,有点长哈,主流程看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加上分表注解就好了。

图片

实践跑起来

这里阿星单独写了一套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));
    }


输出结果

图片

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

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

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

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

项目结构:

图片

到此这篇关于Mybatis实现分表插件的文章就介绍到这了,更多相关Mybatis 分表插件内容请搜索猪先飞以前的文章或继续浏览下面的相关文章希望大家以后多多支持猪先飞!

[!--infotagslink--]

相关文章

  • Mybatis Plus select 实现只查询部分字段

    这篇文章主要介绍了Mybatis Plus select 实现只查询部分字段的操作,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教...2021-09-01
  • 解决Mybatis 大数据量的批量insert问题

    这篇文章主要介绍了解决Mybatis 大数据量的批量insert问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-01-09
  • mybatis的Configuration详解

    这篇文章主要介绍了mybatis的Configuration详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-11-04
  • mybatis 返回Integer,Double,String等类型的数据操作

    这篇文章主要介绍了mybatis 返回Integer,Double,String等类型的数据操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-11-25
  • mysql的3种分表方案

    一、先说一下为什么要分表:当一张的数据达到几百万时,你查询一次所花的时间会变多,如果有联合查询的话,有可能会死在那儿了。分表的目的就在于此,减小数据库的负担,缩短查询时间。根据个人经验,mysql执行一个sql的过程如下:1...2014-05-31
  • MyBatis-Plus的物理删除和逻辑删除(使用场景)

    数据库中的数据删除会分为两种:物理删除 和 逻辑删除,接下来通过本文给大家介绍MyBatis-Plus的物理删除和逻辑删除使用场景分析,感兴趣的朋友一起看看吧...2021-09-25
  • Springboot如何使用mybatis实现拦截SQL分页

    这篇文章主要介绍了Springboot使用mybatis实现拦截SQL分页,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下...2020-06-19
  • Mybatis执行update失败的解决

    这篇文章主要介绍了Mybatis执行update失败的解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教...2021-09-01
  • Mybatis plus中使用in查询出错如何解决

    这篇文章主要介绍了Mybatis plus中使用in查询出错的问题及解决方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2020-08-06
  • 解决Mybatis中mapper.xml文件update,delete及insert返回值问题

    这篇文章主要介绍了解决Mybatis中mapper.xml文件update,delete及insert返回值问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-11-23
  • mybatis-plus雪花算法自动生成机器id原理及源码

    Mybatis-Plus是一个Mybatis的增强工具,它在Mybatis的基础上做了增强,却不做改变,Mybatis-Plus是为简化开发、提高开发效率而生,但它也提供了一些很有意思的插件,比如SQL性能监控、乐观锁、执行分析等,下面一起看看mybatis-plus雪花算法自动生成机器id原理解析...2021-06-04
  • Mybatis用注解写in查询的实现

    这篇文章主要介绍了Mybatis用注解写in查询的实现方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教...2021-07-13
  • Mybatis和Mybatis-Plus时间范围查询方式

    这篇文章主要介绍了Mybatis和Mybatis-Plus时间范围查询方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教...2021-08-06
  • Mybatis之Select Count(*)的获取返回int的值操作

    这篇文章主要介绍了Mybatis之Select Count(*)的获取返回int的值操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-11-23
  • Springboot mybatis plus druid多数据源解决方案 dynamic-datasource的使用详解

    这篇文章主要介绍了Springboot mybatis plus druid多数据源解决方案 dynamic-datasource的使用,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2020-11-18
  • mybatis Map查询结果下划线转驼峰的实例

    这篇文章主要介绍了mybatis Map查询结果下划线转驼峰的实例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-09-24
  • Mybatis Plus 字段为空值时执行更新方法未更新解决方案

    这篇文章主要介绍了Mybatis Plus 字段为空值时执行更新方法未更新解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-09-03
  • 关于IDEA 2020使用 mybatis-log-plugin插件的问题

    这篇文章主要介绍了关于IDEA 2020使用 mybatis-log-plugin插件的问题,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2020-11-10
  • Mybatis返回结果封装map过程解析

    这篇文章主要介绍了Mybatis返回结果封装map过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下...2020-09-19
  • Mybatis-plus使用TableNameHandler分表详解(附完整示例源码)

    这篇文章主要介绍了Mybatis-plus使用TableNameHandler分表详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-01-26