美文网首页Spring-Boot
MyBatis分表插件添加缓存

MyBatis分表插件添加缓存

作者: 西5d | 来源:发表于2019-12-25 14:29 被阅读0次

前言

之前写过一篇MyBatis分表插件的文章,可以回顾下:https://www.jianshu.com/p/ea8059f17643,最后放了几个可以优化的点。最近有时间,将这个插件再次完善下。主要有两点

  • 第一、增加了一个可以配置的参数,能够指定分表数,用来取余。
  • 第二、对Mapper和对应的SQL添加了缓存。

2020-03-03 补充,这个实现有并发的问题,需要再次优化,对缓存的修改需要协调并发访问,后面会有文章单独介绍几种处理方式。

这里着重讲下第二个添加的缓存部分。在之前的文章提到,可以给Mapper对应的方法和table加缓存,避免每次都去解析SQL。其实我们可以发现除了SQL解析,这里还有个个循环遍历,包括遍历方法,再遍历方法参数获取分表键的操作,这块当时就想如何优化下,所以有了今天的文章。所有这些操作的前提是此处的SQL是预编译处理的,不带具体的参数值,而用?代替。在仔细思考后,将一个Dao的对应方法引用,即Mapper id作为key,同时将方法对应的SQL,方法拆分键,方法SQL解析到的原始表名全部缓存起来。下次分表的SQL请求过来可以直接从缓存获取。如果有疑惑的可以直接通过下面的代码带入项目debug跟踪下,容易明白。

代码

配置

shard:
  config:
   tables: ["tb_1","tb_b","tb_c"]
   strategy: hash
   mod : 100

缓存类

作为静态内部类,可以理解为简单的结构体。

@Data
    private static final class ShardEntity {
        private String statement;
        private String originTableName;
        private String shardKey;
    }

分表拦截器


@Intercepts(@Signature(
        type = StatementHandler.class,
        method = "prepare",
        args = {Connection.class, Integer.class}
))
@Component
public class ShardInterceptor implements Interceptor {
    private static final ReflectorFactory defaultReflectorFactory = new DefaultReflectorFactory();
    private static final ConcurrentHashMap<String, ShardEntity> MAPPER_SHARD_CACHE = new ConcurrentHashMap<>();

    @Resource
    private Properties shardConfigProperties;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaObject = MetaObject.forObject(statementHandler,
                SystemMetaObject.DEFAULT_OBJECT_FACTORY,
                SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,
                defaultReflectorFactory
        );

        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        String id = mappedStatement.getId();

        BoundSql boundSql = statementHandler.getBoundSql();
        HashMap<String, Object> parameterObject = (HashMap<String, Object>) boundSql.getParameterObject();

        ShardEntity shardEntity = MAPPER_SHARD_CACHE.get(id);
        String sql;
        if (null != shardEntity) {
            if (null == shardEntity.getShardKey()) {
                //非拆分表缓存命中
                return invocation.proceed();
            }
            Long value = (Long) parameterObject.get(shardEntity.getShardKey());
            String originTable = shardEntity.getOriginTableName();
            String forwardTable = shard(originTable, value);
            Field field = boundSql.getClass().getDeclaredField("sql");
            field.setAccessible(true);
            //field.set(boundSql, shardEntity.getStatement().replace(originTable, forwardTable));
            field.set(boundSql, shardEntity.getStatement().replace(originTable, forwardTable)); 更正:不能使用保存的SQL,不要支持 in (?, ?) 或者 in (?,?,?,?)可变参数的foreach类型。
//同时, 直接用in (List)也可以查到结果,但是内容是不对的。
            return invocation.proceed();
        } else {
            //sql 是预编译的 eg: select * from tb where user_id = ? order by time 的格式
            sql = boundSql.getSql();
            shardEntity = new ShardEntity();
            shardEntity.setStatement(sql);
            MAPPER_SHARD_CACHE.put(id, shardEntity);
        }

        String dao = id.substring(0, id.lastIndexOf("."));
        String methodName = id.substring(id.lastIndexOf(".") + 1);
        Class clazz = Class.forName(dao);

        for (Method method : clazz.getMethods()) {
            if (method.getName().equals(methodName)) {
                Annotation[][] parameterAnnotations = method.getParameterAnnotations();
                int idx = 0;
                for (Annotation[] pa : parameterAnnotations) {
                    for (Annotation a : pa) {
                        if (a instanceof ShardBy) {
                            String shardKey = method.getParameters()[idx].getName();
                            Long value = (Long) parameterObject.get(shardKey);
                            String originTable = getTableName(sql);
                            String forwardTable = shard(originTable, value);
                            Field field = boundSql.getClass().getDeclaredField("sql");
                            field.setAccessible(true);
                            field.set(boundSql, sql.replace(originTable, forwardTable));
                            shardEntity.setOriginTableName(originTable);
                            shardEntity.setShardKey(shardKey);
                            return invocation.proceed();
                        }
                    }
                    idx++;
                }
            }
        }

        return invocation.proceed();
    }

    private String shard(String tableName, Long value) {
        return tableName + "_" + value % Integer.parseInt(shardConfigProperties.getProperty("mod"));
    }

    private String getTableName(String sql) throws Throwable {
        SQLParseInfo parseInfo = SQLParseInfo.getParseInfo(sql);
        if (parseInfo.getTables() == null || parseInfo.getTables().length != 1) {
            throw new Throwable("表数目不为1");
        }
        return parseInfo.getTables()[0].getName();
    }

    @Data
    private static final class ShardEntity {
        private String statement;//SQL
        private String originTableName; //原始表名
        private String shardKey;//拆分键
    }
}

补充

实际项目中使用时,部署到环境中一直提示:反射无法获取方法的参数名称,具体代码如下,但是在本地IDE是没有问题的。

  String shardKey = method.getParameters()[idx].getName();
                            Long value = (Long) parameterObject.get(shardKey);

如上代码,取到的shareKey 是 arg0 , 通过查询得知,这个是jdk1.8以后才支持,而且要在编译阶段显式地添加参数-parameters , 即javac -parameters ,因此要在其他环境使用需要添加maven编译配置,如下:

<plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                    <compilerArgs>
                        <compilerArg>-parameters</compilerArg>
                    </compilerArgs>
                </configuration>
            </plugin>

而在ide中,配置如下图:


image.png

注意:添加在pom后上图位置是默认有的,如果有问题可以再检查下

总结

以上就是全部的实现了,项目实践都是一点点完善优化,逐渐发展的,也可以认识到很多东西都是相通的,总体的套路和方法比较一致。最后补充下jade解析的依赖,很多解析方法没法直接拿过来使用。

       <!-- jade -->
       <dependency>
           <groupId>com.xiaomi</groupId>
           <artifactId>bmw-jade-route</artifactId>
           <version>1.0.11</version>
           <exclusions>
               <exclusion>
                   <artifactId>spring</artifactId>
                   <groupId>org.springframework</groupId>
               </exclusion>
           </exclusions>
       </dependency>

感谢阅读~

相关文章

网友评论

    本文标题:MyBatis分表插件添加缓存

    本文链接:https://www.haomeiwen.com/subject/gercoctx.html