<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>

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

          共 7068字,需瀏覽 15分鐘

           ·

          2020-09-10 18:40

          前言

          這篇文章我們來聊一聊平常工作時使用事務(wù)可能出現(xiàn)的一些問題(本文主要針對使用@Transactional進(jìn)行事務(wù)管理的方式進(jìn)行討論)以及對應(yīng)的解決方案

          1. 事務(wù)失效
          2. 事務(wù)回滾相關(guān)問題
          3. 讀寫分離跟事務(wù)結(jié)合使用時的問題

          事務(wù)失效

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

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

          數(shù)據(jù)庫層面,數(shù)據(jù)庫使用的存儲引擎是否支持事務(wù)?默認(rèn)情況下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進(jìn)行管理(添加@Service注解)

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

          代碼位于:AbstractFallbackTransactionAttributeSource#computeTransactionAttribute

          ?

          也就是說,默認(rèn)情況下你無法使用@Transactional對一個非public的方法進(jìn)行事務(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ù)的實(shí)現(xiàn)是依賴于AOP的,當(dāng)容器在創(chuàng)建dmzService這個Bean時,發(fā)現(xiàn)這個類中存在了被@Transactional標(biāo)注的方法(修飾符為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)異?;貪L事務(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這三個方法模擬代理類實(shí)現(xiàn)的邏輯。因?yàn)槟繕?biāo)類DmzService中的saveAsaveB方法上存在@Transactional注解,所以會對這兩個方法進(jìn)行攔截并嵌入事務(wù)管理的邏輯,同時saveAB方法上沒有@Transactional,相當(dāng)于代理類直接調(diào)用了目標(biāo)類中的方法。

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

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

          常見的自調(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);
          ?}
          }

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

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

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

          由于saveB方法實(shí)際上是由dmzService也就是目標(biāo)類自己調(diào)用的,所以在saveB方法的前后并不會執(zhí)行事務(wù)的相關(guān)操作。這也是自調(diào)用帶來問題的根本原因:「自調(diào)用時,調(diào)用的是目標(biā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)雅

          2. 利用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代表將代理類放入到線程上下文中,默認(rèn)是false
            @EnableAspectJAutoProxy(exposeProxy?=?true)
            ?

            個人比較喜歡的是第二種方式

          這里我們做個來做個小總結(jié)

          總結(jié)

          一圖勝千言

          事務(wù)失效的原因

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

          回滾相關(guān)的問題可以被總結(jié)為兩句話

          1. 想回滾的時候事務(wù)卻提交了
          2. 想提交的時候被標(biāo)記成只能回滾了(rollback only)

          先看第一種情況:「想回滾的時候事務(wù)卻提交了」。這種情況往往是程序員對Spring中事務(wù)的rollbackFor屬性不夠了解導(dǎo)致的。

          ?

          Spring默認(rèn)拋出了未檢查unchecked異常(繼承自 RuntimeException 的異常)或者 Error才回滾事務(wù);其他異常不會觸發(fā)回滾事務(wù),已經(jīng)執(zhí)行的SQL會提交掉。如果在事務(wù)中拋出其他類型的異常,但卻期望 Spring 能夠回滾事務(wù),就需要指定rollbackFor屬性。

          ?

          對應(yīng)代碼其實(shí)我們上篇文章也分析過了,如下:

          回滾代碼
          ?

          以上代碼位于:TransactionAspectSupport#completeTransactionAfterThrowing方法中

          ?

          默認(rèn)情況下,只有出現(xiàn)RuntimeException或者Error才會回滾

          public?boolean?rollbackOn(Throwable?ex)?{
          ????return?(ex?instanceof?RuntimeException?||?ex?instanceof?Error);
          }

          所以,如果你想在出現(xiàn)了非RuntimeException或者Error時也回滾,請指定回滾時的異常,例如:

          @Transactional(rollbackFor?=?Exception.class)

          第二種情況:「想提交的時候被標(biāo)記成只能回滾了(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,所以當(dāng)我們在testRollbackOnly中調(diào)用IndexServicea方法時這兩個方法應(yīng)當(dāng)是共用的一個事務(wù)。按照這種思路,雖然IndexServicea方法拋出了異常,但是我們在testRollbackOnly將異常捕獲了,那么這個事務(wù)應(yīng)該是可以正常提交的,為什么會拋出異常呢?

          如果你看過我之前的源碼分析的文章應(yīng)該知道,在處理回滾時有這么一段代碼

          rollBackOnly設(shè)置

          在提交時又做了下面這個判斷(這個方法我刪掉了一些不重要的代碼

          commit_rollbackOnly

          可以看到當(dāng)提交時發(fā)現(xiàn)事務(wù)已經(jīng)被標(biāo)記為rollbackOnly后會進(jìn)入回滾處理中,并且unexpected傳入的為true。在處理回滾時又有下面這段代碼

          拋出異常

          最后在這里拋出了這個異常。

          ?

          以上代碼均位于AbstractPlatformTransactionManager

          ?

          總結(jié)起來,「主要的原因就是因?yàn)閮?nèi)部事務(wù)回滾時將整個大事務(wù)做了一個rollbackOnly的標(biāo)記」,所以即使我們在外部事務(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é)果,但是它們之間還是有不同的。當(dāng)傳播級別為requires_new時,兩個事務(wù)完全沒有聯(lián)系,各自都有自己的事務(wù)管理機(jī)制(開啟事務(wù)、關(guān)閉事務(wù)、回滾事務(wù))。但是傳播級別為nested時,實(shí)際上只存在一個事務(wù),只是在調(diào)用a方法時設(shè)置了一個保存點(diǎn),當(dāng)a方法回滾時,實(shí)際上是回滾到保存點(diǎn)上,并且當(dāng)外部事務(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。這樣當(dāng)提交事務(wù)時會進(jìn)入下面這段代碼

          顯示回滾

          最大的區(qū)別在于處理回滾時第二個參數(shù)傳入的是false,這意味著回滾是回滾是預(yù)期之中的,所以在處理完回滾后并不會拋出異常。

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

          讀寫分離一般有兩種實(shí)現(xiàn)方式

          1. 配置多數(shù)據(jù)源
          2. 依賴中間件,如MyCat

          如果是配置了多數(shù)據(jù)源的方式實(shí)現(xiàn)了讀寫分離,那么需要注意的是:「如果開啟了一個讀寫事務(wù),那么必須使用寫節(jié)點(diǎn)」,「如果是一個只讀事務(wù),那么可以使用讀節(jié)點(diǎn)」

          如果是依賴于MyCat等中間件那么需要注意:「只要開啟了事務(wù),事務(wù)內(nèi)的SQL都會使用寫節(jié)點(diǎn)(依賴于具體中間件的實(shí)現(xiàn),也有可能會允許使用讀節(jié)點(diǎn),具體策略需要自行跟DB團(tuán)隊確認(rèn))」

          基于上面的結(jié)論,我們在使用事務(wù)時應(yīng)該更加謹(jǐn)慎,在沒有必要開啟事務(wù)時盡量不要開啟。

          ?

          一般我們會在配置文件配置某些約定的方法名字前綴開啟不同的事務(wù)(或者不開啟),但現(xiàn)在隨著注解事務(wù)的流行,好多開發(fā)人員(或者架構(gòu)師)搭建框架的時候在service類上加上了@Transactional注解,導(dǎo)致整個類都是開啟事務(wù)的,這樣嚴(yán)重影響數(shù)據(jù)庫執(zhí)行的效率,更重要的是開發(fā)人員不重視、或者不知道在查詢類的方法上面自己加上@Transactional(propagation=Propagation.NOT_SUPPORTED)就會導(dǎo)致,所有的查詢方法實(shí)際并沒有走從庫,導(dǎo)致主庫壓力過大。

          ?

          其次,關(guān)于如果沒有對只讀事務(wù)做優(yōu)化的話(優(yōu)化意味著將只讀事務(wù)路由到讀節(jié)點(diǎn)),那么@Transactional注解中的readOnly屬性就應(yīng)該要慎用。我們使用readOnly的原本目的是為了將事務(wù)標(biāo)記為只讀,這樣當(dāng)MySQL服務(wù)端檢測到是一個只讀事務(wù)后就可以做優(yōu)化,少分配一些資源(例如:只讀事務(wù)不需要回滾,所以不需要分配undo log段)。但是當(dāng)配置了讀寫分離后,可能會可能會導(dǎo)致只讀事務(wù)內(nèi)所有的SQL都被路由到了主庫,讀寫分離也就失去了意義。

          總結(jié)

          這篇文章主要是總結(jié)了工作中事務(wù)相關(guān)的常見問題,想讓大家少走點(diǎn)彎路!希望大家可以認(rèn)真讀完哦!

          后記

          我整理的4本PDF文檔,公眾號“后端技術(shù)進(jìn)階”后臺回復(fù)“面試突擊”即可免費(fèi)獲取。

          文章有幫助可以點(diǎn)個「在看」或「分享」,都是支持,我都喜歡!

          我是Guide哥,Java后端開發(fā),會一點(diǎn)前端知識,喜歡烹飪,自由的少年。一個三觀比主角還正的技術(shù)人。我們下期再見!

          瀏覽 30
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <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>
                  国产精品成人7777777 | 欧美成人午夜视频 | 亚洲成人影视在线播放 | 一本大道HEYZO无码专区 一道本一区二区三区免费视频 | 欧美中文日韩 |