一、Propagation (事務的傳播屬性)
Propagation-- key屬性確定代理應該給哪個方法增加事務行為。這樣的屬性最重要的部份是傳播行為。有以下選項可供使用:
PROPAGATION_REQUIRED--支持當前事務,如果當前沒有事務,就一個事務。是最常見的選擇。
PROPAGATION_SUPPORTS--支持當前事務,如果當前沒有事務,就以非事務方式執行。
PROPAGATION_MANDATORY--支持當前事務,如果當前沒有事務,就拋出異常。
PROPAGATION_REQUIRES_NEW--新建事務,如果當前存在事務,把當前事務掛起,執行當前新建事務完成以后,上下文事務恢復再執行。
PROPAGATION_NOT_SUPPORTED--以非事務方式執行操作,如果當前存在事務,就把當前事務掛起,執行當前邏輯,結束后恢復上下文的事務。
PROPAGATION_NEVER--以非事務方式執行,如果當前存在事務,則拋出異常。
1: PROPAGATION_REQUIRED
加入當前正要執行的事務不在另外一個事務里,那么就起一個新的事務
比如說,ServiceB.methodB的事務級別定義為PROPAGATION_REQUIRED, 那么由于執行ServiceA.methodA的時候,
ServiceA.methodA已經起了事務,這時調用ServiceB.methodB,ServiceB.methodB看到自己已經運行在ServiceA.methodA
的事務內部,就不再起新的事務。而假如ServiceA.methodA運行的時候發現自己沒有在事務中,他就會為自己分配一個事務。
這樣,在ServiceA.methodA或者在ServiceB.methodB內的任何地方出現異常,事務都會被回滾。即使ServiceB.methodB的事務已經被
提交,但是ServiceA.methodA在接下來fail要回滾,ServiceB.methodB也要回滾
2: PROPAGATION_SUPPORTS
如果當前在事務中,即以事務的形式運行,如果當前不再一個事務中,那么就以非事務的形式運行
3: PROPAGATION_MANDATORY
必須在一個事務中運行。也就是說,他只能被一個父事務調用。否則,他就要拋出異常
4: PROPAGATION_REQUIRES_NEW 執行當前新建事務完成以后,上下文事務恢復再執行。
這個就比較繞口了。 比如我們設計ServiceA.methodA的事務級別為PROPAGATION_REQUIRED,ServiceB.methodB的事務級別為PROPAGATION_REQUIRES_NEW,
那么當執行到ServiceB.methodB的時候,ServiceA.methodA所在的事務就會掛起,ServiceB.methodB會起一個新的事務,等待ServiceB.methodB的事務完成以后,
他才繼續執行。他與PROPAGATION_REQUIRED 的事務區別在于事務的回滾程度了。因為ServiceB.methodB是新起一個事務,那么就是存在
兩個不同的事務。如果ServiceB.methodB已經提交,那么ServiceA.methodA失敗回滾,ServiceB.methodB是不會回滾的。如果ServiceB.methodB失敗回滾,
如果他拋出的異常被ServiceA.methodA捕獲,ServiceA.methodA事務仍然可能提交。
5: PROPAGATION_NOT_SUPPORTED
當前不支持事務。比如ServiceA.methodA的事務級別是PROPAGATION_REQUIRED ,而ServiceB.methodB的事務級別是PROPAGATION_NOT_SUPPORTED ,
那么當執行到ServiceB.methodB時,ServiceA.methodA的事務掛起,而他以非事務的狀態運行完,再繼續ServiceA.methodA的事務。
6: PROPAGATION_NEVER
不能在事務中運行。假設ServiceA.methodA的事務級別是PROPAGATION_REQUIRED, 而ServiceB.methodB的事務級別是PROPAGATION_NEVER ,
那么ServiceB.methodB就要拋出異常了。
7: PROPAGATION_NESTED
理解Nested的關鍵是savepoint。他與PROPAGATION_REQUIRES_NEW的區別是,PROPAGATION_REQUIRES_NEW另起一個事務,將會與他的父事務相互獨立,
而Nested的事務和他的父事務是相依的,他的提交是要等和他的父事務一塊提交的。也就是說,如果父事務最后回滾,他也要回滾的。
而Nested事務的好處是他有一個savepoint。
ServiceA {
/**
- 事務屬性配置為 PROPAGATION_REQUIRED
*/
void methodA() {
try {
//savepoint
ServiceB.methodB(); //PROPAGATION_NESTED 級別
} catch (SomeException) {
// 執行其他業務, 如 ServiceC.methodC();
}
}
}
也就是說ServiceB.methodB失敗回滾,那么ServiceA.methodA也會回滾到savepoint點上,ServiceA.methodA可以選擇另外一個分支,比如ServiceC.methodC,繼續執行,來嘗試完成自己的事務。
但是這個事務并沒有在EJB標準中定義。
那么什么是嵌套事務呢?很多人都不理解,我看過一些博客,都是有些理解偏差。
嵌套是子事務套在父事務中執行,子事務是父事務的一部分,在進入子事務之前,父事務建立一個回滾點,叫save point,然后執行子事務,這個子事務的執行也算是父事務的一部分,然后子事務執行結束,父事務繼續執行。重點就在于那個save point。看幾個問題就明了了:
1)如果子事務回滾,會發生什么?
父事務會回滾到進入子事務前建立的save point,然后嘗試其他的事務或者其他的業務邏輯,父事務之前的操作不會受到影響,更不會自動回滾。
2)如果父事務回滾,會發生什么?
父事務回滾,子事務也會跟著回滾!為什么呢,因為父事務結束之前,子事務是不會提交的,我們說子事務是父事務的一部分,正是這個道理。那么:
3)事務的提交,是什么情況?
是父事務先提交,然后子事務提交,還是子事務先提交,父事務再提交?答案是第二種情況,還是那句話,子事務是父事務的一部分,由父事務統一提交。
Spring事務的隔離級別
ISOLATION_DEFAULT: 這是一個PlatfromTransactionManager默認的隔離級別,使用數據庫默認的事務隔離級別.另外四個與JDBC的隔離級別相對應
ISOLATION_READ_UNCOMMITTED: 這是事務最低的隔離級別,它充許令外一個事務可以看到這個事務未提交的數據。這種隔離級別會產生臟讀,不可重復讀和幻像讀。
ISOLATION_READ_COMMITTED: 保證一個事務修改的數據提交后才能被另外一個事務讀取。另外一個事務不能讀取該事務未提交的數據
ISOLATION_REPEATABLE_READ: 這種事務隔離級別可以防止臟讀,不可重復讀。但是可能出現幻像讀。它除了保證一個事務不能讀取另一個事務未提交的數據外,還保證了避免下面的情況產生(不可重復讀)。
ISOLATION_SERIALIZABLE 這是花費最高代價但是最可靠的事務隔離級別。事務被處理為順序執行。除了防止臟讀,不可重復讀外,還避免了幻像讀。
數據庫并發操作存在的異常情況:
更新丟失(Lost update): 兩個事務都同時更新一行數據但是第二個事務卻中途失敗退出導致對數據兩個修改都失效了這是系統沒有執 行任何鎖操作因此并發事務并沒有被隔離開來。
臟讀取(Dirty Reads): 一個事務開始讀取 了某行數據但是另外一個事務已經更新了此數據但沒有能夠及時提交。這是相當危險很可能所有操作都被回滾。
不可重復讀取(Non-repeatable Reads): 一 個事務對同一行數據重復讀取兩次但是卻得到了不同結果。例如在兩次讀取中途有另外一個事務對該行數據進行了修改并提交。
兩次更新問題(Second lost updates problem): 無法重復讀取特例,有兩個并發事務同時讀取同一行數據然后其中一個對它進行修改提交而另一個也進行了修改提交這就會造成 第一次寫操作失效。
-
幻讀(Phantom Reads): 也稱為幻像(幻影)。事務在操作過程中進行兩次查詢,第二次查詢結果包含了第一次查詢中未出現的數據(這里并不要求兩次查詢SQL語句相同)這是因為在兩次查詢過程中有 另外一個事務插入數據造成的。
為了避免上面出現幾種情況在標準SQL規范中定義了4個事務隔離級別,不同隔離級別對事務處理不同 。
1.未授權讀取(Read Uncommitted): 也稱 未提交讀。允許臟讀取但不允許更新丟失,如果一個事務已經開始寫數據則另外一個數據則不允許同時進行寫操作但允許其他事務讀此行數據。該隔離級別可以通過 “排他寫鎖”實現。事務隔離的最低級別,僅可保證不讀取物理損壞的數據。與READ COMMITTED 隔離級相反,它允許讀取已經被其它用戶修改但尚未提交確定的數據。
授權讀取(Read Committed): 也稱提交 讀。允許不可重復讀取但不允許臟讀取。這可以通過“瞬間共享讀鎖”和“排他寫鎖”實現,讀取數據的事務允許其他事務繼續訪問該行數據,但是未提交寫事務將 會禁止其他事務訪問該行。SQL Server 默認的級別。在此隔離級下,SELECT 命令不會返回尚未提交(Committed) 的數據,也不能返回臟數據。
可重復讀取(Repeatable Read): 禁止 不可重復讀取和臟讀取。但是有時可能出現幻影數據,這可以通過“共享讀鎖”和“排他寫鎖”實現,讀取數據事務將會禁止寫事務(但允許讀事務),寫事務則禁 止任何其他事務。在此隔離級下,用SELECT 命令讀取的數據在整個命令執行過程中不會被更改。此選項會影響系統的效能,非必要情況最好不用此隔離級。
串行(Serializable): 也稱可串行讀。提 供嚴格的事務隔離,它要求事務序列化執行,事務只能一個接著一個地執行,但不能并發執行。如果僅僅通過“行級鎖”是無法實現事務序列化的,必須通過其他機 制保證新插入的數據不會被剛執行查詢操作事務訪問到。事務隔離的最高級別,事務之間完全隔離。如果事務在可串行讀隔離級別上運行,則可以保證任何并發重疊 事務均是串行的。
隔離級別 更新丟失 臟讀取 重復讀取 幻讀
未授權讀取 N Y Y Y
授權讀取 N N Y Y
可重復 讀取 N N N Y
串行 N N N N
所以最安全的,是Serializable,但是伴隨而來也是高昂的性能開銷。
另外,事務常用的兩個屬性:readonly和timeout
一個是設置事務為只讀以提升性能。
另一個是設置事務的超時時間,一般用于防止大事務的發生。還是那句話,事務要盡可能的小!
最后引入一個問題:
一個邏輯操作需要檢查的條件有20條,能否為了減小事務而將檢查性的內容放到事務之外呢?
很多系統都是在DAO的內部開始啟動事務,然后進行操作,最后提交或者回滾。這其中涉及到代碼設計的問題。小一些的系統可以采用這種方式來做,但是在一些比較大的系統,
邏輯較為復雜的系統中,勢必會將過多的業務邏輯嵌入到DAO中,導致DAO的復用性下降。所以這不是一個好的實踐。
來回答這個問題:能否為了縮小事務,而將一些業務邏輯檢查放到事務外面?答案是:對于核心的業務檢查邏輯,不能放到事務之外,而且必須要作為分布式下的并發控制!
一旦在事務之外做檢查,那么勢必會造成事務A已經檢查過的數據被事務B所修改,導致事務A徒勞無功而且出現并發問題,直接導致業務控制失敗。
所以,在分布式的高并發環境下,對于核心業務邏輯的檢查,要采用加鎖機制。
比如事務開啟需要讀取一條數據進行驗證,然后邏輯操作中需要對這條數據進行修改,最后提交。
這樣的一個過程,如果讀取并驗證的代碼放到事務之外,那么讀取的數據極有可能已經被其他的事務修改,當前事務一旦提交,又會重新覆蓋掉其他事務的數據,導致數據異常。
所以在進入當前事務的時候,必須要將這條數據鎖住,使用for update就是一個很好的在分布式環境下的控制手段。
一種好的實踐方式是使用編程式事務而非生命式,尤其是在較為規模的項目中。對于事務的配置,在代碼量非常大的情況下,將是一種折磨,而且人肉的方式,絕對不能避免這種問題。
將DAO保持針對一張表的最基本操作,然后業務邏輯的處理放入manager和service中進行,同時使用編程式事務更精確的控制事務范圍。
特別注意的,對于事務內部一些可能拋出異常的情況,捕獲要謹慎,不能隨便的catch Exception 導致事務的異常被吃掉而不能正常回滾。