Shardbatis開源框架原始碼的修改實踐經驗分享
Shardbatis開源框架原始碼按自身業務的改進
摘要
在研發過程中,我們遇到了單表資料量瓶頸問題,同時又不能增加資料庫的費用,最後選擇了分表技術來解決效能問題。在分表技術的呼叫過程中,我們有2種技術實現方案。第一種基於mybatis的plugin 外掛自研發,一種是採用開源的shardbatis框架。在對比研究分析過程中,發現shardbatis的框架設計理念擴充套件性良好,對於團隊開發有很好規範作用,同時採用配置設計理念,修改配置檔案就能滿足業務應用場景的需求,最後選擇了shardbatis框架,在實際應用過程中發現shardbatis採用sqlparse框架解析SQL,對編寫的MySQL的查詢語句進行了修正,查詢SQL分頁需要特殊處理,同時parselist不能到類級別攔截,最後我們下載原始碼進行了改進,替換了sqlparse解析框架,覆蓋了isParse方法,達到滿足自己的一套業務需求的框架。
Shardbatis的設計原理
1. 容器啟動載入mybatis-config.xml配置檔案
2. 解析mybatis配置檔案的同時載入mybatis攔截器介面的實現類,通過攔截器介面的setProperties 方法載入shard_config.xml引數,註冊分表策略
3. Intercept方法攔截,輪詢所有的mapperid與分表策略中配置的mapperid比較,是否相等,相等表示需要進行分表,如果需要分表,就走分表程式碼執行邏輯,不分表直接呼叫invocation.proceed()
4. 分表邏輯,讀取StatementHandler 的sql 進行表替換,表的替換按照分表對應的具體策略進行替換,替換完成,重新繫結SQL,呼叫invocation.proceed() 返回
5. 業務分表策略必須實現此介面,才能被攔截器解析.
mybatis-config.xml
<?xml version="1.0"encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="lazyLoadingEnabled"value="false" />
<!-- 全域性延遲載入 ,在啟用延遲載入的同時,需要禁用"aggressiveLazyLoading"項 -->
<setting name="aggressiveLazyLoading"value="false" />
<!-- 這個設定項在使用者手冊當中的定義當啟用時,有延遲載入屬性的物件在被呼叫時將會完全載入任意屬性。否則,每種屬性將會按需要載入。 -->
<setting name="lazyLoadTriggerMethods"value="clone" />
<!-- <setting name="defaultExecutorType"value="BATCH" /> -->
</settings>
<plugins>
<plugin interceptor="com.google.code.shardbatis.plugin.ShardPlugin">
<propertyname="shardingConfig" value="shard_config.xml"/>
</plugin>
</plugins>
<plugins>
</plugins>
shard_config.xml
<?xml version="1.0"encoding="UTF-8"?>
<!DOCTYPE shardingConfigPUBLIC "-//shardbatis.googlecode.com//DTDShardbatis 2.0//EN"
"http://shardbatis.googlecode.com/dtd/shardbatis-config.dtd">
<shardingConfig>
<!--parseList可選配置如果配置了parseList,只有在parseList範圍內的sql才會被解析和修改,配置到dao層 -->
<parseList>
<value>com.xxx.member.dao.xxxEdt.insert</value>
</parseList>
<!-- 配置分表策略 -->
<strategy tableName="t_crm_member"strategyClass="com.xxx.member.dao.UserIdStrategy" />
</shardingConfig>
程式碼分析如下:
com.google.code.shardbatis.plugin.ShardPlugin核心類
com.google.code.shardbatis.strategy.ShardStrategy 核心介面
@Intercepts( { @Signature(type = StatementHandler.class, method = "prepare",args = { Connection.class })})
publicclass ShardPlugin implements Interceptor {
public Object intercept(Invocation invocation) throws Throwable{
StatementHandler statementHandler =(StatementHandler) invocation.getTarget();
MappedStatement mappedStatement = null;
if (statementHandlerinstanceofRoutingStatementHandler) {
StatementHandler delegate = (StatementHandler)ReflectionUtils
.getFieldValue(statementHandler, "delegate");
mappedStatement = (MappedStatement)ReflectionUtils.getFieldValue(delegate, "mappedStatement");
} else {
mappedStatement = (MappedStatement)ReflectionUtils.getFieldValue(statementHandler, "mappedStatement");
}
String mapperId = mappedStatement.getId();
if (isShouldParse(mapperId)) {
String sql = statementHandler.getBoundSql().getSql();
if (log.isDebugEnabled()) {
log.debug("Original Sql ["+ mapperId+ "]:"+ sql);
}
Object params = statementHandler.getBoundSql().getParameterObject();
SqlConverterFactory cf = SqlConverterFactory.getInstance();
sql= cf.convert(sql,params, mapperId);
if (log.isDebugEnabled()) {
log.debug("Converted Sql ["+ mapperId+ "]:"+ sql);
}
ReflectionUtils.setFieldValue(statementHandler
.getBoundSql(), "sql", sql);
}
returninvocation.proceed();
}
publicObject plugin(Object target){
}
public void setProperties(Properties properties){
}
}
publicinterface ShardStrategy{
/**
* 得到實際表名
* @param baseTableName 邏輯表名,一般是沒有字首或者是字尾的表名
* @param params mybatis執行某個statement時使用的引數
* @param mapperId mybatis配置的statement id
* @return
*/
String getTargetTableName(String baseTableName,Object params,String mapperId);
}
Shardbatis優化改進
1. SqlConverterFactory.convert 方法
原始碼:
SqlConverterFactory cf =SqlConverterFactory.getInstance();
sql = cf.convert(sql,params, mapperId);
重寫convert方法:
sql = convert(sql, params, mapperId);
protected String convert(String sql,Object params, String mapperId) {
ShardConfigHolder configFactory =ShardConfigHolder.getInstance();
Map<String, ShardStrategy> strategyRegister=configFactory.getStrategyRegister();
Iterator<String> iterators=strategyRegister.keySet().iterator();
while(iterators.hasNext()){
String tableName = iterators.next().toUpperCase();
//獲取分表策略並轉換sql
ShardStrategy strategy =configFactory.getStrategy(tableName);
String shardTable =strategy.getTargetTableName(tableName, params, mapperId);
sql =sql.replaceAll(jointRegex(tableName), jointShard(shardTable));
}
returnsql ;
}
/**
* 生成匹配正則
*
* @param tabelName 表名
* @return
*/
private String jointRegex(String tabelName) {
returnnew StringBuffer(REGEX_PREFIX).append(tabelName).append(REGEX_SUFIX).toString();
}
/**
* 拼接分表名
*
* @param shardTable 分表名
* @return
*/
private String jointShard(String shardTable) {
returnnew StringBuffer(SPACE).append(shardTable).append(SPACE).toString();
}
2. ShardConfigHolder. isParseId方法
shardbatis框架配置必須要到方法,不能到類級別,而我們業務中都是類級別的,對原始碼進行了修改,滿足自身的要求
原始碼:
<?xml version="1.0"encoding="UTF-8"?>
<!DOCTYPE shardingConfigPUBLIC "-//shardbatis.googlecode.com//DTDShardbatis 2.0//EN"
"http://shardbatis.googlecode.com/dtd/shardbatis-config.dtd">
<shardingConfig>
<!--parseList可選配置如果配置了parseList,只有在parseList範圍內的sql才會被解析和修改,配置到dao層 -->
<parseList>
<value>com.xxx.member.dao.xxxEdt.insert</value>
</parseList>
<!-- 配置分表策略 -->
<strategy tableName="t_crm_member"strategyClass="com.xxx.member.dao.UserIdStrategy" />
</shardingConfig>
修改:
<?xml version="1.0"encoding="UTF-8"?>
<!DOCTYPE shardingConfigPUBLIC "-//shardbatis.googlecode.com//DTDShardbatis 2.0//EN"
"http://shardbatis.googlecode.com/dtd/shardbatis-config.dtd">
<shardingConfig>
<!--parseList可選配置如果配置了parseList,只有在parseList範圍內的sql才會被解析和修改,配置到dao層 -->
<parseList>
<value> com.xxx.member.dao.xxxEdt </value>
</parseList>
<!-- 配置分表策略 -->
<strategy tableName="t_crm_member"strategyClass="com.xxx.member.dao.UserIdStrategy" />
</shardingConfig>
publicclass ShardConfigHolder {
publicboolean isParseId(String id) {
id=id.substring(0,id.lastIndexOf("."));
returnparseSet != null && parseSet.contains(id);
}
}
最後,由於筆者水平有限,文章中如有不足之處,敬請讀者斧正,文明交流,溝通分享。