簡介:有時,應用程序的表示層必須處理多個 API 層方法調用之間的協調,以完成單個事務工作單元 。在本文中, 事務策略 系列作者 Mark Richards 將討論客戶端編排(Client Orchestration)事務策 略,並闡述如何在 Java™ 平台中實現它。
如果您一直在閱讀本系列,那麼應該知道現在需要一個有效且可靠的事務策略來確保數據的高度一致 性和高度完整性,而與您所使用的語言、環境、框架和平台無關。在本文中,我將討論客戶端編排事務策 略,而我之前在 “模型和策略概述” 一文中簡要介紹了這方面的內容。我的建議仍然是,在應用程序的 客戶端層必須向 API 層發起一個或多個調用才能完成單個事務工作單元時使用此策略。我將在我的代碼 示例中使用 EJB 3.0 規范;其概念對於 Spring Framework 和 Java Open Transaction Manager (JOTM) 是相同的。
有時,應用程序是使用細粒度的 API 層編寫的,這需要客戶端向 API 層發起多個調用才能實現單個 邏輯工作單元(LUW)。這可能是因為復雜和多樣的客戶端請求不能使用粗粒度的 API 模型進行聚合,或 者僅僅是由較差的應用程序設計造成的。無論何種原因,當來自客戶端的多個 API 層方法調用超過一個 合理的范圍來重構為單個 API 層調用時,則應該摒棄較為簡單的 API 層策略 並采用客戶端編排事務策 略。
基本結構
在 “API 層策略” 中,我概述了構建事務策略的兩條黃金法則:
開始事務的方法被指定為事務所有者。
只有事務所有者才能回滾事務。
我再次提到了這些規則,因為它們同樣適用於客戶端編排事務策略。無論開始事務的方法身處何處, 事務所有者都是管理事務和執行提交或回滾的惟一方法,這一點非常重要。
圖 1 展示了一個適用於大多數 Java 應用程序的典型邏輯應用層棧:
圖 1. 體系結構層次和事務邏輯
圖 1 中的體系結構實現了客戶端編排事務策略。包含事務邏輯的類顯示為紅色陰影。注意,在此策略 中,客戶端層和 API 層包含事務邏輯。客戶端層控制事務作用域。事務從此處開始、提交和回滾。API 層方法包含一些事務指令,它們指示事務管理程序整合和使用由客戶端層開始的事務作用域。業務層和持 久層不包含事務邏輯,這意味著這些層不會開始、提交或回滾事務,它們也不包含事務注釋,比如說 EJB 3.0 中的 @TransactionAttribute。
不要受限於圖 1 顯示的 4 個層次。您的應用程序體系結構可以擁有更多或更少的層次。您可以將表 示層和域層結合在一個 WAR 文件中,或者您的域類可以包含在一個單獨的 EAR 文件中。您可以讓業務邏 輯包含在域類中,並將它們結合為一個層次。這不會對事務策略的運行以及實現造成影響。
這種事務策略非常適合擁有復雜和細粒度 API 層的應用程序。這些應用程序 — 通常稱作 chatty — 需要一些對 API 的調用以實現單個 LUW。客戶端編排事務策略並沒有 API Layer 事務策略那樣的單 API 層調用限制:您可以從客戶端層向 API 層發起一個調用,或者針對每個 LUW 發起一個調用。但是,從應 用程序體系結構的角度來說,這種事務策略的限制大於其他事務策略,因為客戶端層必須能夠開始一個事 務並將它傳播給 API 層。這意味著您不能使用 Java Message Service (JMS) 消息傳遞客戶端、Web 服 務客戶端或者非 Java 客戶端。此外,客戶端層和 API 層之間的通信協議(如果有)必須支持事務的傳 播(舉例來說,通過 Internet Inter-Orb Protocol [RMI-IIOP] 傳輸 RMI;參見 參考資料。)
我並不贊成使用細粒度的 API 層 chatty 應用程序體系結構;我認為,如果 您的應用程序是 chatty 式的,並且不能重構,則客戶端編排事務策略可能是正確的選擇。
策略規則和特性
以下規則和特性適用於客戶端編排事務策略:
只有應用程序體系結構的客戶端層和 API 層中的方法才應該包含事務邏輯。其他方法、類或組件都不 應包含事務邏輯(包括事務注釋、程序化事務邏輯和回滾邏輯)。
客戶端層方法是惟一負責開始、提交和回滾事務的方法。
開始事務的客戶端層方法被稱作事務所有者。
在大多數情況下,客戶端層需要程序化的事務,這表示您必須通過編程獲取事務管理程序,並編寫開 始、提交和回滾邏輯。此規則的例外情況是,管理事務作用域的客戶端層中的客戶端業務代理由 Spring Framework 托管為 Spring bean。在這種情況下,您可以使用 Spring 提供的聲明式事務模型。
由於您不能通過編程傳遞事務上下文,因此 API 層必須使用聲明式事務模型,這表示容器將管理事務 。您只需要指定事務屬性(沒有回滾代碼或回滾指令!)。
API 層中的所有公共寫方法(插入、更新和刪除)都應該標記一個 MANDATORY 事務屬性,這表示需要 事務,但必須在調用方法之前建立事務上下文。與 REQUIRED 屬性不同,MANDATORY 屬性將不會 開始不 存在的事務,而是拋出一個異常,指示需要事務。
API 層中的所有公共寫方法(插入、更新和刪除)都不應該包含回滾邏輯,無論拋出的異常的類型是 什麼。
API 層中的所有公共讀方法默認都應該標記一個 SUPPORTS 事務屬性。這將確保在作用域的上下文中 調用讀方法時,它將包含在事務作用域中。否則,它將在沒有事務上下文的情況下運行,這需要假設它是 在邏輯工作單元(LUW)中惟一調用的方法。我在此處假設讀操作(作為 API 層的入口點)不會對數據庫 調用寫操作。
客戶端層中的事務上下文將傳播給 API 層方法以及在 API 層中調用的所有方法。
如果客戶端層向 API 層發起遠程調用,則客戶端層必須使用支持傳播事務上下文的協議和事務管理程 序(比如說 RMI-IIOP)。
局限和限制
如前所述,這種事務策略的最大一個限制就是,客戶端層必須能夠開始一個事務並將它傳播給 API 層 。這意味著用於在客戶端層和 API 層之間進行通信的協議以及客戶端的類型在應用程序體系結構中發揮 著重要的作用。舉例來說,您不能將此策略用於 Web 服務客戶端或者 JMS 客戶端,同時也不能在客戶端 層和 API 層之間依賴 HTTP 通信;這兩層之間中所使用的協議必須能夠支持事務的傳播。
與 API 層策略 不同,此策略的另一個限制是,您不能 “欺騙” 並將它增量式地引入到應用程序體 系結構中。對於 API 層事務策略,您在重構過程中在客戶端層上開始一個事務並不會引起災難性後果。 在 API 層事務策略中這樣做的影響是客戶端層將不能對異常采取正確的措施,並且您將不能回滾已經回 滾到 API 層中的事務。小問題很多,但並不具有毀滅性。
但是,通過客戶端編排事務策略,由於 API 層使用 MANDATORY 事務屬性,並且不包含事務回滾邏輯 ,因此客戶端層方法必須開始一個事務。將 API 層方法修改為 REQUIRED 並添加回滾邏輯,這樣會帶來 “API 層策略” 中的 “局限和限制” 一節中概述的相同問題。此外,在 API 層方法中使用 REQUIRED 意味著事務可以 由 API 層開始,因此這違反了客戶端編排事務策略的主要原則。
事務策略實現
客戶端編排事務策略的實現相當簡單,但由於它涉及體系結構的客戶端層和 API 層,因此我將針對兩 個層的方法展示事務邏輯和策略實現。
回顧 策略規則和特性 一節,在大多數情況下,客戶端層都需要使用程序化事務,除非它作為 Spring 托管 bean 在 Spring 上下文中運行。由於我在實現示例中使用的是 EJB3,因此我將展示使用程序化事 務的實現。您可以參考 “模型和策略概述” 或者 Spring Framework 文檔,了解如何在 Spring 中使用 程序化事務(參見 參考資料)。
此外,如果您運行了一個外部客戶端,則應該確保事務管理程序支持跨 JVM 傳播事務。在我的示例中 ,我使用 JBoss 4.2.0、EJB 3.0、Java Persistence API (JPA) 和 Hibernate 3.2.3,並運行使用 InnoDB 引擎的 MySQL 5.0.51b。這種環境(特別是 JBoss)支持跨多個 JVM 傳播客戶端事務(使用 RMI-IIOP)。
我將從讀操作開始,因為它們是最早出現的。對於在客戶端層發起的數據庫讀操作,從事務的角度來 說您不需要對客戶端代碼執行任何操作,因為數據庫讀操作不需要事務(參見 “了解事務陷阱” 中的 了解事務陷阱 側欄)。但是,通過 API 層事務策略,您會希望將 API 層讀操作方法設置為 SUPPORTS, 以確保在作用域的上下文中調用讀方法時,它將包含在事務作用域中。
清單 1 演示了一個簡單的調用讀操作的客戶端層方法。注意,getTrade() 讀方法中不需要事務邏輯 :
清單 1. 讀操作 — 客戶端層
package com.trading.client; import javax.naming.InitialContext; import com.trading.common.TradeData; import com.trading.common.TradingService; public class TradingClient { public static void main(String[] args) { new TradingClient().getTrade(); } public void getTrade() { try { InitialContext ctx = new InitialContext(); TradingService service = (TradingService) ctx.lookup("TradingServiceImpl/remote"); TradeData trade = service.getTrade(11); System.out.println(trade); } catch (Exception e) { e.printStackTrace(); } } }
TradingServiceImpl EJB3 無狀態會話 bean 中的相應 getTrade() API 層讀方法如清單 2 所示:
清單 2. 讀操作 — API 層
package com.trading.server; import javax.ejb.Stateless; import javax.ejb.Remote; import javax.ejb.TransactionAttribute; import javax.ejb.TransactionAttributeType; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import com.trading.common.TradeData; import com.trading.common.TradingService; @Stateless @Remote(TradingService.class) public class TradingServiceImpl implements TradingService { @PersistenceContext EntityManager em; @TransactionAttribute(TransactionAttributeType.SUPPORTS) public TradeData getTrade(long tradeId) throws Exception { return em.find(TradeData.class, tradeId); } }
注意,清單 2 中使用了 SUPPORTS 事務屬性,這表示如果此方法是獨立調用的,那麼它將不會開始事 務,但如果它是在已有事務中調用的話,則會使用一個已有的事務上下文。
對於數據庫更新操作、客戶端層,作為事務所有者,負責獲取事務管理程序,開始事務,然後提交事 務或者根據操作的輸出回滾它。通常,您需要使用客戶端層中的程序化事務。在 EJB3 中,其實現方法是 首先與應用服務器建立一個 InitialContext,然後查找 UserTransaction 的 Java Naming and Directory Interface (JNDI) 名稱。對於 JBoss,JNDI 名稱是 UserTransaction。您可以參考我編寫的 事務書籍,獲取大多數常用應用服務器的 JNDI 名稱清單(參見 參考資料),或者參閱您所使用的應用 服務器的文檔。建立 UserTransaction 之後,您可以編寫 begin() 方法來開始事務,以及 commit() 方 法來提交事務,並且 — 如果出現異常 — 編寫 rollback() 方法來回滾事務。清單 3 顯示了一個客戶 端層方法的完整源代碼,該方法向 API 層發起更新請求,以插入股票交易並更新客戶帳戶:
清單 3. 更新操作 — 客戶端層
package com.trading.client; import javax.naming.InitialContext; import javax.transaction.UserTransaction; import com.trading.common.AcctData; import com.trading.common.TradeData; import com.trading.common.TradingService; public class TradingClient { UserTransaction txn = null; public static void main(String[] args) { new TradingClient().placeTrade(); } public void placeTrade() { try { InitialContext ctx = new InitialContext(); TradingService service = (TradingService) ctx.lookup("TradingServiceImpl/remote"); TradeData trade = new TradeData(); trade.setAcctId(1234); trade.setAction("BUY"); trade.setSymbol("AAPL"); trade.setShares(100); trade.setPrice(103.45); txn = (UserTransaction)ctx.lookup("UserTransaction"); txn.begin(); service.insertTrade(trade); service.updateAcct(trade); txn.commit(); } catch (Exception e) { try { txn.rollback(); } catch (Exception e2) { e2.printStackTrace(); } System.out.println("ERROR: Trade Not Placed"); e.printStackTrace(); } } }
由於客戶端層中的更新方法始終是客戶端編排事務策略中的事務所有者,因此 API 層中的公共方法永 遠都不應該開始事務。基於此原因,它們必須使用 MANDATORY 事務屬性,這表示方法需要事務,但不應 該在別處開始它(比如說在客戶端層中)。此外,與第二條黃金法則一致,API 層中的更新方法不應包含 任何事務回滾邏輯。清單 4 顯示了一個完整的 EJB3 無狀態會話 bean 示例,它為 清單 3 中的相應的 客戶端層代碼實現了客戶端編排事務策略:
清單 4. 更新操作 — API 層
package com.trading.server;
import javax.ejb.Remote;
import javax.ejb.SessionContext;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import com.trading.common.AcctData;
import com.trading.common.TradeData;
import com.trading.common.TradingService;
@Stateless
@Remote(TradingService.class)
public class TradingServiceImpl implements TradingService {
@PersistenceContext EntityManager em;
@TransactionAttribute(TransactionAttributeType.MANDATORY)
public TradeData insertTrade(TradeData trade) throws Exception {
trade.setStage ("PLACED");
em.persist(trade);
return trade;
}
@TransactionAttribute(TransactionAttributeType.MANDATORY)
public void updateAcct(TradeData trade) throws Exception {
AcctData acct = em.find (AcctData.class, trade.getAcctId());
if (trade.getAction().equals("BUY")) {
acct.setBalance(acct.getBalance() - (trade.getShares() * trade.getPrice ()));
} else {
acct.setBalance(acct.getBalance() + (trade.getShares() * trade.getPrice()));
}
}
}
從清單 4 中可以看到,如果任何一個更新方法拋出異常,則客戶端層方法將負責執行必要的事務回滾 。您還可以從清單 4 中看出,采用這種策略,客戶端層必須 能夠開始和傳播事務;否則,您將遇到一個 javax.ejb.EJBTransactionRequiredException,這表示需要事務才能調用更新方法。
結束語
當來自客戶端層的大多數請求都需要向 API 層發起多個調用才能完成一個 LUW 時,客戶端編排事務 策略將非常有用。但是需要注意 — 實現這種策略會對應用程序的體系結構造成一些限制,這主要體現在 體系結構能支持哪些類型的客戶端,以及客戶端層和 API 層之間所使用的通信協議。這種事務策略的另 一個缺點是,在客戶端層中使用程序化事務始終會存在引發 “程序員錯誤” 的可能性,更不用說客戶端 開發人員現在必須學習 Java Transaction API (JTA) 和相應的事務邏輯。
不要嘗試在相同應用程序中混合客戶端編排策略和 API 層策略,以期解決應用程序體系結構中的所有 變化。這是不會起作用的,並且會造成數據庫中的數據不一致,以及過度復雜的設計。如果您的客戶端不 支持事務,但您發現客戶端編排事務策略非常合適,那麼需要執行一些重構。擺脫這種 “混合” 問題的 一種方法是提供一個 “替代 API” 層,它使用 API 層事務策略調用使用客戶端編排策略的 API 層。但 是,需要記住對這種替代 API 的調用必須是單一的調用(在 API 層事務策略中指定)。從本質上說,您 將替代 API 作為 API 層的客戶端對待。在此基礎上,您可以發起多個 API 層調用,因為事務會在新的 替代 API 中生成。