事務傳播行為種類
Spring在TransactionDefinition接口中規定了7種類型的事務傳播行為,
它們規定了事務方法和事務方法發生嵌套調用時事務如何進行傳播:
事務傳播行為類型 |
說明 |
PROPAGATION_REQUIRED |
如果當前沒有事務,就新建一個事務,如果已經存在一個事務中,加入到這個事務中。這是最常見的選擇。 |
PROPAGATION_SUPPORTS |
支持當前事務,如果當前沒有事務,就以非事務方式執行。 |
PROPAGATION_MANDATORY |
使用當前的事務,如果當前沒有事務,就拋出異常。 |
PROPAGATION_REQUIRES_NEW |
新建事務,如果當前存在事務,把當前事務掛起。 |
PROPAGATION_NOT_SUPPORTED |
以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。 |
PROPAGATION_NEVER |
以非事務方式執行,如果當前存在事務,則拋出異常。 |
PROPAGATION_NESTED |
如果當前存在事務,則在嵌套事務內執行。如果當前沒有事務,則執行與PROPAGATION_REQUIRED類似的操作。 |
為什么需要嵌套事務?
我們知道,數據庫事務是為了保證數據庫操作原子性而設計的一種解決辦法。例如執行兩條 update 當第二條執行失敗時候順便將前面執行的那條一起回滾。
這種應用場景比較常見,例如銀行轉帳。A賬戶減少的錢要加到B賬戶上。這兩個SQL操作只要有一個失敗,必須一起撤銷。
但是通常銀行轉帳業務無論是否操作成功都會忘數據庫里加入系統日志。如果日志輸出與賬戶金額調整在一個事務里,一旦事務回滾日志也會跟著一起消失。這時候就需要嵌套事務。
時間 | 事務 |
T1 | 開始事務 |
T2 | 記錄日志... |
T3 | 轉賬500元 |
T4 | 記錄日志... |
T5 | 遞交事務 |
為什么有了嵌套事務還需要獨立事務?
假設現在銀行需要知道當前正在進行轉賬的實時交易數。
我們知道一個完整的轉賬業務會記錄兩次日志,第一次用以記錄是什么業務,第二次會記錄這個業務總共耗時。因此完成這個功能時我們只需要查詢還未進行第二次記錄的那些交易日志即可得出結果。
時間 | 事務1 | 事務2 |
T1 | 開始事務 | |
T2 | 記錄日志... | |
T3 | 開始子事務 | |
T4 | 轉賬500元 | |
T5 | 遞交子事務 | |
T6 | 記錄日志... | |
T7 | 遞交事務 |
分析一下上面這種嵌套事務就知道不會得出正確的結果,首先第一條日志會被錄入數據庫的先決條件是轉賬操作成功之后的遞交事務。
如果事務遞交了,交易也就完成了。這樣得出的查詢結果根本不是實時數據。因此嵌套事務解決方案不能滿足需求。倘若日志輸出操作使用的是一個全新的事務,就會保證可以查詢到正確的數據。(如下)。
時間 | 事務1 | 事務2 |
T1 | 開始事務 | 開始事務 |
T2 | 記錄日志... | |
T3 | 遞交事務 | |
T4 | 轉賬500元 | |
T5 | 開始事務 | |
T6 | 記錄日志... | |
T7 | 遞交事務 | 遞交事務 |
Spring 提供的幾種事務控制
1.PROPAGATION_REQUIRED(加入已有事務)
嘗試加入已經存在的事務中,如果沒有則開啟一個新的事務。
2.RROPAGATION_REQUIRES_NEW(獨立事務)
掛起當前存在的事務,并開啟一個全新的事務,新事務與已存在的事務之間彼此沒有關系。
3.PROPAGATION_NESTED(嵌套事務)
在當前事務上開啟一個子事務(Savepoint),如果遞交主事務。那么連同子事務一同遞交。如果遞交子事務則保存點之前的所有事務都會被遞交。
4.PROPAGATION_SUPPORTS(跟隨環境)
是指 Spring 容器中如果當前沒有事務存在,就以非事務方式執行;如果有,就使用當前事務。
5.PROPAGATION_NOT_SUPPORTED(非事務方式)
是指如果存在事務則將這個事務掛起,并使用新的數據庫連接。新的數據庫連接不使用事務。
6.PROPAGATION_NEVER(排除事務)
當存在事務時拋出異常,否則就已非事務方式運行。
7.PROPAGATION_MANDATORY(需要事務)
如果不存在事務就拋出異常,否則就已事務方式運行。
事務管理器API接口
對于開發者而言,對事務管理器的操作只會涉及到“get”、“commit”、“rollback”三個基本操作。因此數據庫事務管理器的接口相對簡單。如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
/** * 數據源的事務管理器。 * @version : 2013-10-30 * @author 趙永春(zyc@hasor.net) */ public interface TransactionManager { //開啟事務,使用不同的傳播屬性來創建事務。 public TransactionStatus getTransaction(TransactionBehavior behavior); //遞交事務 public void commit(TransactionStatus status) throws SQLException; //回滾事務 public void rollBack(TransactionStatus status) throws SQLException; } |
取得的事務狀態使用下面這個接口進行封裝:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
/** * 表示一個事務狀態 * @version : 2013-10-30 * @author 趙永春(zyc@hasor.net) */ public interface TransactionStatus { //獲取事務使用的傳播行為 public TransactionBehavior getTransactionBehavior(); //獲取事務的隔離級別 public TransactionLevel getIsolationLevel(); // //事務是否已經完成,當事務已經遞交或者被回滾就標志著已完成 public boolean isCompleted(); //是否已被標記為回滾,如果返回值為 true 則在commit 時會回滾該事務 public boolean isRollbackOnly(); //是否為只讀模式。 public boolean isReadOnly(); //是否使用了一個全新的數據庫連接開啟事務 public boolean isNewConnection(); //測試該事務是否被掛起 public boolean isSuspend(); //表示事務是否攜帶了一個保存點,嵌套事務通常會創建一個保存點作為嵌套事務與上一層事務的分界點。 //注意:如果事務中包含保存點,則在遞交事務時只處理這個保存點。 public boolean hasSavepoint(); // //設置事務狀態為回滾,作為替代拋出異常進而觸發回滾操作。 public void setRollbackOnly(); //設置事務狀態為只讀。 public void setReadOnly(); } |
除此之外還需要聲明一個枚舉用以確定事務傳播屬性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
/** * 事務傳播屬性 * @version : 2013-10-30 * @author 趙永春(zyc@hasor.net) */ public enum TransactionBehavior { // //加入已有事務,嘗試加入已經存在的事務中,如果沒有則開啟一個新的事務。 PROPAGATION_REQUIRED, // //獨立事務,掛起當前存在的事務,并開啟一個全新的事務,新事務與已存在的事務之間彼此沒有關系。 RROPAGATION_REQUIRES_NEW, // //嵌套事務,在當前事務中開啟一個子事務。如果事務遞交將連同上一級事務一同遞交。 PROPAGATION_NESTED, // //跟隨環境,如果當前沒有事務存在,就以非事務方式執行;如果有,就使用當前事務。 PROPAGATION_SUPPORTS, // //非事務方式,如果當前沒有事務存在,就以非事務方式執行;如果有,就將當前事務掛起。 PROPAGATION_NOT_SUPPORTED, // //排除事務,如果當前沒有事務存在,就以非事務方式執行;如果有,就拋出異常。 PROPAGATION_NEVER, // //強制要求事務,如果當前沒有事務存在,就拋出異常;如果有,就使用當前事務。 PROPAGATION_MANDATORY, } |
約定條件
在實現類似 Spring 那樣的事務控制之前需要做幾個約定:
- 1、每條線程只可以擁有一個活動的數據庫連接,稱之為“當前連接”。
- 2、程序在執行期間如持有數據庫連接,需要使用“引用計數”標記。
- 3、一個事務狀態中最多只能存在一個子事務(Savepoint)。
- 4、當前的數據庫連接是可以被隨時更換的,即使它的“引用計數不為0”。
- 5、數據庫連接具備“事務狀態”。
下面就講講為什么要先有這些約定:
一、為什么要有當前連接?
一般數據庫事務操作遵循(開啟事務 -> 操作 -> 關閉事務)三個步驟,這三個步驟可以看作是固定的。你不能隨意調換它們的順序。在多線程下如果數據庫連接共享,將會打破這個順序。因為極有可能線程 A 將線程 B 的事務一起遞交了。
所以為了減少不必要的麻煩我們使用“當前連接”來存放數據庫連接,并且約定當前連接是與當前線程綁定的。也就是說您在線程A下啟動的數據庫事務,是不會影響到線程B下的數據庫事務。它們之間使用的數據庫連接彼此互不干預。
二、為什么需要引用計數?
引用計數是被用來確定當前數據庫連接是否可以被 close。當引用計數器收到“減法”操作時候如果計數器為零或者小于零,則認為應用程序已經不在使用這個連接,可以放心 close。
三、為什么一個事務狀態中只能存在一個子事務?
答:子事務與父事務會被封裝到不同的兩個事務狀態中。因此事務管理器從設計上就不允許一個事務狀態持有兩個事務特征,這樣會讓系統設計變得復雜。
四、當前的數據庫連接是可以被隨時更換的,即使它的“引用計數不為0”
我們知道,隨意更換當前連接有可能會引發數據庫連接釋放錯誤。但是依然需要這個風險的操作是由于“獨立事務”的要求。
在獨立事務中如果當前連接已經存在事務,則會新建一個數據庫連接作為當前連接并開啟它的事務。
獨立事務的設計是為了保證,處于事務控制中的應用程序對數據庫操作是不會有其它代碼影響到它。并且它也不會影響到別人,故此稱之為“獨立”。
此外在前面提到的場景“為什么有了嵌套事務還需要獨立事務?”也已經解釋獨立事務存在的必要性。
五、數據庫連接具備“事務狀態”
事務管理器在創建事務對象時,需要知道當前數據連接是否已經具有事務狀態。
如果尚未開啟事務,事務管理器可以認為這個連接是一個新的(new狀態),此時在事務管理器收到 commit 請求時,具有new狀態時可以放心大膽的去處理事務遞交操作。
倘若存在事務,則很有可能在事務管理器創建事務對象之前已經對數據庫進行了操作。基于這種情況下事務管理器就不能冒昧的進行 commit 或者 rollback。
因此事務狀態是可以用來決定事務管理器是否真實的去執行 commit 和 rollback 方法。有時候這個狀態也被稱之為“new”狀態。
數據庫連接可能存在的情況
無論是否存在事務管理器,當前數據庫連接都會具有一些固定的狀態。那么下面就先分析一下當前數據庫連接可能存在的情況有哪些?
- 當前連接已經有程序使用(引用計數 !=0)
- 當前連接尚未有程序使用(引用計數 ==0)
- 當前連接已經開啟了事務(autoCommit 值為 false)
- 當前連接尚未開啟事務(autoCommit 值為 true)
上面雖然列出了四種情況,但是實際上可以看作兩個狀態值。
- 1. 引用計數是否為0,表示是否可以關閉連接
- 2. autoCommit是否為false(表示當前連接是否具有事務狀態)
引用計數為0,表示的是沒有任何程序在執行時需要或者正在使用這個連接。也就是說這個數據庫連接的存在與否根本不重要。
autoCommit這個狀態是來自于 Connection 接口,它表示的含義是數據庫連接是否支持自動遞交。如果為 true 表示Connection 在每次執行一條 sql 語句時都會跟隨一個 commit 遞交操作。如果執行失敗,自然就相當于 rollback。因此可以看出這個值的情況反映出當前數據庫連接的事務狀態。
- 1.有事務,引用大于0
- 2.有事務,引用等于0
- 3.沒事務,引用大于0
- 4.沒事務,引用等于0
理解“new”狀態
new狀態是用來標記當事務管理器創建新的事務狀態時,當前連接的事務狀態是如何的。并且輔助事務管理器決定究竟如何處理事務遞交&回滾操作。
上面這條定義準確的定義了 new 狀態的作用,以及如何獲取。那么我們要看看它究竟會決定哪些事情?
根據定義,new 狀態是用來輔助事務遞交與回滾操作。我們先假設下面這個場景:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public static void main(){ DataSource ds= ......; Connection conn = DataSourceUtil.getConnection(ds); //取得數據庫連接,會導致引用計數+1 conn.setAutoCommit( false ); //開啟事務 conn.execute( "update ..." ); //預先執行的 update 語句 TransactionStatus status = tm.getTransaction(PROPAGATION_REQUIRED); //加入到已有事務,引用計數+1 insertData(); //執行數據庫插入 tm.commit(status); //引用計數-1 conn.commit(); //遞交事務 DataSourceUtil.releaseConnection(conn,ds); //釋放連接,引用計數-1 } public static void insertData(){ jdbc.execute( "insert into ..." ); //執行插入語句,在執行過程中引用計數會 +1,然后在-1 } |
在上面這個場景中,在調用 insertData 方法之前使用 REQUIRED(加入已有事務) 行為創建了一個事務。
從邏輯上來講 insertData 方法雖然在完成之后會進行事務遞交操作,但是由于它的事務已經加入到了更外層的事務中。因此這個事務遞交應該是被忽略的,最終的遞交應當是由 conn.commit() 代碼進行。
我們分析一下在這個場景下 new 狀態是怎樣的。
我們不難發現在 getTransaction 方法之前,應用程序實際上已經持有了數據庫連接(引用計數+1),而隨后它又關閉了自動遞交,開啟了事務。這樣一來,就不滿足 new 狀態的特征。
最后在 tm.commit(status) 時候,事務管理器會參照 new 狀態。如果為 false 則不觸發遞交事務的操作。這恰恰保護了上面這個代碼邏輯的正常運行。
現在我們修改上面的代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public static void main(){ DataSource ds= ......; Connection conn = DataSourceUtil.getConnection(ds); //取得數據庫連接,會導致引用計數+1 conn.execute( "update ..." ); //預先執行的 update 語句 TransactionStatus status = tm.getTransaction(PROPAGATION_REQUIRED); //加入到已有事務,引用計數+1 insertData(); //執行數據庫插入 tm.commit(status); //引用計數-1 DataSourceUtil.releaseConnection(conn,ds); //釋放連接,引用計數-1 } public static void insertData(){ jdbc.execute( "insert into ..." ); //執行插入語句,在執行過程中引用計數會 +1,然后在-1 } |
我們發現,原本在申請連接之后的開啟事務代碼和釋放連接之前的事務遞交代碼被刪除了。也就是說在 getTransaction 時候數據庫連接是滿足 new 狀態的特征的。
程序中雖然在第四行有一條 SQL 執行語句,但是由于 Connection 在執行這個 SQL語句的時候使用的是自動遞交事務。因此在 insertData 之后即使出現 rollback 也不會影響到它。
最后在 tm.commit(status) 時候,事務管理器參照 new 狀態。為 true 觸發了交事務的操作。這也恰恰滿足了上面這個代碼邏輯的正常運行。
@黃勇 這里也有一篇文章簡介事務控制 http://my.oschina.net/huangyong/blog/160012 他在文章中詳細說述說了,事務隔離級別。這篇文章正好是本文作為基礎部分的一個重要補充。在這里非常感謝 勇哥的貢獻。
相關博文:
- 脫離 Spring 實現復雜嵌套事務,之一(必要的概念)
- 脫離 Spring 實現復雜嵌套事務,之二(PROPAGATION_REQUIRED - 加入已有事務)
- 脫離 Spring 實現復雜嵌套事務,之三(RROPAGATION_REQUIRES_NEW - 獨立事務)
- 脫離 Spring 實現復雜嵌套事務,之四(PROPAGATION_NESTED - 嵌套事務)
- 脫離 Spring 實現復雜嵌套事務,之五(PROPAGATION_SUPPORTS - 跟隨環境)
- 脫離 Spring 實現復雜嵌套事務,之六(PROPAGATION_NOT_SUPPORTED - 非事務方式)
- 脫離 Spring 實現復雜嵌套事務,之七(PROPAGATION_NEVER - 排除事務)
- 脫離 Spring 實現復雜嵌套事務,之八(PROPAGATION_MANDATORY - 要求存在事務)
- 脫離 Spring 實現復雜嵌套事務,之九(整合七種傳播行為)
- 脫離 Spring 實現復雜嵌套事務,之十(實現篇)
文章列表