1. 程式人生 > >Spring宣告式事務管理及事務巢狀

Spring宣告式事務管理及事務巢狀

一>  事務配置

Spring動態代理的一個重要特徵是,它是針對介面的,所以我們的dao要通過動態代理來讓spring接管事務,就必須在dao前面抽象出一個介面,當然如果沒有這樣的介面,那麼spring會使用CGLIB來解決問題。

    一般地,使用Spring框架時,可在其applicationContext.xml檔案中宣告其對hibernate事務的使用:

上述配置是針對Biz字尾的所有介面類中宣告的方法進行了事務配置,其事務的傳播策略為PROPAGATION_REQUIRED,在在 spring 中一共定義了六種事務傳播屬性:

PROPAGATION_REQUIRED -- 支援當前事務,如果當前沒有事務,就新建一個事務。這是最常見的選擇。
PROPAGATION_SUPPORTS -- 支援當前事務,如果當前沒有事務,就以非事務方式執行。
PROPAGATION_MANDATORY -- 支援當前事務,如果當前沒有事務,就丟擲異常。
PROPAGATION_REQUIRES_NEW -- 新建事務,如果當前存在事務,把當前事務掛起。
PROPAGATION_NOT_SUPPORTED -- 以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。
PROPAGATION_NEVER -- 以非事務方式執行,如果當前存在事務,則丟擲異常。
PROPAGATION_NESTED -- 如果當前存在事務,則在巢狀事務內執行。如果當前沒有事務,則進行與PROPAGATION_REQUIRED類似的操作。


前六個策略類似於EJB CMT,第七個(PROPAGATION_NESTED)是Spring所提供的一個特殊變數。
它要求事務管理器或者使用JDBC 3.0 Savepoint API提供巢狀事務行為(如Spring的DataSourceTransactionManager)。

二>  事務巢狀

在我所見過的誤解中, 最常見的是下面這種:

引用
假如有兩個業務介面 ServiceA 和 ServiceB, 其中 ServiceA 中有一個方法實現如下


那麼如果 ServiceB 的 methodB  如果配置了事務, 就必須配置為 PROPAGATION_NESTED



這種想法可能害了不少人, 認為 Service 之間應該避免互相呼叫, 其實根本不用擔心這點,PROPAGATION_REQUIRED 已經說得很明白,
如果當前執行緒中已經存在事務, 方法呼叫會加入此事務, 如果當前沒有事務,就新建一個事務, 所以 ServiceB#methodB() 的事務只要遵循最普通的規則配置為 PROPAGATION_REQUIRED 即可, 如果 ServiceB#methodB (我們稱之為內部事務, 為下文打下基礎) 拋了異常, 那麼 ServiceA#methodA(我們稱之為外部事務) 如果沒有特殊配置此異常時事務提交 (即 +MyCheckedException的用法), 那麼整個事務是一定要 rollback 的, 什麼 Service 只能調 Dao 之類的言論純屬無稽之談, spring 只負責配置了事務屬性方法的攔截, 它怎麼知道你這個方法是在 Service 還是 Dao 裡 ?

也就是說, 最容易弄混淆的其實是 PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED, 那麼這兩種方式又有何區別呢? 我簡單的翻譯一下 Juergen Hoeller 的話 :
   
    PROPAGATION_REQUIRES_NEW 啟動一個新的, 不依賴於環境的 "內部" 事務. 這個事務將被完全 commited 或 rolled back 而不依賴於外部事務, 它擁有自己的隔離範圍, 自己的鎖, 等等. 當內部事務開始執行時, 外部事務將被掛起, 內務事務結束時, 外部事務將繼續執行.


    另一方面, PROPAGATION_NESTED 開始一個 "巢狀的" 事務,  它是已經存在事務的一個真正的子事務. 潛套事務開始執行時,  它將取得一個 savepoint. 如果這個巢狀事務失敗, 我們將回滾到此 savepoint. 巢狀事務是外部事務的一部分, 只有外部事務結束後它才會被提交.

    由此可見, PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED 的最大區別在於, PROPAGATION_REQUIRES_NEW 完全是一個新的事務, 而 PROPAGATION_NESTED 則是外部事務的子事務, 如果外部事務 commit, 潛套事務也會被 commit, 這個規則同樣適用於 roll back.
   

   
    那麼外部事務如何利用巢狀事務的 savepoint 特性呢, 我們用程式碼來說話

這種情況下, 因為 ServiceB#methodB 的事務屬性為 PROPAGATION_REQUIRES_NEW, 所以兩者不會發生任何關係, ServiceA#methodA 和 ServiceB#methodB 不會因為對方的執行情況而影響事務的結果, 因為它們根本就是兩個事務, 在 ServiceB#methodB 執行時 ServiceA#methodA 的事務已經掛起了 (關於事務掛起的內容已經超出了本文的討論範圍, 有時間我會再寫一些掛起的文章) .

那麼 PROPAGATION_NESTED 又是怎麼回事呢? 繼續看程式碼

這種方式也是巢狀事務最有價值的地方, 它起到了分支執行的效果, 如果 ServiceB.methodB 失敗, 那麼執行 ServiceC.methodC(), 而 ServiceB.methodB 已經回滾到它執行之前的 SavePoint, 所以不會產生髒資料(相當於此方法從未執行過), 這種特性可以用在某些特殊的業務中, 而 PROPAGATION_REQUIRED 和 PROPAGATION_REQUIRES_NEW 都沒有辦法做到這一點. (題外話 : 看到這種程式碼, 似乎似曾相識, 想起了 prototype.js 中的 Try 函式 )

2. 程式碼不做任何修改, 那麼如果內部事務(即 ServiceB#methodB) rollback, 那麼首先 ServiceB.methodB 回滾到它執行之前的 SavePoint(在任何情況下都會如此),
   外部事務(即 ServiceA#methodA) 將根據具體的配置決定自己是 commit 還是 rollback (+MyCheckedException).
  
  
上面大致講述了巢狀事務的使用場景, 下面我們來看如何在 spring 中使用 PROPAGATION_NESTED, 首先來看 AbstractPlatformTransactionManager

一目瞭然

1. 我們要設定 transactionManager 的 nestedTransactionAllowed 屬性為 true, 注意, 此屬性預設為 false!!!

再看 AbstractTransactionStatus#createAndHoldSavepoint() 方法

可以看到 Savepoint 是 SavepointManager.createSavepoint 實現的, 再看 SavepointManager 的層次結構, 發現
  其 Template 實現是 JdbcTransactionObjectSupport, 常用的 DatasourceTransactionManager, HibernateTransactionManager 中的 TransactonObject 都是它的子類 :
  
  JdbcTransactionObjectSupport 告訴我們必須要滿足兩個條件才能 createSavepoint :
 
2. java.sql.Savepoint 必須存在, 即 jdk 版本要 1.4+
3. Connection.getMetaData().supportsSavepoints() 必須為 true, 即 jdbc drive 必須支援 JDBC 3.0


確保以上條件都滿足後, 你就可以嘗試使用 PROPAGATION_NESTED 了.