• 基于mybatis的一个分表插件


    本文转载自微信公众号:Java极客技术

    原文链接:https://mp.weixin.qq.com/s/QKY1gAyQ9F5aL0D3XVwbZQ

    大家好,我是摸鱼失败的阿星:

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

    关键代码如下

     

     唉,本来还想摸鱼的,看来摸鱼的时间是没了,还多了一项任务。

    分析

    竟然交给阿星来做了,就撸起袖子开干吧,先看看分表功能的需求

    • 支持自定义分表策略
    • 能控制影响范围
    • 通用性

    分表会提前建立好,所以不需要考虑表不存在的问题,核心逻辑实现,通过分表策略得到分表名,再把分表名动态替换到sql

    分表策略

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

    public interface ITableShardStrategy {
        /**
         * @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取模策略代码,取模两张表

    @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类的任意一个函数上,并且保证小粒度覆盖粗粒度。

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

    @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函数就好了。

    @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
         * @description: 基本数据类型验证,true是,false否
         * @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信息对象
         * @description: 替换sql
         * @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
         * @description: 获取MetaObject对象-mybatis里面提供的一个工具类,类似反射的效果
         * @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;
        }
    
        /**
         * @description: 获取分表注解
         * @param method
         * @param mappedStatement
         * @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加上分表注解就好了。

    插件的实践 

    TableShardStrategy定义

    // 根据日期分表
    @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());
            }
        }
    }
    
    // 根据id取模分表
    @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接口

    @TableShard(tableNamePrefix = "log_date",shardStrategy = TableShardStrategyDate.class)
    public interface LogDateMapper {
    
        /**
         * 查询列表-根据日期分表
         */
        List<LogDate> queryList();
    
        /**
         * 单插入-根据日期分表
         */
        void  save(LogDate logDate);
    
    }
    @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>

    单元测试1

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

     单元测试2

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

  • 相关阅读:
    顺便说说webservice
    了解c3p0,dbcp与druid
    静心己过
    慢慢来写SpringMVC基本项目
    关于druid的配置说明
    想法
    看见了别人的数据库题,随便写写
    Java 工具类
    Java 工具类
    使用JavaMail实现发送模板邮件以及保存到发件箱
  • 原文地址:https://www.cnblogs.com/wangyongwen/p/14798182.html
Copyright © 2020-2023  润新知