一個@Transaction哪裏來這麼多坑?

點擊藍色「程序員DMZ 」關注我喲程序員

好看記得加個「星標」哈!web

前言

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

  1. 事務失效
  2. 事務回滾相關問題
  3. 讀寫分離跟事務結合使用時的問題

事務失效

事務失效咱們通常要從兩個方面排查問題數據庫

數據庫層面

數據庫層面,數據庫使用的存儲引擎是否支持事務?默認狀況下MySQL數據庫使用的是Innodb存儲引擎(5.5版本以後),它是支持事務的,可是若是你的表特意修改了存儲引擎,例如,你經過下面的語句修改了表使用的存儲引擎爲MyISAM,而MyISAM又是不支持事務的緩存

alter table table_name engine=myisam;

這樣就會出現「事務失效」的問題了微信

「解決方案」:修改存儲引擎爲Innodb架構

業務代碼層面

業務層面的代碼是否有問題,這就有不少種可能了app

  1. 咱們要使用Spring的聲明式事務,那麼須要執行事務的Bean是否已經交由了Spring管理?在代碼中的體現就是類上是否有 @ServiceComponent等一系列註解

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

  1. @Transactional註解是否被放在了合適的位置。在上篇文章中咱們對Spring中事務失效的原理作了詳細的分析,其中也分析了Spring內部是如何解析 @Transactional註解的,咱們稍微回顧下代碼:
註解解析

代碼位於:AbstractFallbackTransactionAttributeSource#computeTransactionAttribute編輯器

也就是說,默認狀況下你沒法使用@Transactional對一個非public的方法進行事務管理

「解決方案」:修改須要事務管理的方法爲public

  1. 出現了自調用。什麼是自調用呢?咱們看個例子
@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方法中調用了本類中的saveAsaveB方法,這就是自調用。在上面的例子中saveAsaveB上的事務會失效

那麼自調用爲何會致使事務失效呢?咱們知道Spring中事務的實現是依賴於AOP的,當容器在建立dmzService這個Bean時,發現這個類中存在了被@Transactional標註的方法(修飾符爲public)那麼就須要爲這個類建立一個代理對象並放入到容器中,建立的代理對象等價於下面這個類

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 {
            // 開啓事務
            startTransaction();
            dmzService.saveA(a);
        } catch (Exception e) {
            // 出現異常回滾事務
            rollbackTransaction();
        }
        // 提交事務
        commitTransaction();
    }

    public void saveB(B b) {
        try {
            // 開啓事務
            startTransaction();
            dmzService.saveB(b);
        } catch (Exception e) {
            // 出現異常回滾事務
            rollbackTransaction();
        }
        // 提交事務
        commitTransaction();
    }
}

上面是一段僞代碼,經過startTransactionrollbackTransactioncommitTransaction這三個方法模擬代理類實現的邏輯。由於目標類DmzService中的saveAsaveB方法上存在@Transactional註解,因此會對這兩個方法進行攔截並嵌入事務管理的邏輯,同時saveAB方法上沒有@Transactional,至關於代理類直接調用了目標類中的方法。

咱們會發現當經過代理類調用saveAB時整個方法的調用鏈以下:

實際上咱們在調用saveAsaveB時調用的是目標類中的方法,這種清空下,事務固然會失效。

常見的自調用致使的事務失效還有一個例子,以下:

@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);
 }
}

當咱們調用save方法時,咱們預期的執行流程是這樣的

也就是說兩個事務之間互不干擾,每一個事務都有本身的開啓、回滾、提交操做。

但根據以前的分析咱們知道,實際上在調用saveB方法時,是直接調用的目標類中的saveB方法,在saveB方法先後並不會有事務的開啓或者提交、回滾等操做,實際的流程是下面這樣的

因爲saveB方法其實是由dmzService也就是目標類本身調用的,因此在saveB方法的先後並不會執行事務的相關操做。這也是自調用帶來問題的根本緣由:「自調用時,調用的是目標類中的方法而不是代理類中的方法」

「解決方案」

  1. 本身注入本身,而後顯示的調用,例如:

    @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);
     }
    }

    這種方案看起來不是很優雅

  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表明將代理類放入到線程上下文中,默認是false
    @EnableAspectJAutoProxy(exposeProxy = true)

    我的比較喜歡的是第二種方式

這裏咱們作個來作個小總結

總結

一圖勝千言

事務失效的緣由

事務回滾相關問題

回滾相關的問題能夠被總結爲兩句話

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

先看第一種狀況:「想回滾的時候事務卻提交了」。這種狀況每每是程序員對Spring中事務的rollbackFor屬性不夠了解致使的。

Spring默認拋出了未檢查unchecked異常(繼承自 RuntimeException 的異常)或者 Error纔回滾事務;其餘異常不會觸發回滾事務,已經執行的SQL會提交掉。若是在事務中拋出其餘類型的異常,但卻指望 Spring 可以回滾事務,就須要指定rollbackFor屬性。

對應代碼其實咱們上篇文章也分析過了,以下:

回滾代碼

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

默認狀況下,只有出現RuntimeException或者Error纔會回滾

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

因此,若是你想在出現了非RuntimeException或者Error時也回滾,請指定回滾時的異常,例如:

@Transactional(rollbackFor = Exception.class)

第二種狀況:「想提交的時候被標記成只能回滾了(rollback only)」

對應的異常信息以下:

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方法都開啓了事務,而且事務的傳播級別爲required,因此當咱們在testRollbackOnly中調用IndexServicea方法時這兩個方法應當是共用的一個事務。按照這種思路,雖然IndexServicea方法拋出了異常,可是咱們在testRollbackOnly將異常捕獲了,那麼這個事務應該是能夠正常提交的,爲何會拋出異常呢?

若是你看過我以前的源碼分析的文章應該知道,在處理回滾時有這麼一段代碼

rollBackOnly設置

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

commit_rollbackOnly

能夠看到當提交時發現事務已經被標記爲rollbackOnly後會進入回滾處理中,而且unexpected傳入的爲true。在處理回滾時又有下面這段代碼

拋出異常

最後在這裏拋出了這個異常。

以上代碼均位於AbstractPlatformTransactionManager

總結起來,「主要的緣由就是由於內部事務回滾時將整個大事務作了一個rollbackOnly的標記」,因此即便咱們在外部事務中catch了拋出的異常,整個事務仍然沒法正常提交,而且若是你但願正常提交,Spring還會拋出一個異常。

「解決方案」:

這個解決方案要依賴業務而定,你要明確你想要的結果是什麼

  1. 內部事務發生異常,外部事務catch異常後,內部事務自行回滾,不影響外部事務

將內部事務的傳播級別設置爲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();
}

雖然這二者都能獲得上面的結果,可是它們之間仍是有不一樣的。當傳播級別爲requires_new時,兩個事務徹底沒有聯繫,各自都有本身的事務管理機制(開啓事務、關閉事務、回滾事務)。可是傳播級別爲nested時,實際上只存在一個事務,只是在調用a方法時設置了一個保存點,當a方法回滾時,其實是回滾到保存點上,而且當外部事務提交時,內部事務纔會提交,外部事務若是回滾,內部事務會跟着回滾。

  1. 內部事務發生異常時,外部事務catch異常後,內外兩個事務都回滾,可是方法不拋出異常
@Transactional
public void testRollbackOnly() {
   try {
      indexService.a();
   } catch (ClassNotFoundException e) {
      // 加上這句代碼
      TransactionInterceptor.currentTransactionStatus().setRollbackOnly();
   }
}

經過顯示的設置事務的狀態爲RollbackOnly。這樣當提交事務時會進入下面這段代碼

顯示回滾

最大的區別在於處理回滾時第二個參數傳入的是false,這意味着回滾是回滾是預期之中的,因此在處理完回滾後並不會拋出異常。

讀寫分離跟事務結合使用時的問題

讀寫分離通常有兩種實現方式

  1. 配置多數據源
  2. 依賴中間件,如 MyCat

若是是配置了多數據源的方式實現了讀寫分離,那麼須要注意的是:「若是開啓了一個讀寫事務,那麼必須使用寫節點」「若是是一個只讀事務,那麼可使用讀節點」

若是是依賴於MyCat等中間件那麼須要注意:「只要開啓了事務,事務內的SQL都會使用寫節點(依賴於具體中間件的實現,也有可能會容許使用讀節點,具體策略須要自行跟DB團隊確認)」

基於上面的結論,咱們在使用事務時應該更加謹慎,在沒有必要開啓事務時儘可能不要開啓。

通常咱們會在配置文件配置某些約定的方法名字前綴開啓不一樣的事務(或者不開啓),但如今隨着註解事務的流行,好多開發人員(或者架構師)搭建框架的時候在service類上加上了@Transactional註解,致使整個類都是開啓事務的,這樣嚴重影響數據庫執行的效率,更重要的是開發人員不重視、或者不知道在查詢類的方法上面本身加上@Transactional(propagation=Propagation.NOT_SUPPORTED)就會致使,全部的查詢方法實際並無走從庫,致使主庫壓力過大。

其次,關於若是沒有對只讀事務作優化的話(優化意味着將只讀事務路由到讀節點),那麼@Transactional註解中的readOnly屬性就應該要慎用。咱們使用readOnly的本來目的是爲了將事務標記爲只讀,這樣當MySQL服務端檢測到是一個只讀事務後就能夠作優化,少分配一些資源(例如:只讀事務不須要回滾,因此不須要分配undo log段)。可是當配置了讀寫分離後,可能會可能會致使只讀事務內全部的SQL都被路由到了主庫,讀寫分離也就失去了意義。

總結

本文爲事務專欄最後一篇啦!這篇文章主要是總結了工做中事務相關的常見問題,想讓你們少走點彎路!但願你們能夠認真讀完哦,有什麼問題能夠直接在後臺私信我或者加我微信!

這篇文章也是整個Spring系列的最後一篇文章,以後可能會出一篇源碼閱讀心得,跟你們聊聊如何學習源碼。

另外今年也給本身定了個小目標,就是完成SSM框架源碼的閱讀。目前來講Spring是完成,接下來就是SpringMVC跟MyBatis。

在分析MyBatis前,會從JDBC源碼出發,而後就是MyBatis對配置的解析、MyBatis執行流程、MyBatis的緩存、MyBatis的事務管理以及MyBatis的插件機制。

在學習SpringMVC前,會從TomCat出發,先講清楚TomCat的原理,咱們再來看SpringMVC。整個來講相比於Spring源碼,我以爲應該不算特別難。

但願在這個過程當中能夠跟你們一塊兒進步!!!

我叫DMZ,一個陪你一塊兒慢慢進步的小菜鳥~!

往期精選


Spring事務源碼分析專題(一)JdbcTemplate使用及源碼分析

Spring事務源碼分析專題(二)Mybatis的使用及跟Spring整合原理分析

Spring事務專題(三)事務的基本概念,Mysql事務處理原理

Spring事務專題(四)Spring中事務使用、抽象機制及模擬Spring事務實現

本文分享自微信公衆號 - 程序員DMZ(programerDmz)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索