Mybatis?Interceptor線程安全引發(fā)的bug問題
Interceptor線程安全引發(fā)的bug
先看下發(fā)現(xiàn)這個bug的一個背景,但背景中的問題,并非這個bug導致:
最近業(yè)務部門的一位開發(fā)同事找過來說,自己在使用公司的框架向數(shù)據(jù)庫新增數(shù)據(jù)時,新增的數(shù)據(jù)被莫名其妙的回滾了,并且本地開發(fā)環(huán)境能夠復現(xiàn)這個問題。
公司的框架是基于SpringBoot+Mybatis整合實現(xiàn),按道理這么多項目已經(jīng)在使用了, 如果是bug那么早就應該出現(xiàn)問題。
我的第一想法是不是他的業(yè)務邏輯有啥異常導致事務回滾了,但是也并沒有出現(xiàn)什么明顯的異常,并且新增的數(shù)據(jù)在數(shù)據(jù)庫中是可以看到的。
于是猜測有定時任務在刪數(shù)據(jù)。詢問了這位同事,得到的答案卻是否定的。
沒有辦法,既然能本地復現(xiàn)那便是最好解決了,決定在本地開發(fā)環(huán)境跟源碼找問題。
剛開始調(diào)試時只設置了幾個斷點,代碼執(zhí)行流程一切正常,查看數(shù)據(jù)庫中新增的數(shù)據(jù)也確實存在,但是當代碼全部執(zhí)行完成后,數(shù)據(jù)庫中的數(shù)據(jù)卻不存在了,程序也沒有任何異常。
繼續(xù)深入斷點調(diào)試,經(jīng)過十幾輪的斷點調(diào)試發(fā)現(xiàn)偶爾會出現(xiàn)org.apache.ibatis.executor.ExecutorException: Executor was closed.,但是程序跳過一些斷點時,就一切正常。
在經(jīng)過n輪調(diào)試未果之后,還是懷疑數(shù)據(jù)庫有定時任務或者數(shù)據(jù)庫有問題。
于是重新創(chuàng)建一個測試庫新增數(shù)據(jù),這次數(shù)據(jù)新增一切正常,此時還是滿心歡喜,至少已經(jīng)定位出問題的大致原因了,趕緊找了DBA幫忙查詢是否有SQL在刪數(shù)據(jù),果然證實了自己的想法。
后來讓這位開發(fā)同事再次確認是否在開發(fā)環(huán)境的機器上有定時任務有刪除數(shù)據(jù)的服務。
這次盡然告訴我確實有定時任務刪數(shù)據(jù),問題得以解決,原來他是新接手這個項目,對項目不是很熟悉,真的。。。。。。
現(xiàn)在我們回到標題重點沒有考慮Interceptor線程安全,導致斷點調(diào)試時才會出現(xiàn)的bug
晚上下班后,突然想到調(diào)試中遇到的org.apache.ibatis.executor.ExecutorException: Executor was closed.是啥情況?難道這地方還真的是有bug?
馬上雙十一到了,這要是在雙十一時整個大bug,那問題可大了。第二天上班后,決定要深入研究一下這個問題。由于不知道是什么情況下才能觸發(fā)這個異常,只能還是一步一步斷點調(diào)試。
首先看實現(xiàn)的Mybatis攔截器,主要代碼如下:
@Intercepts({
@Signature(method = "query", type = Executor.class, args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(method = "query", type = Executor.class, args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
@Signature(method = "update", type = Executor.class, args = {MappedStatement.class, Object.class})
})
public class MybatisExecutorInterceptor implements Interceptor {
private static final String DB_URL = "DB_URL";
private Executor target;
private ConcurrentHashMap<Object, Object> cache = new ConcurrentHashMap<>();
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object proceed = invocation.proceed();
//Executor executor = (Executor) invocation.getTarget();
Transaction transaction = target.getTransaction();
if (cache.get(DB_URL) != null) {
//其他邏輯處理
System.out.println(cache.get(DB_URL));
} else if (transaction instanceof SpringManagedTransaction) {
Field dataSourceField = SpringManagedTransaction.class.getDeclaredField("dataSource");
ReflectionUtils.makeAccessible(dataSourceField);
DataSource dataSource = (DataSource) ReflectionUtils.getField(dataSourceField, transaction);
String dbUrl = dataSource.getConnection().getMetaData().getURL();
cache.put(DB_URL, dbUrl);
//其他邏輯處理
System.out.println(cache.get(DB_URL));
}
//其他邏輯略...
return proceed;
}
@Override
public Object plugin(Object target) {
if (target instanceof Executor) {
this.target = (Executor) target;
return Plugin.wrap(target, this);
}
return target;
}
}
調(diào)試過程中,一步步斷點,便會出現(xiàn)如下異常:
Caused by: org.apache.ibatis.executor.ExecutorException: Executor was closed.
at org.apache.ibatis.executor.BaseExecutor.getTransaction(BaseExecutor.java:78)
at org.apache.ibatis.executor.CachingExecutor.getTransaction(CachingExecutor.java:51)
at com.bruce.integration.mybatis.plugin.MybatisExecutorInterceptor.intercept(MybatisExecutorInterceptor.java:37)
at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:61)
根據(jù)異常信息,將代碼定位到了org.apache.ibatis.executor.BaseExecutor.getTransaction() 方法
@Override
public Transaction getTransaction() {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
return transaction;
}
發(fā)現(xiàn)當變量closed為true時會拋出異常。那么只要定位到修改closed變量值的方法不就知道了。通過idea工具的搜索只找到了一個修改該變量值的地方。
那就是org.apache.ibatis.executor.BaseExecutor#close()方法
@Override
public void close(boolean forceRollback) {
try {
....省略
} catch (SQLException e) {
// Ignore. There's nothing that can be done at this point.
log.warn("Unexpected exception on closing transaction. Cause: " + e);
} finally {
....省略
closed = true; //只有該處修改為true
}
}
于是將斷點添加到finally代碼塊中,看看什么時候會走到這個方法。
當一步步debug時,發(fā)現(xiàn)還沒有走到close方法時,closed的值已經(jīng)被修改為true,又拋出了Executor was closed.異常。
奇怪了?難道還有其他代碼會反射修改這個變量,按道理Mybatis要是修改自己代碼中的變量值,不至于用這種方式啊,太不優(yōu)雅了,還增加代碼復雜度。
沒辦法,又是經(jīng)過n次一步步的斷點調(diào)試。
終于偶然的發(fā)現(xiàn)在idea debug窗口顯示出這樣的提示信息。
Skipped breakpoint at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor:423 because it happened inside debugger evaluation

從提示上看,不過是跳過了某個斷點而已,其實之前就已經(jīng)注意到這個提示,但是這次懷著好奇搜索了下解決方案。
原來idea在展示類的成員變量,或者方法參數(shù)時會調(diào)用對象的toString(),懷著試試看的心態(tài),去掉了idea中的toString選項。

再次斷點調(diào)試,這次竟然不再出現(xiàn)異常,原來是idea顯示變量時調(diào)用對象的toString()方法搞得鬼???
難怪在BaseExecutor#close()方法中的斷點一直進不去,卻修改了變量值。
那為什么idea展示變量,調(diào)用toString()方法會導致此時查詢所使用Executor被close呢?
根據(jù)上面的提示,查看org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor源碼,看看具體是什么邏輯
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
try {
Object result = method.invoke(sqlSession, args);
if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
// force commit even on non-dirty sessions because some databases require
// a commit/rollback before calling close()
sqlSession.commit(true);
}
return result;
} catch (Throwable t) {
Throwable unwrapped = unwrapThrowable(t);
if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
// release the connection to avoid a deadlock if the translator is no loaded. See issue #22
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
sqlSession = null;
Throwable translated = SqlSessionTemplate.this.exceptionTranslator
.translateExceptionIfPossible((PersistenceException) unwrapped);
if (translated != null) {
unwrapped = translated;
}
}
throw unwrapped;
} finally {
if (sqlSession != null) {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
}
從代碼上看,這是jdk動態(tài)代理中的一個攔截器實現(xiàn)類,因為通過jdk動態(tài)代理,代理了Mybatis中的SqlSession接口,在idea中變量視圖展示時被調(diào)用了toString()方法,導致被攔截。
而invoke()方法中最后一定會在finally中關閉當前線程所關聯(lián)的sqlSession,導致調(diào)用BaseExecutor.close()方法。
為了驗證這個想法,在SqlSessionInterceptor中對攔截到的toString()方法做了如下處理,如果是toString()方法不再向下繼續(xù)執(zhí)行,只要返回是哪些接口的代碼類即可.
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (args == null && "toString".equals(method.getName())) {
return Arrays.toString(proxy.getClass().getInterfaces());
}
... 其他代碼省略
}
}
恢復idea中的設置,再次調(diào)試,果然不會再出現(xiàn)Executor was closed.異常。
這看似mybatis-spring在實現(xiàn)SqlSessionInterceptor 時考慮不周全導致的一個bug,為了不泄露公司的框架代碼還原這個bug,于是單獨搭建了SpringBoot+Mybatis整合工程,并且寫了一個類似邏輯的攔截器。
代碼如下:
@Intercepts({
@Signature(method = "query", type = Executor.class, args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(method = "query", type = Executor.class, args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
@Signature(method = "update", type = Executor.class, args = {MappedStatement.class, Object.class})
})
public class MybatisExecutorInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object proceed = invocation.proceed();
Executor executor = (Executor) invocation.getTarget();
Transaction transaction = executor.getTransaction();
//其他邏輯略...
return proceed;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
}
再次在SqlSessionInterceptor中斷點執(zhí)行,經(jīng)過幾次debug,嘗試還原這個bug時,程序盡然一路暢通完美通過,沒有任何異常。
此刻我立刻想起了之前觀察到的一段不合理代碼,在文章開頭的實例代碼中Executor被做為成員變量保存,但是mybatis中Interceptor實現(xiàn)類是在程序啟動時就被實例化的,并且是一個單實例對象。
而在每次執(zhí)行SQL時都會去創(chuàng)建一個新的Executor對象并且會經(jīng)過Interceptor的public Object plugin(Object target),用于判斷是否需要對該Executor對象進行代理。
而示例中重寫的plugin方法,每次都對Executor重新賦值,實際上這是線程不安全的。
由于在idea中debug時展示變量調(diào)用了toString()方法,同樣會創(chuàng)建SqlSession、Executor經(jīng)過plugin方法,導致Executor成員變量實際上是被替換的。

解決方案
直接通過invocation.getTarget()去獲取被代理對象即可,而不是使用成員變量。
為什么線上程序沒有報Executor was closed問題???
- 因為線上不會像在idea中一樣去調(diào)用toString() 方法
- 代碼中使用了緩存,當使用了Executor 獲取到url后,下次請求過來就不會再使用Executor對象,也就不會出現(xiàn)異常。
- 程序剛啟動時并發(fā)量不夠大,如果在程序剛起來時,立刻有足夠的請求量,仍然會拋出異常,但是只要有一次結(jié)果被緩存,后續(xù)也就不會出現(xiàn)異常。
總結(jié)
實際上還是MybatisExecutorInterceptor中將Executor做為成員變量,對Executor更改,出現(xiàn)線程不安全導致的異常。而idea中顯示變量值調(diào)用toString()方法只是讓異常發(fā)生的誘因。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
Java使用動態(tài)規(guī)劃算法思想解決背包問題
背包問題(Knapsack problem)是一種組合優(yōu)化的NP完全問題。問題可以描述為:給定一組物品,每種物品都有自己的重量和價格,在限定的總重量內(nèi),我們?nèi)绾芜x擇,才能使得物品的總價格最高2022-04-04
SpringBoot通過請求對象獲取輸入流無數(shù)據(jù)
這篇文章主要介紹了使用SpringBoot通過請求對象獲取輸入流無數(shù)據(jù),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-03-03
Spring Security使用Lambda DSL配置流程詳解
Spring Security 5.2 對 Lambda DSL 語法的增強,允許使用lambda配置HttpSecurity、ServerHttpSecurity,重要提醒,之前的配置方法仍然有效。lambda的添加旨在提供更大的靈活性,但是用法是可選的。讓我們看一下HttpSecurity的lambda配置與以前的配置樣式相比2023-02-02

