起因
rhea專案有兩個ut一直都是掛的,之前也經過幾個同事排查過,但是都沒有找到解決辦法,慢慢的這個問題就擱置了,因為之前負責rhea專案的同事離職,我臨時接手了這個專案,剛好最近來了一個新同事在做新的功能開發的時候遇到了這個問題,于是我就接了一個鍋,最終證明這個鍋很好玩,
rhea是一個典型的使用mybatis orm的springboot專案,我們使用h2記憶體資料庫做單元測驗,每個單元測驗都在一個事務內,都由Transactional進行注解,testGetBGWechatAccountByOpenid這個ut的核心呼叫鏈如下

呼叫深度較深,并且有多處使用到了事務,其中BasePlatformUserService.insert這個方法用到了Propagation.REQUIRES_NEW,也就是圖中最右邊的這個鏈路中最終插入了一個PlatformUser
ut代碼如下:
@Test
@Transactional
public void testGetBGWechatAccountByOpenid() {
OpenidRo openidRo = OpenidRo.builder()
.openid(openidZmall)
.appId(appIdZmall)
.unionid(unionid)
.openAppId(openAppId)
.platformCategory(PlatformCategoriesEnum.Zmall.getValue())
.service(ServicesEnum.Server.getValue())
.serviceBusinessGroupId(serviceBusinessGroup2)
.alived(false)
.build();
RheaAccount rheaAccount = platformUserService.getAccountByOpenId(openidRo);
Assert.assertEquals(rheaAccount.getPhone(), phone2);
RheaPlatformUser platformUser = platformUserMapper.getByOpenIdAndBG(
openidZmall, appIdZmall, serviceBusinessGroup2, ServicesEnum.Server.getValue());
Assert.assertEquals(rheaAccount.getId(), platformUser.getAccountId());
}
但是在ut里面使用getByOpenIdAndBG查詢platformUser卻是null導致最終platformUser.getAccountId()這個方法拋出了NPE,
知識儲備
排查這個問題會用到以下兩個知識點
- 事務傳播行為-Propagation
- mybatis快取
- 事務和mybatis Session的關聯
事務傳播行為
Springboot的Transactional的實作包含兩部分,一個部分是事務傳播行為,一個部分是資料庫隔離級別,代碼如下:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
@AliasFor("transactionManager")
String value() default "";
@AliasFor("value")
String transactionManager() default "";
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
...
資料庫隔離級別默認是Isolation.DEFAULT,也就是使用資料庫自身的隔離級別,Mysql的默認隔離級別是REPEATABLE_READ可重復讀,Oracle的默認事務隔離級別是讀已提交READ_COMMITTED,具體的隔離級別不在此討論,我們需要關注事務的傳播行為,也就是Propagation,Propagation實作如下:
public enum Propagation {
REQUIRED(0),
SUPPORTS(1),
MANDATORY(2),
REQUIRES_NEW(3),
NOT_SUPPORTED(4),
NEVER(5),
NESTED(6);
private final int value;
private Propagation(int value) {
this.value = https://www.cnblogs.com/CHLL55/p/value;
}
public int value() {
return this.value;
}
}
這里我們只是用到了REQUIRED和REQUIRED_NEW,REQUIRED也是默認的傳播行為,這兩個傳播行為的區別在于:
- REQUIRED:默認的spring事務傳播級別,使用該級別的特點是,如果背景關系中已經存在事務,那么就加入到事務中執行,如果當前背景關系中不存在事務,則新建事務執行,所以這個級別通常能滿足處理大多數的業務場景,
- REQUIRED_NEW:從字面即可知道,new,每次都要一個新事務,該傳播級別的特點是,每次都會新建一個事務,并且同時將背景關系中的事務掛起,執行當前新建事務完成以后,背景關系事務恢復再執行,
- 只有在被呼叫方法中的資料庫操作需要保存到資料庫中,而不管覆寫事務的結果如何時,才應該使用
REQUIRES_NEW事務屬性 - 舉個栗子:假設嘗試的所有股票交易都必須被記錄在一個審計資料庫中,出于驗證錯誤、資金不足或其他原因,不管交易是否失敗,這條資訊都需要被持久化,如果沒有對審計方法使用
REQUIRES_NEW屬性,審計記錄就會連同嘗試執行的交易一起回滾,使用REQUIRES_NEW屬性可以確保不管初始事務的結果如何,審計資料都會被保存
- 只有在被呼叫方法中的資料庫操作需要保存到資料庫中,而不管覆寫事務的結果如何時,才應該使用
mybatis快取
Mybatis-config.xml中可以配置mybatis的本地快取范圍localCacheScope,
mybatis官網解釋:MyBatis 利用本地快取機制(Local Cache)防止回圈參考(circular references)和加速重復嵌套查詢, 默認值為 SESSION,這種情況下會快取一個會話中執行的所有查詢, 若設定值為 STATEMENT,本地會話僅用在陳述句執行上,對相同 SqlSession 的不同呼叫將不會共享資料,
用白話解釋:
- SESSION范圍的快取:在同一個SqlSession中多次查詢會快取的mapper中的方法,經過驗證,key是單個查詢方法
- 連續查詢則后續的查詢會使用第一個查詢的快取結果——debug時無法找到查詢的sql日志
- 間斷的查詢則會實際執行每個查詢操作——可以找到每個查詢的sql日志
- 連續的定義是:在當前Session中執行DML操作,或者開啟了其他Session執行了DML操作,都認為是連續
- STATEMENT范圍的快取:本質是不使用快取
在新版本的mysql中資料庫自身有自己的快取,我們并不需要Mybatis的快取,而且Mybatis不是最底層的快取,因為多個Session的存在,往往導致一些問題,
修改mybatis的默認快取范圍可以在Mybatis-config.xml中加入以下配置:
<!--設定快取作用域,它決定是否使用mybatis的快取, 系統默認值是SESSION,為了不使用mybatis快取,設定為STATEMENT -->
<setting name="localCacheScope" value=https://www.cnblogs.com/CHLL55/p/"STATEMENT"/>
使用以下配置可以列印出mybatis執行時的操作log和sql陳述句:
<setting name="logImpl" value=https://www.cnblogs.com/CHLL55/p/"STDOUT_LOGGING" />
事務和mybatis Session的關聯
開啟一個新的事務并且在新的事務中首次執行mybatis操作時會開啟新的mybatis Session,因此在REQUIRES_NEW中執行mybatis操作一定會開啟新的Session
排查程序
- 確保mapper方法對應的sql是對的
- 將使用
REQUIRES_NEW的方法改為默認的REQUIRED,發現能查詢到platformUser - 在ut中使用其他方法查詢插入的platformUser,發現能查詢到
- mybatis配置加上日志,debug發現ut中的查詢platformUserMapper.getByOpenIdAndBG發現沒有列印sql
- 猜測可能是查詢是用了mybatis快取,取消快取發現可以查詢到真實記錄
- 分析:
REQUIRES_NEW開啟的新事務中開啟的新Session插入的記錄并沒有打破老Session快取的查詢結果,因此在老Session中使用相同的查詢陳述句是查詢不到真實記錄的
具體的debug日志如下:

紅框中的就是最外層的事務開啟的老session,綠色框是中間REQUIRES_NEW新事務中開啟的新Session,所以對于紅框這個Session而言,它并不知道已經發生了DML操作,因此在后續繼續查詢時會使用最開始的查詢結果,也就是null,
這種問題通常發生在getOrCreate操作中,
解決
去掉Mybatis層面的快取
<!--設定快取作用域,它決定是否使用mybatis的快取, 系統默認值是SESSION,為了不使用mybatis快取,設定為STATEMENT -->
<setting name="localCacheScope" value=https://www.cnblogs.com/CHLL55/p/"STATEMENT"/>
解決這個問題對于REQUIRES_NEW這個傳播行為的理解就更深刻了,
參考:
- 了解事務陷阱:https://www.ibm.com/developerworks/cn/java/j-ts1.html
- Spring五個事務隔離級別和七個事務傳播行為:https://blog.csdn.net/caoxiaohong1005/article/details/79984912
- Innodb中的事務隔離級別和鎖的關系:https://tech.meituan.com/2014/08/20/innodb-lock.html
- Mybatis XML配置:https://mybatis.org/mybatis-3/zh/configuration.html
記得幫我點贊哦!
精心整理了計算機各個方向的從入門、進階、實戰的視頻課程和電子書,按照目錄合理分類,總能找到你需要的學習資料,還在等什么?快去關注下載吧!!!

念念不忘,必有回響,小伙伴們幫我點個贊吧,非常感謝,
我是職場亮哥,YY高級軟體工程師、四年作業經驗,拒絕咸魚爭當龍頭的斜杠程式員,
聽我說,進步多,程式人生一把梭
如果有幸能幫到你,請幫我點個【贊】,給個關注,如果能順帶評論給個鼓勵,將不勝感激,
職場亮哥文章串列:更多文章

本人所有文章、回答都與著作權保護平臺有合作,著作權歸職場亮哥所有,未經授權,轉載必究!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/27406.html
標籤:Java
上一篇:JAVA 復寫
