BUG
基於前一篇文章關於Sping多數據源實現,已經被我運用到實際項目中。但最近開始出現一些問題,服務剛啟動,能看到數據源切換混亂的場景。由於項目中設計,服務啟動會去從庫查一些配置項數據,需要切換數據源,但經常數據查詢失敗,發現跑到主庫去了,但隨后又正常。
本着總想搞點大新聞的心態,開始了Debug之旅。
每次的坑,通常是我無意間挖的,這次也不例外。debug發現,一次操作,數據源被獲取了兩次。其中第一次是被分頁插件PageHelper消耗了。看了下源碼,是由於我干掉了一個配置。新項目這邊有人說需要mysql、oracle多庫同存的業務需求,我把PageHelper的方言配置,原本寫死的 【dialect=mysql】給干掉了。
/**
* 設置屬性值
*
* @param p 屬性值
*/
public void setProperties(Properties p) {
//MyBatis3.2.0版本校驗
try {
Class.forName("org.apache.ibatis.scripting.xmltags.SqlNode");//SqlNode是3.2.0之后新增的類
} catch (ClassNotFoundException e) {
throw new RuntimeException("您使用的MyBatis版本太低,MyBatis分頁插件PageHelper支持MyBatis3.2.0及以上版本!");
}
//數據庫方言
String dialect = p.getProperty("dialect");
if (dialect == null || dialect.length() == 0) {
autoDialect = true;
this.properties = p;
} else {
autoDialect = false;
sqlUtil = new SqlUtil(dialect);
sqlUtil.setProperties(p);
}
}
加載時,判斷沒有設置方言,則 autoDialect = true
/**
* Mybatis攔截器方法
*
* @param invocation 攔截器入參
* @return 返回執行結果
* @throws Throwable 拋出異常
*/
public Object intercept(Invocation invocation) throws Throwable {
if (autoDialect) {
initSqlUtil(invocation);
}
return sqlUtil.processPage(invocation);
}
/**
* 初始化sqlUtil
*
* @param invocation
*/
public synchronized void initSqlUtil(Invocation invocation) {
if (sqlUtil == null) {
String url = null;
try {
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
MetaObject msObject = SystemMetaObject.forObject(ms);
DataSource dataSource = (DataSource) msObject.getValue("configuration.environment.dataSource");
url = dataSource.getConnection().getMetaData().getURL();
} catch (SQLException e) {
throw new RuntimeException("分頁插件初始化異常:" + e.getMessage());
}
if (url == null || url.length() == 0) {
throw new RuntimeException("無法自動獲取jdbcUrl,請在分頁插件中配置dialect參數!");
}
String dialect = Dialect.fromJdbcUrl(url);
if (dialect == null) {
throw new RuntimeException("無法自動獲取數據庫類型,請通過dialect參數指定!");
}
sqlUtil = new SqlUtil(dialect);
sqlUtil.setProperties(properties);
properties = null;
autoDialect = false;
}
}
沒有意外,然后就需要獲取數據庫連接,根據url判斷方言。
回到前面的問題,為什么多獲取了一次數據庫連接,就導致后面數據源不正常,就得看第一版代碼的坑爹之處:
標記存上下文:
public class DataRouteContext {
private static ThreadLocal<Deque<String>> route = new ThreadLocal<>();
public static String getRoute(){
Deque<String> deque = route.get();
if (deque == null || deque.size() == 0) {
return null;
}
return deque.pop();
}
Aspect:
@Aspect
@Component
@Order(1)
public class DataRouteAspect {
// @Around("execution(public * *(..)) && @annotation(dataRoute))")
@Around("@annotation(dataRoute)")
public Object setRouteName(ProceedingJoinPoint jp, DataRoute dataRoute) throws Throwable {
String routeKey = dataRoute.value();
DataRouteLogger.info("Aspect 數據路由設置為:"+routeKey);
if (StringUtils.isNotBlank(routeKey)) {
DataRouteContext.setRoute(routeKey);
}
return jp.proceed();
}
}
獲取數據源:
@Override
public Connection getConnection(String username, String password) throws SQLException {
DataSource ds = null;
String routeName = DataRouteContext.getRoute();
if (routeName != null) {
DataRouteLogger.info("dataSource changed , current dataSource is:"+routeName);
ds = sourceMap.get(routeName);
} else {
DataRouteLogger.info("current dataSource is:defaultSource");
ds = this.defaultSource;
}
if (ds == null){
DataRouteLogger.error("dataSource is:" + routeName + " not found");
throw new IllegalArgumentException("dataSource is: " + routeName + "not found");
}
if(username == null || password == null) {
return ds.getConnection();
}
return ds.getConnection(username, password);
}
AOP將在需要切換數據源的方法前,往線程上下文隊列里放一個數據源名稱,然后獲取數據源時,會根據上下文隊列里取到的數據源名稱,切換不同的數據源,取不到,則為默認數據源。
標記存放方式是隊列,取是用pop(),返回並移除,存一次用一次,之前分頁插件配置了方言,所以不會中間獲取一次數據源,一切正常。當我刪除了方言配置,中間獲取了一次,就導致消耗掉了一次標記,到了正式使用的時候,就再拿不到對應數據源。
為什么之后又正常,那是因為分頁插件,加載了一次方言后,就不再加載。所以之后獲取數據源就正常了。
修復
要解決上面的問題,就需要解決數據源標記丟失的問題,所以修改了上下文隊列獲取標記的方法,將pop()改成peek()。返回數據,不移除。
public class DataRouteContext {
private static ThreadLocal<Deque<String>> route = new ThreadLocal<>();
public static String getRoute(){
Deque<String> deque = route.get();
if (deque == null || deque.size() == 0) {
return null;
}
return deque.peek();
}
然后修改了AOP邏輯,增加了reset動作
@Aspect
@Component
@Order(1)
public class DataRouteAspect {
// @Around("execution(public * *(..)) && @annotation(dataRoute))")
@Around("@annotation(dataRoute)")
public Object setRouteName(ProceedingJoinPoint jp, DataRoute dataRoute) throws Throwable {
String routeKey = dataRoute.value();
DataRouteLogger.info("Aspect 數據路由設置為:"+routeKey);
if (StringUtils.isNotBlank(routeKey)) {
DataRouteContext.setRoute(routeKey);
}
Object result = jp.proceed();
DataRouteContext.reset();
return result;
}
}
至此,之前的BUG就解決了。
另一個問題
分頁插件多數據源配置,還需要新增一個參數
autoRuntimeDialect=true