帶你讀懂Spring 事務——事務的傳播機制(藏)

不瞭解事務的先看上一篇:帶你讀懂Spring 事務——認識事務

一、什麼是事務的傳播?

簡單的理解就是多個事務方法相互調用時,事務如何在這些方法間傳播。

舉個栗子,方法A是一個事務的方法,方法A執行過程中調用了方法B,那麼方法B有無事務以及方法B對事務的要求不同都會對方法A的事務具體執行造成影響,同時方法A的事務對方法B的事務執行也有影響,這種影響具體是什麼就由兩個方法所定義的事務傳播類型所決定。

二、Spring事務傳播類型枚舉Propagation介紹

在Spring中對於事務的傳播行爲定義了七種類型分別是:REQUIRED、SUPPORTS、MANDATORY、REQUIRES_NEW、NOT_SUPPORTED、NEVER、NESTED

在Spring源碼中這七種類型被定義爲了枚舉。源碼在org.springframework.transaction.annotation包下的Propagation,源碼中註釋很多,對傳播行爲的七種類型的不同含義都有解釋,後文中錘子我也會給大家分析,我在這裏就不貼所有的源碼,只把這個類上的註解貼一下,翻譯一下就是:表示與TransactionDefinition接口相對應的用於@Transactional註解的事務傳播行爲的枚舉。

也就是說枚舉類Propagation是爲了結合@Transactional註解使用而設計的,這個枚舉裏面定義的事務傳播行爲類型與TransactionDefinition中定義的事務傳播行爲類型是對應的,所以在使用@Transactional註解時我們就要使用Propagation枚舉類來指定傳播行爲類型,而不直接使用TransactionDefinition接口裏定義的屬性。

在TransactionDefinition接口中定義了Spring事務的一些屬性,不僅包括事務傳播特性類型,還包括了事務的隔離級別類型(事務的隔離級別後面文章會詳細講解),更多詳細信息,大家可以打開源碼自己翻譯一下里面的註釋


package org.springframework.transaction.annotation;

import org.springframework.transaction.TransactionDefinition;

/**
 * Enumeration that represents transaction propagation behaviors for use
 * with the {@link Transactional} annotation, corresponding to the
 * {@link TransactionDefinition} interface.
 *
 * @author Colin Sampaleanu
 * @author Juergen Hoeller
 * @since 1.2
 */
public enum Propagation {
	...
}

三、七種事務傳播行爲詳解與示例

在介紹七種事務傳播行爲前,我們先設計一個場景,幫助大家理解,場景描述如下

現有兩個方法A和B,方法A執行會在數據庫ATable插入一條數據,方法B執行會在數據庫BTable插入一條數據,僞代碼如下:

//將傳入參數a存入ATable
pubilc void A(a){
	insertIntoATable(a);	
}
//將傳入參數b存入BTable
public void B(b){
	insertIntoBTable(b);
}

接下來,我們看看在如下場景下,沒有事務,情況會怎樣


public void testMain(){
	A(a1);	//調用A入參a1
	testB();	//調用testB
}

public void testB(){
	B(b1);	//調用B入參b1
	throw Exception;	 //發生異常拋出
	B(b2);	//調用B入參b2
}

在這裏要做一個重要提示:Spring中事務的默認實現使用的是AOP,也就是代理的方式,如果大家在使用代碼測試時,同一個Service類中的方法相互調用需要使用注入的對象來調用,不要直接使用this.方法名來調用,this.方法名調用是對象內部方法調用,不會通過Spring代理,也就是事務不會起作用

以上僞代碼描述的一個場景,方法testMain和testB都沒有事務,執行testMain方法,那麼結果會怎麼樣呢?

相信大家都知道了,就是a1數據成功存入ATable表,b1數據成功存入BTable表,而在拋出異常後b2數據存儲就不會執行,也就是b2數據不會存入數據庫,這就是沒有事務的場景。

可想而知,在上一篇文章(認識事務)中舉例的轉賬操作,如果在某一步發生異常,且沒有事務,那麼錢是不是就憑空消失了,所以事務在數據庫操作中的重要性可想而知。接下我們就開始理解七種不同事務傳播類型的含義

REQUIRED(Spring默認的事務傳播類型)

如果當前沒有事務,則自己新建一個事務,如果當前存在事務,則加入這個事務

源碼說明如下:


	/**
	 * Support a current transaction, create a new one if none exists.
	 * Analogous to EJB transaction attribute of the same name.
	 * <p>This is the default setting of a transaction annotation.
	 */
	REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),

*(示例1)*根據場景舉栗子,我們在testMain和testB上聲明事務,設置傳播行爲REQUIRED,僞代碼如下:


@Transactional(propagation = Propagation.REQUIRED)
public void testMain(){
	A(a1);	//調用A入參a1
	testB();	//調用testB
}
@Transactional(propagation = Propagation.REQUIRED)
public void testB(){
	B(b1);	//調用B入參b1
	throw Exception;	 //發生異常拋出
	B(b2);	//調用B入參b2
}

該場景下執行testMain方法結果如何呢?

數據庫沒有插入新的數據,數據庫還是保持着執行testMain方法之前的狀態,沒有發生改變。testMain上聲明瞭事務,在執行testB方法時就加入了testMain的事務(當前存在事務,則加入這個事務),在執行testB方法拋出異常後事務會發生回滾,又testMain和testB使用的同一個事務,所以事務回滾後testMain和testB中的操作都會回滾,也就使得數據庫仍然保持初始狀態

*(示例2)*根據場景再舉一個栗子,我們只在testB上聲明事務,設置傳播行爲REQUIRED,僞代碼如下:


public void testMain(){
	A(a1);	//調用A入參a1
	testB();	//調用testB
}
@Transactional(propagation = Propagation.REQUIRED)
public void testB(){
	B(b1);	//調用B入參b1
	throw Exception;	 //發生異常拋出
	B(b2);	//調用B入參b2
}

這時的執行結果又如何呢?

數據a1存儲成功,數據b1和b2沒有存儲。由於testMain沒有聲明事務,testB有聲明事務且傳播行爲是REQUIRED,所以在執行testB時會自己新建一個事務(如果當前沒有事務,則自己新建一個事務),testB拋出異常則只有testB中的操作發生了回滾,也就是b1的存儲會發生回滾,但a1數據不會回滾,所以最終a1數據存儲成功,b1和b2數據沒有存儲

SUPPORTS

當前存在事務,則加入當前事務,如果當前沒有事務,就以非事務方法執行

源碼註釋如下(太長省略了一部分),其中裏面有一個提醒翻譯一下就是:“對於具有事務同步的事務管理器,SUPPORTS與完全沒有事務稍有不同,因爲它定義了可能應用同步的事務範圍”。這個是與事務同步管理器相關的一個注意項,這裏不過多討論。


	/**
	 * Support a current transaction, execute non-transactionally if none exists.
	 * Analogous to EJB transaction attribute of the same name.
	 * <p>Note: For transaction managers with transaction synchronization,
	 * {@code SUPPORTS} is slightly different from no transaction at all,
	 * as it defines a transaction scope that synchronization will apply for.
	 ...
	 */
	SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),

*(示例3)*根據場景舉栗子,我們只在testB上聲明事務,設置傳播行爲SUPPORTS,僞代碼如下:


public void testMain(){
	A(a1);	//調用A入參a1
	testB();	//調用testB
}
@Transactional(propagation = Propagation.SUPPORTS)
public void testB(){
	B(b1);	//調用B入參b1
	throw Exception;	 //發生異常拋出
	B(b2);	//調用B入參b2
}

這種情況下,執行testMain的最終結果就是,a1,b1存入數據庫,b2沒有存入數據庫。由於testMain沒有聲明事務,且testB的事務傳播行爲是SUPPORTS,所以執行testB時就是沒有事務的(如果當前沒有事務,就以非事務方法執行),則在testB拋出異常時也不會發生回滾,所以最終結果就是a1和b1存儲成功,b2沒有存儲。

那麼當我們在testMain上聲明事務且使用REQUIRED傳播方式的時候,這個時候執行testB就滿足當前存在事務,則加入當前事務,在testB拋出異常時事務就會回滾,最終結果就是a1,b1和b2都不會存儲到數據庫

MANDATORY

當前存在事務,則加入當前事務,如果當前事務不存在,則拋出異常。

源碼註釋如下:


	/**
	 * Support a current transaction, throw an exception if none exists.
	 * Analogous to EJB transaction attribute of the same name.
	 */
	MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),

*(示例4)*場景舉栗子,我們只在testB上聲明事務,設置傳播行爲MANDATORY,僞代碼如下:


public void testMain(){
	A(a1);	//調用A入參a1
	testB();	//調用testB
}
@Transactional(propagation = Propagation.MANDATORY)
public void testB(){
	B(b1);	//調用B入參b1
	throw Exception;	 //發生異常拋出
	B(b2);	//調用B入參b2
}

這種情形的執行結果就是a1存儲成功,而b1和b2沒有存儲。b1和b2沒有存儲,並不是事務回滾的原因,而是因爲testMain方法沒有聲明事務,在去執行testB方法時就直接拋出事務要求的異常(如果當前事務不存在,則拋出異常),所以testB方法裏的內容就沒有執行。

那麼如果在testMain方法進行事務聲明,並且設置爲REQUIRED,則執行testB時就會使用testMain已經開啓的事務,遇到異常就正常的回滾了。

REQUIRES_NEW

創建一個新事務,如果存在當前事務,則掛起該事務。

可以理解爲設置事務傳播類型爲REQUIRES_NEW的方法,在執行時,不論當前是否存在事務,總是會新建一個事務。

源碼註釋如下


	/**
	 * Create a new transaction, and suspend the current transaction if one exists.
	 ...
	 */
	REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),

*(示例5)*場景舉栗子,爲了說明設置REQUIRES_NEW的方法會開啓新事務,我們把異常發生的位置換到了testMain,然後給testMain聲明事務,傳播類型設置爲REQUIRED,testB也聲明事務,設置傳播類型爲REQUIRES_NEW,僞代碼如下

@Transactional(propagation = Propagation.REQUIRED)
public void testMain(){
	A(a1);	//調用A入參a1
	testB();	//調用testB
	throw Exception;	 //發生異常拋出
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void testB(){
	B(b1);	//調用B入參b1
	B(b2);	//調用B入參b2
}

這種情形的執行結果就是a1沒有存儲,而b1和b2存儲成功,因爲testB的事務傳播設置爲REQUIRES_NEW,所以在執行testB時會開啓一個新的事務,testMain中發生的異常時在testMain所開啓的事務中,所以這個異常不會影響testB的事務提交,testMain中的事務會發生回滾,所以最終a1就沒有存儲,而b1和b2就存儲成功了。

與這個場景對比的一個場景就是testMain和testB都設置爲REQUIRED,那麼上面的代碼執行結果就是所有數據都不會存儲,因爲testMain和testMain是在同一個事務下的,所以事務發生回滾時,所有的數據都會回滾

NOT_SUPPORTED

始終以非事務方式執行,如果當前存在事務,則掛起當前事務

可以理解爲設置事務傳播類型爲NOT_SUPPORTED的方法,在執行時,不論當前是否存在事務,都會以非事務的方式運行。

源碼說明如下


/**
	 * Execute non-transactionally, suspend the current transaction if one exists.
	 ...
	 */
	NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),

*(示例6)*場景舉栗子,testMain傳播類型設置爲REQUIRED,testB傳播類型設置爲NOT_SUPPORTED,且異常拋出位置在testB中,僞代碼如下

@Transactional(propagation = Propagation.REQUIRED)
public void testMain(){
	A(a1);	//調用A入參a1
	testB();	//調用testB
}
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void testB(){
	B(b1);	//調用B入參b1
	throw Exception;	 //發生異常拋出
	B(b2);	//調用B入參b2
}

該場景的執行結果就是a1和b2沒有存儲,而b1存儲成功。testMain有事務,而testB不使用事務,所以執行中testB的存儲b1成功,然後拋出異常,此時testMain檢測到異常事務發生回滾,但是由於testB不在事務中,所以只有testMain的存儲a1發生了回滾,最終只有b1存儲成功,而a1和b1都沒有存儲

NEVER

不使用事務,如果當前事務存在,則拋出異常

很容易理解,就是我這個方法不使用事務,並且調用我的方法也不允許有事務,如果調用我的方法有事務則我直接拋出異常。

源碼註釋如下:


	/**
	 * Execute non-transactionally, throw an exception if a transaction exists.
	 * Analogous to EJB transaction attribute of the same name.
	 */
	NEVER(TransactionDefinition.PROPAGATION_NEVER),

*(示例7)*場景舉栗子,testMain設置傳播類型爲REQUIRED,testB傳播類型設置爲NEVER,並且把testB中的拋出異常代碼去掉,則僞代碼如下

@Transactional(propagation = Propagation.REQUIRED)
public void testMain(){
	A(a1);	//調用A入參a1
	testB();	//調用testB
}
@Transactional(propagation = Propagation.NEVER)
public void testB(){
	B(b1);	//調用B入參b1
	B(b2);	//調用B入參b2
}

該場景執行,直接拋出事務異常,且不會有數據存儲到數據庫。由於testMain事務傳播類型爲REQUIRED,所以testMain是運行在事務中,而testB事務傳播類型爲NEVER,所以testB不會執行而是直接拋出事務異常,此時testMain檢測到異常就發生了回滾,所以最終數據庫不會有數據存入。

NESTED

如果當前事務存在,則在嵌套事務中執行,否則REQUIRED的操作一樣(開啓一個事務)

這裏需要注意兩點:

  • 和REQUIRES_NEW的區別

REQUIRES_NEW是新建一個事務並且新開啓的這個事務與原有事務無關,而NESTED則是當前存在事務時(我們把當前事務稱之爲父事務)會開啓一個嵌套事務(稱之爲一個子事務)。
在NESTED情況下父事務回滾時,子事務也會回滾,而在REQUIRES_NEW情況下,原有事務回滾,不會影響新開啓的事務。

  • 和REQUIRED的區別

REQUIRED情況下,調用方存在事務時,則被調用方和調用方使用同一事務,那麼被調用方出現異常時,由於共用一個事務,所以無論調用方是否catch其異常,事務都會回滾
而在NESTED情況下,被調用方發生異常時,調用方可以catch其異常,這樣只有子事務回滾,父事務不受影響

*(示例8)*場景舉栗子,testMain設置爲REQUIRED,testB設置爲NESTED,且異常發生在testMain中,僞代碼如下

@Transactional(propagation = Propagation.REQUIRED)
public void testMain(){
	A(a1);	//調用A入參a1
	testB();	//調用testB
	throw Exception;	 //發生異常拋出
}
@Transactional(propagation = Propagation.NEVER)
public void testB(){
	B(b1);	//調用B入參b1
	B(b2);	//調用B入參b2
}

該場景下,所有數據都不會存入數據庫,因爲在testMain發生異常時,父事務回滾則子事務也跟着回滾了,可以與*(示例5)*比較看一下,就找出了與REQUIRES_NEW的不同

*(示例9)*場景舉栗子,testMain設置爲REQUIRED,testB設置爲NESTED,且異常發生在testB中,僞代碼如下

@Transactional(propagation = Propagation.REQUIRED)
public void testMain(){
	A(a1);	//調用A入參a1
	try{
		testB();	//調用testB
	}catch(Exception e){
		
	}
	A(a2);
}
@Transactional(propagation = Propagation.NEVER)
public void testB(){
	B(b1);	//調用B入參b1
	throw Exception;	 //發生異常拋出
	B(b2);	//調用B入參b2
}

這種場景下,結果是a1,a2存儲成功,b1和b2存儲失敗,因爲調用方catch了被調方的異常,所以只有子事務回滾了。

同樣的代碼,如果我們把testB的傳播類型改爲REQUIRED,結果也就變成了:沒有數據存儲成功。就算在調用方catch了異常,整個事務還是會回滾,因爲,調用方和被調方共用的同一個事務

文章歡迎轉載,轉載請註明出處,個人公衆號【愛做夢的錘子】,全網同id,個站 http://te-amo.site,歡迎關注,裏面會分享更多有用知識,還有我的私密照片

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章