Mybatis分表插件(二)

前言

之前寫過一篇MyBatis分表插件的文章,見前文:最後放了幾個可以優化的點。最近有時間,將這個插件再次完善下。主要有兩點

  • 第一、增加了一個可以配置的參數,能夠指定分表數,用來取餘。
  • 第二、對Mapper和對應的SQL添加了緩存。

2020-03-03 補充,這個實現有併發的問題,需要再次優化,對緩存的修改需要協調併發訪問,後面會有文章單獨介紹幾種處理方式。

這裡著重講下第二個添加的緩存部分。在之前的文章提到,可以給Mapper對應的方法和table加緩存,避免每次都去解析SQL。其實我們可以發現除了SQL解析,這裡還有個個循環遍歷,包括遍歷方法,再遍歷方法參數獲取分表鍵的操作,這塊當時就想如何優化下,所以有了今天的文章。所有這些操作的前提是此處的SQL是預編譯處理的,不帶具體的參數值,而用?代替。在仔細思考後,將一個Dao的對應方法引用,即Mapper id作為key,同時將方法對應的SQL,方法拆分鍵,方法SQL解析到的原始表名全部緩存起來。下次分表的SQL請求過來可以直接從緩存獲取。如果有疑惑的可以直接通過下面的代碼帶入項目debug跟蹤下,容易明白。


Mybatis分表插件(二)

代碼

配置

<code>shard:
config:
tables: ["tb_1","tb_b","tb_c"]
strategy: hash
mod : 100
/<code>

緩存類

作為靜態內部類,可以理解為簡單的結構體。

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

/<code>

分表攔截器

<code>
@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> 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> parameterObject = (HashMap<string>) 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;//拆分鍵
}
}

/<string>/<string>/<string>/<code>

補充

實際項目中使用時,部署到環境中一直提示:反射無法獲取方法的參數名稱,具體代碼如下,但是在本地IDE是沒有問題的。

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

如上代碼,取到的shareKey 是 arg0 , 通過查詢得知,這個是jdk1.8以後才支持,而且要在編譯階段顯式地添加參數-parameters , 即javac -parameters ,因此要在其他環境使用需要添加maven編譯配置,如下:

<code><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>
/<code>

而在ide中,配置如下圖:


Mybatis分表插件(二)

image.png

注意:添加在pom後上圖位置是默認有的,如果有問題可以再檢查下

總結

以上就是全部的實現了,項目實踐都是一點點完善優化,逐漸發展的,也可以認識到很多東西都是相通的,總體的套路和方法比較一致。最後補充下jade解析的依賴,很多解析方法沒法直接拿過來使用。

<code>       
<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>
/<code>


分享到:


相關文章: