建議自查!MySQL驅動Bug引發的事務不回滾問題,也許你正面臨該風險!

程序猿DD_ 2022-06-23 14:26:01 阅读数:976

自查mysqlbug不回正面

cfc88d26b1077d8566245effbeea22c4.png

作者:KL博主

來源:https://my.oschina.net/klblog/blog/5542934

關於事務不回滾的問題,我們之前有講過多期:

今天分享一個開源文檔在線預覽項目解决方案kkFileView作者發現的一個最新情况。

如標題,最終查明問題是因為 mysql-connector-java:8.0.28 的一個 bug 導致的。但是在真相未浮出之前,整個問題可謂撲朔迷離,博主好久沒有排查過如此得勁的 bug ,隨著一層層的 debug 深入,真相也隨之浮出水面。這個問題屬於底層 jdbc 驅動的問題,具有普遍性,可能不知不覺中,你的應用也在線上遭受這個 bug 的摧殘,所以,請耐心聽我講完這個故事,然後回去檢查下你的應用狀態,是否也踩坑了。喜歡直接的可以直接拉到文末結語看結果。

背景

講故事一般先介紹人物、背景。這裏也不列外,先把相關方介紹下。通常,故事情節越豐富越精彩,但是這裏博主會考慮篇幅 (不講廢話) 會把一些與結果走向無關的細節忽略掉,力求敘述完整就好。

  • commons-db : 我們內部維護的,一個采用注解驅動的 Spring 生態下的多數據源管理組件。組件給每個 DataSource 預設了些性能優化的默認值,沒有全部列出,不過包含了影響問題走向的屬性(useLocalSessionState),如下:

Properties defaultProperties = new Properties();
defaultProperties.put("prepStmtCacheSize", 300);
defaultProperties.put("prepStmtCacheSqlLimit", 2048);
defaultProperties.put("useLocalSessionState", true);
defaultProperties.put("cacheResultSetMetadata", true);
defaultProperties.put("elideSetAutoCommits", true);
  • java-project : 用來測試組件功能的項目,會作為和出現問題的項目做行為測試對比。spring-boot:2.5.4、mysql-connector-java:8.0.26

  • store:遊戲庫項目,正是這個項目發現了問題。spring-boot:2.6.6 、mysql-connector-java:8.0.28

  • 阿裏雲 RDS (MySQL): 阿裏雲 MySQL 默認的隔離級別為 READ_COMMITTED,而 MySQL 默認的隔離級別為 REPEATABLE_READ

說明:java-project 和 store 的 commons-db 版本其實不一樣,因為不影響結果。這裏假設他們版本一致。

問題

一天,開發反饋,在 store 項目裏使用 commons-db 組件時,出現了事務回滾不生效的問題。如下圖代碼所示:

@Transactional
@DataSource(type = Type.MASTER,value = "developer")
public void addUser(ApolloUser user){
    userRepository.save(user);
    int i = 1/0; //拋异常
}
  • 具體錶現為:執行 addUser 方法,當 1/0 拋出 RuntimeException 類型异常時,user 對象還是添加成功了。一句話總結就是,【事務回滾不生效了】。

假設

  • 假設 1:曾假設過是不是 @Transactional 的 aop 沒生效,導致並未開啟顯式事務。

  • 假設 1 不成立,因為在開啟了 debug 日志模式後,清晰的輸出了事務每個階段的行為日志,如:

a4f1f78b2ca4a976f85ba354fff6d26b.png
img
  • 假設 2:考慮到使用了 commons-db , 如果框架層連接管理問題,導致了事務的開啟、事務回滾時獲取到的連接不一致,也有可能導致這個問題。

  • 假設 2 不成立:馬上就否了,因為從上面日志上可以看到連接是同一個連接。而且不同連接執行非預期的開啟、回滾事務操作應該會拋异常才是。

那麼到這裏,問題陷入了僵局。不禁沉思,一個看上去人畜無害的代碼,一個看上去邏輯清晰的事務日志,為什麼會事務回滾失效呢?????

轉機

轉機 1

隨後,我在 java-project 項目裏,使用相同的 MySQL 測試了下,發現事務回滾成功了。說明這個問題僅僅影響特定的環境,而且可以通過對比兩個項目的差异找到問題,離真相更近了。

轉機 2

開發那邊又傳來一個關鍵的信息,在 store 項目中,當設置隔離級別為 REPEATABLE_READ 時,事務回滾生效了。代碼如:

@Transactional(isolation = Isolation.REPEATABLE_READ)
   @DataSource(type = Type.MASTER,value = "developer")
   public void addUser(ApolloUser user){
       userRepository.save(user);
       int i = 1/0;
   }

到這裏,然道要懷疑是隔離級別的問題麼?顯然是不成立的,因為對事務的認知字典裏,就沒出現過隔離級別影響事務回滾的字條。然後從 java-project 的測試也可以看出,在相同的 RC 隔離級別下,java-project 可以成功。

第一個解决方法

然後終歸是向前進了一步了,可以臨時用設置隔離級別的辦法來解决【事務回滾不生效問題】。不過,不同的隔離級別,對事務鎖、並發性能是不一樣,這個在調整前必須要有預期。

轉機 3

事出反常必有妖,本著不信是隔離級別導致的問題,我在 store 項目裏將  isolation 設置成 Isolation.READ_UNCOMMITTED ,發現事務回滾也生效了。這也說明了和隔離級別沒有直接的關系。然後本著探究【為啥默認的 READ_COMMITTED 導致事務不生效?】的思路排查了下,發現了些問題,如下代碼是事務邏輯中的一部分(源碼見:DataSourceUtils.prepareConnectionForTransaction ()):

e54b6a9e9e63a14b059ac97b05a35c9e.png
img

發現,相比 RR、RU , 差別就是當隔離級別是 READ_COMMITTED 時,不會在對 session 有更新操作了。到這一步也只是多了一個明確的現象,可以解釋知道真相後的行為,並沒有觸達真相邊緣。

分析

上文整了一堆,還沒發現真實問題。所以先不做其他測試了,先分析下有預期後,在針對性去驗證。

先來看下普遍的正常的 Spring Transactional 完整的事務回滾的過程,普遍的指的是沒有做過特殊參數配置的,一般這些參數也不會配置。

  • 1、在添加了 @Transactional 的方法執行前,會執行事務管理器(DataSourceTransactionManager)的 doBegin 方法創建一個事務,在 doBegin 方法裏,會設置 autoCommit = false。會判當前隔離級別是否和用戶定義的一致,否則就更新隔離級別。

3caedc85784b8c4df05452d161cafb62.png
img
  • 2、方法執行失敗後,會執行事務管理器(DataSourceTransactionManager)的 doRollback 方法回滾事務。

從 Spring Transactional 的事務日志沒看出來問題,創建事務、設置手動提交事務、回滾事務都有日志打印。那麼我們就深入到驅動層、或者抓包看,是否這些指令都發到 MySQL Server 了。

定比特問題

如分析,在 store 項目中,將斷點打在 mysql-connector-java 驅動的 NativeSession.execSQL () 方法裏,和 MySQL Server 交互的所有指令,最終都會調用這個方法執行。果然發現了問題:

  • 事務回滾失敗時,事務流程並未執行 SET autocommit=0 指令。

等於說事務回滾失敗時,事務一直是自動提交的模式,所以,异常回滾操作並不會回滾已經持久化了的數據。

發現這個問題後,接著定比特為什麼 Spring 執行了 Set autoCommit=false , 而最終確並未執行的問題,這裏再次通過【轉機 1】的 java-project 項目做單步調試對比,發現一段關鍵代碼(ConnectionImpl.setAutoCommit ())兩個項目裏的代碼不一致:

java-project,mysql-connector-java:8.0.26(事務回滾生效)

a7191cabea3c5e23188ed1d6592c260b.png
img

store,mysql-connector-java:8.0.28(事務回滾不生效)

5c7073d8f8897239edf3507e6f28fa3c.png
img

這裏稍微介紹下這個參數

  • useLocalSessionState:維護本地 sessionState , 在需要判斷 【事務提交模式】、【隔離級別】設置時,獲取本地狀態,而不是每次像 MySQL Server 發起詢問。

這個參數有助於减少和 MySQL 的交互,可以提昇寫數據性能。所以在參數性能優化時,被默認設置為 true 了。這裏,如果 useLocalSessionState=false,則正好會掩蓋這個 bug。

解密

因為在 store,mysql-connector-java:8.0.28 有問題的版本的 isAutocommit () 行為邏輯和 isAutoCommit () 不一致,本該調用判斷 isAutocommit 返回 true 時,卻返回了 false。最終才導致了 store 在接收到 Spring Transactional 設置 autoCommit=false 的請求時,因為 needsSetOnServer=false , 直接跳過了真正的發起 Set autocommit=0 指令的執行。導致當前事務模式是自動提交模式,所以當事務裏有任何增删改操作時,會在執行完後立馬 commit 持久化。這時如果异常而發起事務 rollback ,自然不會回滾之前已經自動提交的事務。這個很好的解釋了開頭貼出的事務日志很完整,但是事務就是回滾不生效的問題。

第二個解决方法

排查到這裏,第二個解决問題的方法就出現了,只需要讓判斷是否需要執行 Set autocommit=0 時的 needsSetOnServer=true 成立就行了。所以,只要對 store 應用做如下兩個參數任一參數配置調整,則可以解决問題了。這個方法比第一個方法要合適些:

useLocalSessionState=false
auto-commit=false

解釋為啥 isolation 設置成 Isolation.REPEATABLE_READ 會生效

所以到這裏就結束了嗎?並沒有,預期是即使 useLocalSessionState=ture ,事務也應該完整。然後別忘了 isAutoCommit () 和 isAutocommit () 的差异。先來看下他們的定義:

public boolean isAutocommit() {
  return (this.statusFlags & 2) != 0;
}
 
public boolean isAutoCommit() {
  return this.autoCommit;
}

原來在 mysql-connector-java:8.0.28 驅動裏,使用 statusFlags 狀態代替了 autoCommit 的標識(這裏先不考究為什麼做這個改動),這個解釋了

  • 轉機 2:當設置隔離級別為 REPEATABLE_READ 時,事務回滾生效了。是因為當用戶定義的隔離級別 RR 和默認的 RC 不一致時,會觸發 session 設置新的隔離級別,此時也會將 statusFlags = 0 更新為 statusFlags = 2. 故在調用 isAutocommit () 返回 true ,滿足了執行 SET autocommit=0 指令的條件。

這裏雖然知道了原因,也確切知道 isAutoCommit () != isAutocommit () ,但是為啥做如此改動確並不清楚。這裏具體問題暫且不錶,先來複現下問題。

複現問題

既然問題已經大差不差的定比特到了,那麼按常規排查流程,按預期的問題場景複現下,明確下問題邊界。因為還還有可能有其他的影響因素一起導致的問題。在 java-project 項目中,做如下依賴的版本調整

  • 昇級 spring-boot:2.6.6 版本和 store 保持一致:問題複現了

  • 保持 spring-boot:2.5.4,調整 mysql-connector-java:8.0.28 :問題也複現了

到這裏,基本排除了 Spring Transactional 的嫌疑了。然後將矛頭鎖定到了 mysql-connector-java:8.0.28 身上。

確認 bug

考慮到從 mysql-connector-java:8.0.26 的 isAutoCommit 更改到了 mysql-connector-java:8.0.28 的 isAutocommit 肯定是有原因的,帶著弄清楚代碼作者提交這個改動的意圖,去翻了下 github。

  • https://github.com/mysql/mysql-connector-j

找了下 github 的提交記錄  commit ,發現,最新版本的又改回了 isAutoCommit () 了,然後 Commit Message 明確說明了這是 8.0.28 版本的 bug,如。

07784f2f58613bc680b78f1fd4750b05.png
img

至此,終於真相大白了。

修複

  • 8.0.29 release:https://dev.mysql.com/doc/relnotes/connector-j/8.0/en/news-8-0-29.html

  • A connection did not maintain the correct autocommit state when it was used in a pool with useLocalSessionState=true. (Bug #106435, Bug #33850099)

最終解决方法

如 8.0.29 release 公告說明,已經修複了 8.0.28 在設置 useLocalSessionState=true 的情况下,autoCommit 狀態設置的問題。所以,應用昇級到 mysql-connector-java:8.0.29 版本即可

結語

先總結下問題錶像為 Spring Transactional【事務回滾不生效,回滾前提交的數據不會回滾】,根本原因是 【mysql-connector-java:8.0.28 版本提交的一個改動 bug ,導致在啟用 useLocalSessionState=true 的情况下,autoCommit 狀態設置有問題】。

然後因為 spring-boot:2.6.3 ~ 2.6.7 ,這五個版本默認的 MySQL 驅動就是 mysql-connector-java:8.0.28 ,而 useLocalSessionState=true 幾乎是 Java JDBC DataSource 裏的標配,所以這個 bug 估計會影響一大波人。然後因為只是影響回滾操作,所以這個問題會隱藏的很深,不容易察覺,所謂影響深遠。

最後,轉發本文支持一下作者,同時也讓更多小夥伴知道並提前處理該問題,避免半夜被叫起來處理問題的尷尬吧

我們創建了一個高質量的技術交流群,與優秀的人在一起,自己也會優秀起來,趕緊點擊加群,享受一起成長的快樂。另外,如果你最近想跳槽的話,年前我花了2周時間收集了一波大廠面經,節後准備跳槽的可以點擊這裏領取

推薦閱讀

··································

你好,我是程序猿DD,10年開發老司機、阿裏雲MVP、騰訊雲TVP、出過書創過業、國企4年互聯網6年。從普通開發到架構師、再到合夥人。一路過來,給我最深的感受就是一定要不斷學習並關注前沿。只要你能堅持下來,多思考、少抱怨、勤動手,就很容易實現彎道超車!所以,不要問我現在幹什麼是否來得及。如果你看好一個事情,一定是堅持了才能看到希望,而不是看到希望才去堅持。相信我,只要堅持下來,你一定比現在更好!如果你還沒什麼方向,可以先關注我,這裏會經常分享一些前沿資訊,幫你積累彎道超車的資本。

點擊領取2022最新10000T學習資料

版权声明:本文为[程序猿DD_]所创,转载请带上原文链接,感谢。 https://gsmany.com/2022/174/202206231338592507.html