https://zhuanlan.zhihu.com/p/148504094
一、什麼是事務的傳播?
簡單的理解就是多個事務方法相互呼叫時,事務如何在這些方法間傳播。
舉個例子,方法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.NESTED)
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.NESTED)
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,歡迎關注,裡面會分享更多有用知識,還有我的私密照片