1. 程式人生 > >Spring @Transactional (一) 加強版

Spring @Transactional (一) 加強版

清單 4. 使用 @Transactional 註釋

view plaincopy to clipboardprint?
public class TradingServiceImpl {  
   @PersistenceContext(unitName="trading") EntityManager em;  

   @Transactional
   public long insertTrade(TradeData trade) throws Exception {  
      em.persist(trade);  
      return trade.getTradeId();  
   }  
}
public class TradingServiceImpl {
   @PersistenceContext(unitName="trading") EntityManager em;

   @Transactional
   public long insertTrade(TradeData trade) throws Exception {
      em.persist(trade);
      return trade.getTradeId();
   }
}

現在重新測試程式碼,您發現上述方法仍然不能工作。問題在於您必須告訴 Spring Framework,您正在對事務管理應用註釋。除非您進行充分的單元測試,否則有時候很難發現這個陷阱。這通常只會導致開發人員在 Spring 配置檔案中簡單地新增事務邏輯,而不會使用註釋。

要在 Spring 中使用 @Transactional 註釋,必須在 Spring 配置檔案中新增以下程式碼行:

view plaincopy to clipboardprint?
<tx:annotation-driven transaction-manager="transactionManager"/>
<tx:annotation-driven transaction-manager="transactionManager"/>

transaction-manager 屬性儲存一個對在 Spring 配置檔案中定義的事務管理器 bean 的引用。這段程式碼告訴 Spring 在應用事務攔截器時使用 @Transaction 註釋。如果沒有它,就會忽略 @Transactional 註釋,導致程式碼不會使用任何事務。

讓基本的 @Transactional 註釋在 清單 4 的程式碼中工作僅僅是開始。注意,清單 4 使用 @Transactional 註釋時沒有指定任何額外的註釋引數。我發現許多開發人員在使用 @Transactional 註釋時並沒有花時間理解它的作用。例如,像我一樣在清單 4 中單獨使用 @Transactional 註釋時,事務傳播模式被設定成什麼呢?只讀標誌被設定成什麼呢?事務隔離級別的設定是怎樣的?更重要的是,事務應何時回滾工作?理解如何使用這個註釋對於 確保在應用程式中獲得合適的事務支援級別非常重要。回答我剛才提出的問題:在單獨使用不帶任何引數的 @Transactional 註釋時,傳播模式要設定為 REQUIRED,只讀標誌設定為 false,事務隔離級別設定為 READ_COMMITTED,而且事務不會針對受控異常(checked exception)回滾。

@Transactional 只讀標誌陷阱

我在工作中經常碰到的一個常見陷阱是 Spring @Transactional 註釋中的只讀標誌沒有得到恰當使用。這裡有一個快速測試方法:在使用標準 JDBC 程式碼獲得 Java 永續性時,如果只讀標誌設定為 true,傳播模式設定為 SUPPORTS,清單 5 中的 @Transactional 註釋的作用是什麼呢?


清單 5. 將只讀標誌與 SUPPORTS 傳播模式結合使用 — JDBC

view plaincopy to clipboardprint?
@Transactional(readOnly = true, propagation=Propagation.SUPPORTS)  
public long insertTrade(TradeData trade) throws Exception {  
   //JDBC Code...  
}
@Transactional(readOnly = true, propagation=Propagation.SUPPORTS)
public long insertTrade(TradeData trade) throws Exception {
   //JDBC Code...
}

當執行清單 5 中的 insertTrade() 方法時,猜一猜會得到下面哪一種結果:
丟擲一個只讀連線異常
正確插入交易訂單並提交資料
什麼也不做,因為傳播級別被設定為 SUPPORTS
是哪一個呢?正確答案是 B。交易訂單會被正確地插入到資料庫中,即使只讀標誌被設定為 true,且事務傳播模式被設定為 SUPPORTS。但這是如何做到的呢?由於傳播模式被設定為 SUPPORTS,所以不會啟動任何事物,因此該方法有效地利用了一個本地(資料庫)事務。只讀標誌只在事務啟動時應用。在本例中,因為沒有啟動任何事 務,所以只讀標誌被忽略。

Spring Framework @Transactional 註釋陷阱-4

清單 6 中的 @Transactional 註釋在設定了只讀標誌且傳播模式被設定為 REQUIRED 時,它的作用是什麼呢?


清單 6. 將只讀標誌與 REQUIRED 傳播模式結合使用 — JDBC

view plaincopy to clipboardprint?
@Transactional(readOnly = true, propagation=Propagation.REQUIRED)  
public long insertTrade(TradeData trade) throws Exception {  
   //JDBC code...  
}
@Transactional(readOnly = true, propagation=Propagation.REQUIRED)
public long insertTrade(TradeData trade) throws Exception {
   //JDBC code...
}

執行清單 6 中的 insertTrade() 方法會得到下面哪一種結果呢:

丟擲一個只讀連線異常
正確插入交易訂單並提交資料
什麼也不做,因為只讀標誌被設定為 true
根據前面的解釋,這個問題應該很好回答。正確的答案是 A。會丟擲一個異常,表示您正在試圖對一個只讀連線執行更新。因為啟動了一個事務(REQUIRED),所以連線被設定為只讀。毫無疑問,在試圖執行 SQL 語句時,您會得到一個異常,告訴您該連線是一個只讀連線。

關於只讀標誌很奇怪的一點是:要使用它,必須啟動一個事務。如果只是讀取資料,需要事務嗎?答案是根本不需要。啟動一個事務來執行只讀操作會增加處 理執行緒的開銷,並會導致資料庫發生共享讀取鎖定(具體取決於使用的資料庫型別和設定的隔離級別)。總的來說,在獲取基於 JDBC 的 Java 永續性時,使用只讀標誌有點毫無意義,並會啟動不必要的事務而增加額外的開銷。

使用基於 ORM 的框架會怎樣呢?按照上面的測試,如果在結合使用 JPA 和 Hibernate 時呼叫 insertTrade() 方法,清單 7 中的 @Transactional 註釋會得到什麼結果?


清單 7. 將只讀標誌與 REQUIRED 傳播模式結合使用 — JPA

view plaincopy to clipboardprint?
@Transactional(readOnly = true, propagation=Propagation.REQUIRED)  
public long insertTrade(TradeData trade) throws Exception {  
   em.persist(trade);  
   return trade.getTradeId();  
}
@Transactional(readOnly = true, propagation=Propagation.REQUIRED)
public long insertTrade(TradeData trade) throws Exception {
   em.persist(trade);
   return trade.getTradeId();
}

清單 7 中的 insertTrade() 方法會得到下面哪一種結果:

丟擲一個只讀連線異常
正確插入交易訂單並提交資料
什麼也不做,因為 readOnly 標誌被設定為 true
正確的答案是 B。交易訂單會被準確無誤地插入資料庫中。請注意,上一示例表明,在使用 REQUIRED 傳播模式時,會丟擲一個只讀連線異常。使用 JDBC 時是這樣。使用基於 ORM 的框架時,只讀標誌只是對資料庫的一個提示,並且一條基於 ORM 框架的指令(本例中是 Hibernate)將物件快取的 flush 模式設定為 NEVER,表示在這個工作單元中,該物件快取不應與資料庫同步。不過,REQUIRED 傳播模式會覆蓋所有這些內容,允許事務啟動並工作,就好像沒有設定只讀標誌一樣。

這令我想到了另一個我經常碰到的主要陷阱。閱讀了前面的所有內容後,您認為如果只對 @Transactional 註釋設定只讀標誌,清單 8 中的程式碼會得到什麼結果呢?


清單 8. 使用只讀標誌 — JPA

view plaincopy to clipboardprint?
@Transactional(readOnly = true)  
public TradeData getTrade(long tradeId) throws Exception {  
   return em.find(TradeData.class, tradeId);  
}
@Transactional(readOnly = true)
public TradeData getTrade(long tradeId) throws Exception {
   return em.find(TradeData.class, tradeId);
}

清單 8 中的 getTrade() 方法會執行以下哪一種操作?

啟動一個事務,獲取交易訂單,然後提交事務
獲取交易訂單,但不啟動事務
正確的答案是 A。一個事務會被啟動並提交。不要忘了,@Transactional 註釋的預設傳播模式是 REQUIRED。這意味著事務會在不必要的情況下啟動。根據使用的資料庫,這會引起不必要的共享鎖,可能會使資料庫中出現死鎖的情況。此外,啟動和停止 事務將消耗不必要的處理時間和資源。總的來說,在使用基於 ORM 的框架時,只讀標誌基本上毫無用處,在大多數情況下會被忽略。但如果您堅持使用它,請記得將傳播模式設定為 SUPPORTS(如清單 9 所示),這樣就不會啟動事務:
清單 9. 使用只讀標誌和 SUPPORTS 傳播模式進行選擇操作

view plaincopy to clipboardprint?
@Transactional(readOnly = true, propagation=Propagation.SUPPORTS)  
public TradeData getTrade(long tradeId) throws Exception {  
   return em.find(TradeData.class, tradeId);  
}
@Transactional(readOnly = true, propagation=Propagation.SUPPORTS)
public TradeData getTrade(long tradeId) throws Exception {
   return em.find(TradeData.class, tradeId);
}

另外,在執行讀取操作時,避免使用 @Transactional 註釋,如清單 10 所示:

清單 10. 刪除 @Transactional 註釋進行選擇操作

view plaincopy to clipboardprint?
public TradeData getTrade(long tradeId) throws Exception {  
   return em.find(TradeData.class, tradeId);  
}
public TradeData getTrade(long tradeId) throws Exception {
   return em.find(TradeData.class, tradeId);
}

REQUIRES_NEW 事務屬性陷阱

不管是使用 Spring Framework,還是使用 EJB,使用 REQUIRES_NEW 事務屬性都會得到不好的結果並導致資料損壞和不一致。REQUIRES_NEW 事務屬性總是會在啟動方法時啟動一個新的事務。許多開發人員都錯誤地使用 REQUIRES_NEW 屬性,認為它是確保事務啟動的正確方法。

Spring Framework @Transactional 註釋陷阱-5

清單 11. 使用 REQUIRES_NEW 事務屬性

view plaincopy to clipboardprint?
@Transactional(propagation=Propagation.REQUIRES_NEW)  
public long insertTrade(TradeData trade) throws Exception {...}  

@Transactional(propagation=Propagation.REQUIRES_NEW)  
public void updateAcct(TradeData trade) throws Exception {...}
@Transactional(propagation=Propagation.REQUIRES_NEW)
public long insertTrade(TradeData trade) throws Exception {...}

@Transactional(propagation=Propagation.REQUIRES_NEW)
public void updateAcct(TradeData trade) throws Exception {...}

注意,清單 11 中的兩個方法都是公共方法,這意味著它們可以單獨呼叫。當使用 REQUIRES_NEW 屬性的幾個方法通過服務間通訊或編排在同一邏輯工作單元內呼叫時,該屬性就會出現問題。例如,假設在清單 11 中,您可以獨立於一些用例中的任何其他方法來呼叫 updateAcct() 方法,但也有在 insertTrade() 方法中呼叫 updateAcct() 方法的情況。現在如果呼叫 updateAcct() 方法後丟擲異常,交易訂單就會回滾,但帳戶更新將會提交給資料庫,如清單 12 所示:


清單 12. 使用 REQUIRES_NEW 事務屬性的多次更新

view plaincopy to clipboardprint?
@Transactional(propagation=Propagation.REQUIRES_NEW)  
public long insertTrade(TradeData trade) throws Exception {  
   em.persist(trade);  
   updateAcct(trade);  
   //exception occurs here! Trade rolled back but account update is not!  
   ...  
}
@Transactional(propagation=Propagation.REQUIRES_NEW)
public long insertTrade(TradeData trade) throws Exception {
   em.persist(trade);
   updateAcct(trade);
   //exception occurs here! Trade rolled back but account update is not!
   ...
}

之所以會發生這種情況是因為 updateAcct() 方法中啟動了一個新事務,所以在 updateAcct() 方法結束後,事務將被提交。使用 REQUIRES_NEW 事務屬性時,如果存在現有事務上下文,當前的事務會被掛起並啟動一個新事務。方法結束後,新的事務被提交,原來的事務繼續執行。

由於這種行為,只有在被呼叫方法中的資料庫操作需要儲存到資料庫中,而不管覆蓋事務的結果如何時,才應該使用 REQUIRES_NEW 事務屬性。比如,假設嘗試的所有股票交易都必須被記錄在一個審計資料庫中。出於驗證錯誤、資金不足或其他原因,不管交易是否失敗,這條資訊都需要被持久 化。如果沒有對審計方法使用 REQUIRES_NEW 屬性,審計記錄就會連同嘗試執行的交易一起回滾。使用 REQUIRES_NEW 屬性可以確保不管初始事務的結果如何,審計資料都會被儲存。這裡要注意的一點是,要始終使用 MANDATORY 或 REQUIRED 屬性,而不是 REQUIRES_NEW,除非您有足夠的理由來使用它,類似審計示例中的那些理由。

事務回滾陷阱

我將最常見的事務陷阱留到最後來講。遺憾的是,我在生產程式碼中多次遇到這個錯誤。我首先從 Spring Framework 開始,然後介紹 EJB 3。

到目前為止,您研究的程式碼類似清單 13 所示:


清單 13. 沒有回滾支援

view plaincopy to clipboardprint?
@Transactional(propagation=Propagation.REQUIRED)  
public TradeData placeTrade(TradeData trade) throws Exception {  
   try {  
      insertTrade(trade);  
      updateAcct(trade);  
      return trade;  
   } catch (Exception up) {  
      //log the error  
      throw up;  
   }  
}
@Transactional(propagation=Propagation.REQUIRED)
public TradeData placeTrade(TradeData trade) throws Exception {
   try {
      insertTrade(trade);
      updateAcct(trade);
      return trade;
   } catch (Exception up) {
      //log the error
      throw up;
   }
}
假設帳戶中沒有足夠的資金來購買需要的股票,或者還沒有準備購買或出售股票,並丟擲了一個受控異常(例如 FundsNotAvailableException),那麼交易訂單會儲存在資料庫中嗎?還是整個邏輯工作單元將執行回滾?答案出乎意料:根據受控異 常(不管是在 Spring Framework 中還是在 EJB 中),事務會提交它還未提交的所有工作。使用清單 13,這意味著,如果在執行 updateAcct() 方法期間丟擲受控異常,就會儲存交易訂單,但不會更新帳戶來反映交易情況。

這可能是在使用事務時出現的主要資料完整性和一致性問題了。執行時異常(即非受控異常)自動強制執行整個邏輯工作單元的回滾,但受控異常不會。因此,清單 13 中的程式碼從事務角度來說毫無用處;儘管看上去它使用事務來維護原子性和一致性,但事實上並沒有。

儘管這種行為看起來很奇怪,但這樣做自有它的道理。首先,不是所有受控異常都是不好的;它們可用於事件通知或根據某些條件重定向處理。但更重要的 是,應用程式程式碼會對某些型別的受控異常採取糾正操作,從而使事務全部完成。例如,考慮下面一種場景:您正在為線上書籍零售商編寫程式碼。要完成圖書的訂 單,您需要將電子郵件形式的確認函作為訂單處理的一部分發送。如果電子郵件伺服器關閉,您將傳送某種形式的 SMTP 受控異常,表示郵件無法傳送。如果受控異常引起自動回滾,整個圖書訂單就會由於電子郵件伺服器的關閉全部回滾。通過禁止自動回滾受控異常,您可以捕獲該異 常並執行某種糾正操作(如向掛起佇列傳送訊息),然後提交剩餘的訂單。

Spring Framework @Transactional 註釋陷阱-6

使用 Declarative 事務模式時,必須指定容器或框架應該如何處理受控異常。在 Spring Framework 中,通過 @Transactional 註釋中的 rollbackFor 引數進行指定,如清單 14 所示:


清單 14. 新增事務回滾支援 — Spring

view plaincopy to clipboardprint?
@Transactional(propagation=Propagation.REQUIRED, rollbackFor=Exception.class)  
public TradeData placeTrade(TradeData trade) throws Exception {  
   try {  
      insertTrade(trade);  
      updateAcct(trade);  
      return trade;  
   } catch (Exception up) {  
      //log the error  
      throw up;  
   }  
}
@Transactional(propagation=Propagation.REQUIRED, rollbackFor=Exception.class)
public TradeData placeTrade(TradeData trade) throws Exception {
   try {
      insertTrade(trade);
      updateAcct(trade);
      return trade;
   } catch (Exception up) {
      //log the error
      throw up;
   }
}

注意,@Transactional 註釋中使用了 rollbackFor 引數。這個引數接受一個單一異常類或一組異常類,您也可以使用 rollbackForClassName 引數將異常的名稱指定為 Java String 型別。還可以使用此屬性的相反形式(noRollbackFor)指定除某些異常以外的所有異常應該強制回滾。通常大多數開發人員指定 Exception.class 作為值,表示該方法中的所有異常應該強制回滾。

在回滾事務這一點上,EJB 的工作方式與 Spring Framework 稍微有點不同。EJB 3.0 規範中的 @TransactionAttribute 註釋不包含指定回滾行為的指令。必須使用 SessionContext.setRollbackOnly() 方法將事務標記為執行回滾,如清單 15 所示:


清單 15. 新增事務回滾支援 — EJB

view plaincopy to clipboardprint?
@TransactionAttribute(TransactionAttributeType.REQUIRED)  
public TradeData placeTrade(TradeData trade) throws Exception {  
   try {  
      insertTrade(trade);  
      updateAcct(trade);  
      return trade;  
   } catch (Exception up) {  
      //log the error  
      sessionCtx.setRollbackOnly();  
      throw up;  
   }  
}
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public TradeData placeTrade(TradeData trade) throws Exception {
   try {
      insertTrade(trade);
      updateAcct(trade);
      return trade;
   } catch (Exception up) {
      //log the error
      sessionCtx.setRollbackOnly();
      throw up;
   }
}
呼叫 setRollbackOnly() 方法後,就不能改變主意了;惟一可能的結果是在啟動事務的方法完成後回滾事務。本系列後續文章中描述的事務策略將介紹何時、何處使用回滾指令,以及何時使用 REQUIRED 與 MANDATORY 事務屬性。

Isolation Level(事務隔離等級)

1、Serializable:最嚴格的級別,事務序列執行,資源消耗最大;
2、REPEATABLE READ:保證了一個事務不會修改已經由另一個事務讀取但未提交(回滾)的資料。避免了“髒讀取”和“不可重複讀取”的情況,但是帶來了更多的效能損失。
3、READ COMMITTED:大多數主流資料庫的預設事務等級,保證了一個事務不會讀到另一個並行事務已修改但未提交的資料,避免了“髒讀取”。該級別適用於大多數系統。
4、Read Uncommitted:保證了讀取過程中不會讀取到非法資料。隔離級別在於處理多事務的併發問題。
我們知道並行可以提高資料庫的吞吐量和效率,但是並不是所有的併發事務都可以併發執行。
我們首先說併發中可能發生的3中不討人喜歡的事情
1: Dirty reads--讀髒資料。也就是說,比如事務A的未提交(還依然快取)的資料被事務B讀走,如果事務A失敗回滾,會導致事務B所讀取的的資料是錯誤的。
2: non-repeatable reads--資料不可重複讀。比如事務A中兩處讀取資料-total-的值。在第一讀的時候,total是100,然後事務B就把total的資料改成 200,事務A再讀一次,結果就發現,total竟然就變成200了,造成事務A資料混亂。
3: phantom reads--幻象讀資料,這個和non-repeatable reads相似,也是同一個事務中多次讀不一致的問題。但是non-repeatable reads的不一致是因為他所要取的資料集被改變了(比如total的資料),但是phantom reads所要讀的資料的不一致卻不是他所要讀的資料集改變,而是他的條件資料集改變。比如Select account.id where account.name="ppgogo*",第一次讀去了6個符合條件的id,第二次讀取的時候,由於事務b把一個帳號的名字由"dd"改 成"ppgogo1",結果取出來了7個數據。

Dirty reads non-repeatable reads phantom reads
Serializable 不會 不會 不會
REPEATABLE READ 不會 不會
READ COMMITTED 不會
Read Uncommitted

readOnly
事務屬性中的readOnly標誌表示對應的事務應該被最優化為只讀事務。