前言
之前写过一篇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中,配置如下图:
![](https://img.haomeiwen.com/i7294252/ab25ef14d4cf5772.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>
感谢阅读~
网友评论