<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          一個@Transaction哪里來這么多坑?

          共 7334字,需瀏覽 15分鐘

           ·

          2020-08-28 13:56

          前言

          在之前的文章中已經(jīng)對Spring中的事務(wù)做了詳細的分析了,這篇文章我們來聊一聊平常工作時使用事務(wù)可能出現(xiàn)的一些問題(本文主要針對使用@Transactional進行事務(wù)管理的方式進行討論)以及對應(yīng)的解決方案

          1. 事務(wù)失效

          2. 事務(wù)回滾相關(guān)問題

          3. 讀寫分離跟事務(wù)結(jié)合使用時的問題


          事務(wù)失效

          事務(wù)失效我們一般要從兩個方面排查問題

          數(shù)據(jù)庫層面

          數(shù)據(jù)庫層面,數(shù)據(jù)庫使用的存儲引擎是否支持事務(wù)?默認情況下MySQL數(shù)據(jù)庫使用的是Innodb存儲引擎(5.5版本之后),它是支持事務(wù)的,但是如果你的表特地修改了存儲引擎,例如,你通過下面的語句修改了表使用的存儲引擎為MyISAM,而MyISAM又是不支持事務(wù)的

          alter?table?table_name?engine=myisam;

          這樣就會出現(xiàn)“事務(wù)失效”的問題了

          「解決方案」:修改存儲引擎為Innodb。

          業(yè)務(wù)代碼層面

          業(yè)務(wù)層面的代碼是否有問題,這就有很多種可能了

          1. 我們要使用Spring的聲明式事務(wù),那么需要執(zhí)行事務(wù)的Bean是否已經(jīng)交由了Spring管理?在代碼中的體現(xiàn)就是類上是否有@ServiceComponent等一系列注解

          「解決方案」:將Bean交由Spring進行管理(添加@Service注解)

          1. @Transactional注解是否被放在了合適的位置。在上篇文章中我們對Spring中事務(wù)失效的原理做了詳細的分析,其中也分析了Spring內(nèi)部是如何解析@Transactional注解的,我們稍微回顧下代碼:

          注解解析


          ?代碼位于:AbstractFallbackTransactionAttributeSource#computeTransactionAttribute?

          也就是說,默認情況下你無法使用@Transactional對一個非public的方法進行事務(wù)管理

          「解決方案」:修改需要事務(wù)管理的方法為public。

          1. 出現(xiàn)了自調(diào)用。什么是自調(diào)用呢?我們看個例子

          @Service
          public?class?DmzService?{
          ?
          ?public?void?saveAB(A?a,?B?b)?{
          ??saveA(a);
          ??saveB(b);
          ?}

          ?@Transactional
          ?public?void?saveA(A?a)?{
          ??dao.saveA(a);
          ?}
          ?
          ?@Transactional
          ?public?void?saveB(B?b){
          ??dao.saveB(a);
          ?}
          }

          上面三個方法都在同一個類DmzService中,其中saveAB方法中調(diào)用了本類中的saveAsaveB方法,這就是自調(diào)用。在上面的例子中saveAsaveB上的事務(wù)會失效

          那么自調(diào)用為什么會導(dǎo)致事務(wù)失效呢?我們知道Spring中事務(wù)的實現(xiàn)是依賴于AOP的,當容器在創(chuàng)建dmzService這個Bean時,發(fā)現(xiàn)這個類中存在了被@Transactional標注的方法(修飾符為public)那么就需要為這個類創(chuàng)建一個代理對象并放入到容器中,創(chuàng)建的代理對象等價于下面這個類

          public?class?DmzServiceProxy?{

          ????private?DmzService?dmzService;

          ????public?DmzServiceProxy(DmzService?dmzService)?{
          ????????this.dmzService?=?dmzService;
          ????}

          ????public?void?saveAB(A?a,?B?b)?{
          ????????dmzService.saveAB(a,?b);
          ????}

          ????public?void?saveA(A?a)?{
          ????????try?{
          ????????????//?開啟事務(wù)
          ????????????startTransaction();
          ????????????dmzService.saveA(a);
          ????????}?catch?(Exception?e)?{
          ????????????//?出現(xiàn)異常回滾事務(wù)
          ????????????rollbackTransaction();
          ????????}
          ????????//?提交事務(wù)
          ????????commitTransaction();
          ????}

          ????public?void?saveB(B?b)?{
          ????????try?{
          ????????????//?開啟事務(wù)
          ????????????startTransaction();
          ????????????dmzService.saveB(b);
          ????????}?catch?(Exception?e)?{
          ????????????//?出現(xiàn)異?;貪L事務(wù)
          ????????????rollbackTransaction();
          ????????}
          ????????//?提交事務(wù)
          ????????commitTransaction();
          ????}
          }

          上面是一段偽代碼,通過startTransaction、rollbackTransaction、commitTransaction這三個方法模擬代理類實現(xiàn)的邏輯。因為目標類DmzService中的saveAsaveB方法上存在@Transactional注解,所以會對這兩個方法進行攔截并嵌入事務(wù)管理的邏輯,同時saveAB方法上沒有@Transactional,相當于代理類直接調(diào)用了目標類中的方法。

          我們會發(fā)現(xiàn)當通過代理類調(diào)用saveAB時整個方法的調(diào)用鏈如下:

          實際上我們在調(diào)用saveAsaveB時調(diào)用的是目標類中的方法,這種清空下,事務(wù)當然會失效。

          常見的自調(diào)用導(dǎo)致的事務(wù)失效還有一個例子,如下:

          @Service
          public?class?DmzService?{
          ?@Transactional
          ?public?void?save(A?a,?B?b)?{
          ??saveB(b);
          ?}
          ?
          ?@Transactional(propagation?=?Propagation.REQUIRES_NEW)
          ?public?void?saveB(B?b){
          ??dao.saveB(a);
          ?}
          }

          當我們調(diào)用save方法時,我們預(yù)期的執(zhí)行流程是這樣的

          也就是說兩個事務(wù)之間互不干擾,每個事務(wù)都有自己的開啟、回滾、提交操作。

          但根據(jù)之前的分析我們知道,實際上在調(diào)用saveB方法時,是直接調(diào)用的目標類中的saveB方法,在saveB方法前后并不會有事務(wù)的開啟或者提交、回滾等操作,實際的流程是下面這樣的

          由于saveB方法實際上是由dmzService也就是目標類自己調(diào)用的,所以在saveB方法的前后并不會執(zhí)行事務(wù)的相關(guān)操作。這也是自調(diào)用帶來問題的根本原因:「自調(diào)用時,調(diào)用的是目標類中的方法而不是代理類中的方法」

          「解決方案」

          1. 自己注入自己,然后顯示的調(diào)用,例如:
            @Service
            public?class?DmzService?{
            ?//?自己注入自己
            ?@Autowired
            ?DmzService?dmzService;
            ?
            ?@Transactional
            ?public?void?save(A?a,?B?b)?{
            ??dmzService.saveB(b);
            ?}

            ?@Transactional(propagation?=?Propagation.REQUIRES_NEW)
            ?public?void?saveB(B?b){
            ??dao.saveB(a);
            ?}
            }
            這種方案看起來不是很優(yōu)雅
            利用AopContext,如下:
            @Service
            public?class?DmzService?{

            ?@Transactional
            ?public?void?save(A?a,?B?b)?{
            ??((DmzService)?AopContext.currentProxy()).saveB(b);
            ?}

            ?@Transactional(propagation?=?Propagation.REQUIRES_NEW)
            ?public?void?saveB(B?b){
            ??dao.saveB(a);
            ?}
            }
            ?使用上面這種解決方案需要注意的是,需要在配置類上新增一個配置
            //?exposeProxy=true代表將代理類放入到線程上下文中,默認是false
            @EnableAspectJAutoProxy(exposeProxy?=?true)
            ?
            個人比較喜歡的是第二種方式
          這里我們做個來做個小總結(jié)

          總結(jié)

          一圖勝千言
          事務(wù)失效的原因


          事務(wù)回滾相關(guān)問題

          回滾相關(guān)的問題可以被總結(jié)為兩句話
          1. 想回滾的時候事務(wù)卻提交了
          2. 想提交的時候被標記成只能回滾了(rollback only)
          先看第一種情況:「想回滾的時候事務(wù)卻提交了」。這種情況往往是程序員對Spring中事務(wù)的rollbackFor屬性不夠了解導(dǎo)致的。
          ?Spring默認拋出了未檢查unchecked異常(繼承自 RuntimeException 的異常)或者 Error才回滾事務(wù);其他異常不會觸發(fā)回滾事務(wù),已經(jīng)執(zhí)行的SQL會提交掉。如果在事務(wù)中拋出其他類型的異常,但卻期望 Spring 能夠回滾事務(wù),就需要指定rollbackFor屬性。?
          對應(yīng)代碼其實我們上篇文章也分析過了,如下:
          回滾代碼
          ?以上代碼位于:TransactionAspectSupport#completeTransactionAfterThrowing方法中?
          默認情況下,只有出現(xiàn)RuntimeException或者Error才會回滾
          public?boolean?rollbackOn(Throwable?ex)?{
          ????return?(ex?instanceof?RuntimeException?||?ex?instanceof?Error);
          }
          所以,如果你想在出現(xiàn)了非RuntimeException或者Error時也回滾,請指定回滾時的異常,例如:
          @Transactional(rollbackFor?=?Exception.class)
          第二種情況:「想提交的時候被標記成只能回滾了(rollback only)」
          對應(yīng)的異常信息如下:
          Transaction?rolled?back?because?it?has?been?marked?as?rollback-only
          我們先來看個例子吧
          @Service
          public?class?DmzService?{

          ?@Autowired
          ?IndexService?indexService;

          ?@Transactional
          ?public?void?testRollbackOnly()?{
          ??try?{
          ???indexService.a();
          ??}?catch?(ClassNotFoundException?e)?{
          ???System.out.println("catch");
          ??}
          ?}
          }

          @Service
          public?class?IndexService?{
          ?@Transactional(rollbackFor?=?Exception.class)
          ?public?void?a()?throws?ClassNotFoundException{
          ??//?......
          ??throw?new?ClassNotFoundException();
          ?}
          }
          在上面這個例子中,DmzServicetestRollbackOnly方法跟IndexServicea方法都開啟了事務(wù),并且事務(wù)的傳播級別為required,所以當我們在testRollbackOnly中調(diào)用IndexServicea方法時這兩個方法應(yīng)當是共用的一個事務(wù)。按照這種思路,雖然IndexServicea方法拋出了異常,但是我們在testRollbackOnly將異常捕獲了,那么這個事務(wù)應(yīng)該是可以正常提交的,為什么會拋出異常呢?
          如果你看過我之前的源碼分析的文章應(yīng)該知道,在處理回滾時有這么一段代碼
          rollBackOnly設(shè)置
          在提交時又做了下面這個判斷(這個方法我刪掉了一些不重要的代碼
          commit_rollbackOnly
          可以看到當提交時發(fā)現(xiàn)事務(wù)已經(jīng)被標記為rollbackOnly后會進入回滾處理中,并且unexpected傳入的為true。在處理回滾時又有下面這段代碼
          拋出異常
          最后在這里拋出了這個異常。
          ?以上代碼均位于AbstractPlatformTransactionManager?
          總結(jié)起來,「主要的原因就是因為內(nèi)部事務(wù)回滾時將整個大事務(wù)做了一個rollbackOnly的標記」,所以即使我們在外部事務(wù)中catch了拋出的異常,整個事務(wù)仍然無法正常提交,并且如果你希望正常提交,Spring還會拋出一個異常。
          「解決方案」:
          這個解決方案要依賴業(yè)務(wù)而定,你要明確你想要的結(jié)果是什么
          1. 內(nèi)部事務(wù)發(fā)生異常,外部事務(wù)catch異常后,內(nèi)部事務(wù)自行回滾,不影響外部事務(wù)
          ?將內(nèi)部事務(wù)的傳播級別設(shè)置為nested/requires_new均可。在我們的例子中就是做如下修改:
          //?@Transactional(rollbackFor?=?Exception.class,propagation?=?Propagation.REQUIRES_NEW)
          @Transactional(rollbackFor?=?Exception.class,propagation?=?Propagation.NESTED)
          public?void?a()?throws?ClassNotFoundException{
          ???//?......
          ???throw?new?ClassNotFoundException();
          }
          ?
          雖然這兩者都能得到上面的結(jié)果,但是它們之間還是有不同的。當傳播級別為requires_new時,兩個事務(wù)完全沒有聯(lián)系,各自都有自己的事務(wù)管理機制(開啟事務(wù)、關(guān)閉事務(wù)、回滾事務(wù))。但是傳播級別為nested時,實際上只存在一個事務(wù),只是在調(diào)用a方法時設(shè)置了一個保存點,當a方法回滾時,實際上是回滾到保存點上,并且當外部事務(wù)提交時,內(nèi)部事務(wù)才會提交,外部事務(wù)如果回滾,內(nèi)部事務(wù)會跟著回滾。
          1. 內(nèi)部事務(wù)發(fā)生異常時,外部事務(wù)catch異常后,內(nèi)外兩個事務(wù)都回滾,但是方法不拋出異常
          @Transactional
          public?void?testRollbackOnly()?{
          ???try?{
          ??????indexService.a();
          ???}?catch?(ClassNotFoundException?e)?{
          ??????//?加上這句代碼
          ??????TransactionInterceptor.currentTransactionStatus().setRollbackOnly();
          ???}
          }
          ?
          通過顯示的設(shè)置事務(wù)的狀態(tài)為RollbackOnly。這樣當提交事務(wù)時會進入下面這段代碼
          顯示回滾
          最大的區(qū)別在于處理回滾時第二個參數(shù)傳入的是false,這意味著回滾是回滾是預(yù)期之中的,所以在處理完回滾后并不會拋出異常。

          ?

          讀寫分離跟事務(wù)結(jié)合使用時的問題

          讀寫分離一般有兩種實現(xiàn)方式
          1. 配置多數(shù)據(jù)源
          2. 依賴中間件,如MyCat
          如果是配置了多數(shù)據(jù)源的方式實現(xiàn)了讀寫分離,那么需要注意的是:「如果開啟了一個讀寫事務(wù),那么必須使用寫節(jié)點」,「如果是一個只讀事務(wù),那么可以使用讀節(jié)點」
          如果是依賴于MyCat等中間件那么需要注意:「只要開啟了事務(wù),事務(wù)內(nèi)的SQL都會使用寫節(jié)點(依賴于具體中間件的實現(xiàn),也有可能會允許使用讀節(jié)點,具體策略需要自行跟DB團隊確認)」
          基于上面的結(jié)論,我們在使用事務(wù)時應(yīng)該更加謹慎,在沒有必要開啟事務(wù)時盡量不要開啟。
          ?一般我們會在配置文件配置某些約定的方法名字前綴開啟不同的事務(wù)(或者不開啟),但現(xiàn)在隨著注解事務(wù)的流行,好多開發(fā)人員(或者架構(gòu)師)搭建框架的時候在service類上加上了@Transactional注解,導(dǎo)致整個類都是開啟事務(wù)的,這樣嚴重影響數(shù)據(jù)庫執(zhí)行的效率,更重要的是開發(fā)人員不重視、或者不知道在查詢類的方法上面自己加上@Transactional(propagation=Propagation.NOT_SUPPORTED)就會導(dǎo)致,所有的查詢方法實際并沒有走從庫,導(dǎo)致主庫壓力過大。?
          其次,關(guān)于如果沒有對只讀事務(wù)做優(yōu)化的話(優(yōu)化意味著將只讀事務(wù)路由到讀節(jié)點),那么@Transactional注解中的readOnly屬性就應(yīng)該要慎用。我們使用readOnly的原本目的是為了將事務(wù)標記為只讀,這樣當MySQL服務(wù)端檢測到是一個只讀事務(wù)后就可以做優(yōu)化,少分配一些資源(例如:只讀事務(wù)不需要回滾,所以不需要分配undo log段)。但是當配置了讀寫分離后,可能會可能會導(dǎo)致只讀事務(wù)內(nèi)所有的SQL都被路由到了主庫,讀寫分離也就失去了意義。

          總結(jié)

          本文為事務(wù)專欄最后一篇啦!這篇文章主要是總結(jié)了工作中事務(wù)相關(guān)的常見問題,想讓大家少走點彎路!希望大家可以認真讀完哦,有什么問題可以直接在后臺私信我或者加我微信!
          這篇文章也是整個Spring系列的最后一篇文章,之后可能會出一篇源碼閱讀心得,跟大家聊聊如何學(xué)習源碼。
          另外今年也給自己定了個小目標,就是完成SSM框架源碼的閱讀。目前來說Spring是完成,接下來就是SpringMVC跟MyBatis。
          在分析MyBatis前,會從JDBC源碼出發(fā),然后就是MyBatis對配置的解析、MyBatis執(zhí)行流程、MyBatis的緩存、MyBatis的事務(wù)管理以及MyBatis的插件機制。
          在學(xué)習SpringMVC前,會從TomCat出發(fā),先講清楚TomCat的原理,我們再來看SpringMVC。整個來說相比于Spring源碼,我覺得應(yīng)該不算特別難。

          有道無術(shù),術(shù)可成;有術(shù)無道,止于術(shù)

          歡迎大家關(guān)注Java之道公眾號


          好文章,我在看??

          瀏覽 53
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  777精品久无码人妻蜜桃 | 婷婷色色五月天 | 色色网的五月天 | 涩涩涩涩av | 黄色天堂av |