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


前言

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

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

事務失效

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

數據庫層面

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

alter table table_name engine=myisam;

這樣就會出現“事務失效”的問題了

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

業務代碼層面

業務層面的代碼是否有問題,這就有很多種可能了

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

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

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

image-20200818152357704

代碼位於: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方法時,我們預期的執行流程是這樣的

事務失效(自調用requires_new)

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

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

事務失效(自調用requires_new)執行流程

由於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屬性。

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

image-20200818195112983

以上代碼位於: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,或者掃描下方二維碼,跟着我一起認認真真學Java,踏踏實實做一個coder。

公眾號

我叫DMZ,一個在學習路上匍匐前行的小菜鳥!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM