<kbd id="5sdj3"></kbd>
<th id="5sdj3"></th>

  • <dd id="5sdj3"><form id="5sdj3"></form></dd>
    <td id="5sdj3"><form id="5sdj3"><big id="5sdj3"></big></form></td><del id="5sdj3"></del>

  • <dd id="5sdj3"></dd>
    <dfn id="5sdj3"></dfn>
  • <th id="5sdj3"></th>
    <tfoot id="5sdj3"><menuitem id="5sdj3"></menuitem></tfoot>

  • <td id="5sdj3"><form id="5sdj3"><menu id="5sdj3"></menu></form></td>
  • <kbd id="5sdj3"><form id="5sdj3"></form></kbd>

    建議自查!MySQL驅(qū)動Bug引發(fā)的事務(wù)不回滾問題,也許你正面臨該風(fēng)險!

    共 7021字,需瀏覽 15分鐘

     ·

    2022-06-28 18:19

    不點藍字關(guān)注,我們哪來故事?


    作者:KL博主

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

    關(guān)于事務(wù)不回滾的問題,我們之前有講過多期:

    • 我來出個題:這些事務(wù)會不會回滾?大概率你會錯!

    • 為什么catch了異常,但事務(wù)還是回滾了?

    • 群友:事務(wù)中的異常不也拋出了,為什么沒catch到而回滾?

    • 為什么加了@Transactional注解,事務(wù)沒有回滾?

    今天分享一個開源文檔在線預(yù)覽項目解決方案kkFileView作者發(fā)現(xiàn)的一個最新情況。

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

    背景

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

    • commons-db : 我們內(nèi)部維護的,一個采用注解驅(qū)動的 Spring 生態(tài)下的多數(shù)據(jù)源管理組件。組件給每個 DataSource 預(yù)設(shè)了些性能優(yōu)化的默認(rèn)值,沒有全部列出,不過包含了影響問題走向的屬性(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 : 用來測試組件功能的項目,會作為和出現(xiàn)問題的項目做行為測試對比。spring-boot:2.5.4、mysql-connector-java:8.0.26
    • store:游戲庫項目,正是這個項目發(fā)現(xiàn)了問題。spring-boot:2.6.6 、mysql-connector-java:8.0.28
    • 阿里云 RDS (MySQL): 阿里云 MySQL 默認(rèn)的隔離級別為 READ_COMMITTED,而 MySQL 默認(rèn)的隔離級別為 REPEATABLE_READ

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

    問題

    一天,開發(fā)反饋,在 store 項目里使用 commons-db 組件時,出現(xiàn)了事務(wù)回滾不生效的問題。如下圖代碼所示:

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

    假設(shè)

    • 假設(shè) 1:曾假設(shè)過是不是 @Transactional 的 aop 沒生效,導(dǎo)致并未開啟顯式事務(wù)。
    • 假設(shè) 1 不成立,因為在開啟了 debug 日志模式后,清晰的輸出了事務(wù)每個階段的行為日志,如:
    img
    • 假設(shè) 2:考慮到使用了 commons-db , 如果框架層連接管理問題,導(dǎo)致了事務(wù)的開啟、事務(wù)回滾時獲取到的連接不一致,也有可能導(dǎo)致這個問題。
    • 假設(shè) 2 不成立:馬上就否了,因為從上面日志上可以看到連接是同一個連接。而且不同連接執(zhí)行非預(yù)期的開啟、回滾事務(wù)操作應(yīng)該會拋異常才是。

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

    轉(zhuǎn)機

    轉(zhuǎn)機 1

    隨后,我在 java-project 項目里,使用相同的 MySQL 測試了下,發(fā)現(xiàn)事務(wù)回滾成功了。說明這個問題僅僅影響特定的環(huán)境,而且可以通過對比兩個項目的差異找到問題,離真相更近了。

    轉(zhuǎn)機 2

    開發(fā)那邊又傳來一個關(guān)鍵的信息,在 store 項目中,當(dāng)設(shè)置隔離級別為 REPEATABLE_READ 時,事務(wù)回滾生效了。代碼如:

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

    到這里,然道要懷疑是隔離級別的問題么?顯然是不成立的,因為對事務(wù)的認(rèn)知字典里,就沒出現(xiàn)過隔離級別影響事務(wù)回滾的字條。然后從 java-project 的測試也可以看出,在相同的 RC 隔離級別下,java-project 可以成功。

    第一個解決方法

    然后終歸是向前進了一步了,可以臨時用設(shè)置隔離級別的辦法來解決【事務(wù)回滾不生效問題】。不過,不同的隔離級別,對事務(wù)鎖、并發(fā)性能是不一樣,這個在調(diào)整前必須要有預(yù)期。

    轉(zhuǎn)機 3

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

    img

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

    分析

    上文整了一堆,還沒發(fā)現(xiàn)真實問題。所以先不做其他測試了,先分析下有預(yù)期后,在針對性去驗證。

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

    • 1、在添加了 @Transactional 的方法執(zhí)行前,會執(zhí)行事務(wù)管理器(DataSourceTransactionManager)的 doBegin 方法創(chuàng)建一個事務(wù),在 doBegin 方法里,會設(shè)置 autoCommit = false。會判當(dāng)前隔離級別是否和用戶定義的一致,否則就更新隔離級別。
    img
    • 2、方法執(zhí)行失敗后,會執(zhí)行事務(wù)管理器(DataSourceTransactionManager)的 doRollback 方法回滾事務(wù)。

    從 Spring Transactional 的事務(wù)日志沒看出來問題,創(chuàng)建事務(wù)、設(shè)置手動提交事務(wù)、回滾事務(wù)都有日志打印。那么我們就深入到驅(qū)動層、或者抓包看,是否這些指令都發(fā)到 MySQL Server 了。

    定位問題

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

    • 事務(wù)回滾失敗時,事務(wù)流程并未執(zhí)行 SET autocommit=0 指令。

    等于說事務(wù)回滾失敗時,事務(wù)一直是自動提交的模式,所以,異?;貪L操作并不會回滾已經(jīng)持久化了的數(shù)據(jù)。

    發(fā)現(xiàn)這個問題后,接著定位為什么 Spring 執(zhí)行了 Set autoCommit=false , 而最終確并未執(zhí)行的問題,這里再次通過【轉(zhuǎn)機 1】的 java-project 項目做單步調(diào)試對比,發(fā)現(xiàn)一段關(guān)鍵代碼(ConnectionImpl.setAutoCommit ())兩個項目里的代碼不一致:

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

    img

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

    img

    這里稍微介紹下這個參數(shù)

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

    這個參數(shù)有助于減少和 MySQL 的交互,可以提升寫數(shù)據(jù)性能。所以在參數(shù)性能優(yōu)化時,被默認(rèn)設(shè)置為 true 了。這里,如果 useLocalSessionState=false,則正好會掩蓋這個 bug。

    解密

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

    第二個解決方法

    排查到這里,第二個解決問題的方法就出現(xiàn)了,只需要讓判斷是否需要執(zhí)行 Set autocommit=0 時的 needsSetOnServer=true 成立就行了。所以,只要對 store 應(yīng)用做如下兩個參數(shù)任一參數(shù)配置調(diào)整,則可以解決問題了。這個方法比第一個方法要合適些:

    useLocalSessionState=false
    auto-commit=false

    解釋為啥 isolation 設(shè)置成 Isolation.REPEATABLE_READ 會生效

    所以到這里就結(jié)束了嗎?并沒有,預(yù)期是即使 useLocalSessionState=ture ,事務(wù)也應(yīng)該完整。然后別忘了 isAutoCommit () 和 isAutocommit () 的差異。先來看下他們的定義:

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

    原來在 mysql-connector-java:8.0.28 驅(qū)動里,使用 statusFlags 狀態(tài)代替了 autoCommit 的標(biāo)識(這里先不考究為什么做這個改動),這個解釋了

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

    這里雖然知道了原因,也確切知道 isAutoCommit () != isAutocommit () ,但是為啥做如此改動確并不清楚。這里具體問題暫且不表,先來復(fù)現(xiàn)下問題。

    復(fù)現(xiàn)問題

    既然問題已經(jīng)大差不差的定位到了,那么按常規(guī)排查流程,按預(yù)期的問題場景復(fù)現(xiàn)下,明確下問題邊界。因為還還有可能有其他的影響因素一起導(dǎo)致的問題。在 java-project 項目中,做如下依賴的版本調(diào)整

    • 升級 spring-boot:2.6.6 版本和 store 保持一致:問題復(fù)現(xiàn)了
    • 保持 spring-boot:2.5.4,調(diào)整 mysql-connector-java:8.0.28 :問題也復(fù)現(xiàn)了

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

    確認(rèn) 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 ,發(fā)現(xiàn),最新版本的又改回了 isAutoCommit () 了,然后 Commit Message 明確說明了這是 8.0.28 版本的 bug,如。

    img

    至此,終于真相大白了。

    修復(fù)

    • 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 公告說明,已經(jīng)修復(fù)了 8.0.28 在設(shè)置 useLocalSessionState=true 的情況下,autoCommit 狀態(tài)設(shè)置的問題。所以,應(yīng)用升級到 mysql-connector-java:8.0.29 版本即可

    結(jié)語

    先總結(jié)下問題表像為 Spring Transactional【事務(wù)回滾不生效,回滾前提交的數(shù)據(jù)不會回滾】,根本原因是 【mysql-connector-java:8.0.28 版本提交的一個改動 bug ,導(dǎo)致在啟用 useLocalSessionState=true 的情況下,autoCommit 狀態(tài)設(shè)置有問題】。

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

    最后,轉(zhuǎn)發(fā)本文支持一下作者,同時也讓更多小伙伴知道并提前處理該問題,避免半夜被叫起來處理問題的尷尬吧!

    ////// END //////
    ↓ 點擊下方關(guān)注,看更多架構(gòu)分享 ↓
    瀏覽 53
    點贊
    評論
    收藏
    分享

    手機掃一掃分享

    分享
    舉報
    評論
    圖片
    表情
    推薦
    點贊
    評論
    收藏
    分享

    手機掃一掃分享

    分享
    舉報

    <kbd id="5sdj3"></kbd>
    <th id="5sdj3"></th>

  • <dd id="5sdj3"><form id="5sdj3"></form></dd>
    <td id="5sdj3"><form id="5sdj3"><big id="5sdj3"></big></form></td><del id="5sdj3"></del>

  • <dd id="5sdj3"></dd>
    <dfn id="5sdj3"></dfn>
  • <th id="5sdj3"></th>
    <tfoot id="5sdj3"><menuitem id="5sdj3"></menuitem></tfoot>

  • <td id="5sdj3"><form id="5sdj3"><menu id="5sdj3"></menu></form></td>
  • <kbd id="5sdj3"><form id="5sdj3"></form></kbd>
    免费观看黄色的网站 | 99国产婷婷踪合在线免费视频 | 伊人网大香蕉视频在线观看 | 日韩av高清无码 五月天婷婷丁香网 | 性猛交XXXXX富婆免费视频 |