事務策略: 模型和策略概述

混淆事務模型與事務策略是一個常見的錯誤。本系列關於 事務策略 的第二篇文章將概述 Java™ 平臺支持的三種事務模型,並介紹使用這些模型的四種主要事務策略。通過使用 Spring Framework 和 Enterprise JavaBeans (EJB) 3.0 規範中的示例,Mark Richards 將解釋事務模型的運行原理以及它們如何形成開發各種事務策略(從基本的事務處理到高速事務處理系統)的基礎。

開發人員、設計人員和架構師經常會混淆事務模型事務策略 。我經常會讓與客戶接觸的架構師和技術總監描述他們項目的事務策略。我通常會獲得三種迴應。有時,他們會說 “我們實際上並未在應用程序中使用事務。”另一些時候,我會聽到迷惑的回答:“我不明白你的意思。”但是,我也會遇到非常自信的回答:“我們使用聲明式事務。”在本文中,術語聲明式事務 描述的是一個事務模型 ,但它絕不是一種事務策略

Java 平臺支持的三種事務模型包括:

  • Local Transaction 模型
  • Programmatic Transaction 模型
  • Declarative Transaction 模型

這些模型描述事務在 Java 平臺中的基本運行方式,以及它們是如何實現的。但是,它們僅提供了事務處理的規則和語義。如何應用事務模型則完全由您決定。舉例來說,應該如何在 REQUIREDMANDATORY 事務屬性之間做出選擇?您應該在何時何種情況下指定事務回滾指令?您應該在何時考慮 Programmatic Transaction 模型與 Declarative Transaction 模型的優劣?您應該如何優化高性能系統的事務?事務模型本身無法回答這些問題。您必須通過開發自己的事務策略或採用本文介紹的四種主要事務策略之一來解決它們。

如本系列的 第一篇文章 所述,許多常見的事務陷阱都會影響到事務行爲,並且由此會降低數據的完整性和一致性。同樣,缺乏有效的(或任何)事務策略將對您數據的完整性和一致性造成負面影響。本文所描述的事務模型是開發有效事務策略的基本元素。理解這些模型之間的差異以及它們的運行方式對於理解使用它們的事務策略非常重要。在介紹完三種事務模型之後,我將討論適用於大多數業務應用程序(從簡單的 Web 應用程序到大型的高速事務處理系統)的四種事務策略。 事務策略 系列的後續文章將詳細討論這些策略。

Local Transaction 模型

在 Local Transaction 模型中,事務由底層數據庫資源管理程序(而非應用程序所在的容器或框架)管理,這便是它得名的原因。在這個模型中,您將管理連接 ,而不是事務 。從 “瞭解事務陷阱 ” 一文可知,在使用對象關係映射框架,如 Hibernate、TopLink 或 the Java Persistence API (JPA),執行數據庫更新時,您不能使用 Local Transaction 模型。在使用數據訪問對象(DAO)或基於 JDBC 的框架和數據庫存儲過程時,您可以使用它。

您可以採用以下兩種方式來使用 Local Transaction 模型:讓數據庫來管理連接,或者以編程的方式管理連接。要讓數據庫管理連接,您需要將 JDBC Connection 對象上的 autoCommit 屬性設置爲 true (默認值),這將通知底層數據庫管理系統(DBMS)在完成插入、更新或刪除操作之後提交事務,或者在操作失敗時返回任務。清單 1 展示了這種技巧,它在 TRADE 表中插入了一條股票交易命令:


清單 1. 包括一個更新的本地事務

public class TradingServiceImpl {
public void processTrade(TradeData trade) throws Exception {
Connection dbConnection = null;
try {
DataSource ds = (DataSource)
(new InitialContext()).lookup("jdbc/MasterDS");
dbConnection = ds.getConnection();
dbConnection.setAutoCommit(true);
Statement sql = dbConnection.createStatement();
String stmt = "insert into TRADE ...";
sql.executeUpdate(stmt1);
} finally {
if (dbConnection != null)
dbConnection.close();
}
}
}

 

注意,在清單 1 中,autoCommit 值設置爲 true ,這將指示 DBMS 應該在各數據庫語句之後提交本地事務。如果在邏輯工作單元(LUW)中維護一個數據庫活動,那麼這一技巧將非常有用。但是,假設清單 1 中的 processTrade() 方法還將更新 ACCT 表中的餘額以反映交易訂單的值。在本例中,兩個數據庫操作是相互獨立的,針對 TRADE 表的插入操作將在更新 ACCT 表之後提交給數據庫。若 ACCT 表更新失敗,沒有任何機制可以回滾到對 TRADE 表的更新操作,從而造成數據庫中的數據不一致。

此場景又引出了第二個技巧:以編程的方式管理連接。在此技巧中,您將 Connection 對象上的 autoCommit 屬性設置爲 false ,並手動提交或回滾連接。清單 2 演示了此技巧:


清單 2. 包括多個更新的本地事務

	
public class TradingServiceImpl {
public void processTrade(TradeData trade) throws Exception {
Connection dbConnection = null;
try {
DataSource ds = (DataSource)
(new InitialContext()).lookup("jdbc/MasterDS");
dbConnection = ds.getConnection();
dbConnection.setAutoCommit(false);
Statement sql = dbConnection.createStatement();
String stmt1 = "insert into TRADE ...";
sql.executeUpdate(stmt1);
String stmt2 = "update ACCT set balance...";
sql.executeUpdate(stmt2);
dbConnection.commit();
} catch (Exception up) {
dbConnection.rollback();
throw up;
} finally {
if (dbConnection != null)
dbConnection.close();
}
}
}

 

注意,在清單 2 中,autoCommit 屬性設置爲 false ,這將通知底層 DBMS 在代碼而非數據庫中管理連接。在本例中,如果一切正常,您必須調用 Connection 對象上的 commit() 方法;否則,如果出現異常,則調用 rollback() 方法。通過這種方式,您可以在相同的工作單元中協調兩個數據庫活動。

雖然 Local Transaction 模型現在看來有些過時,但它是本文結束部分介紹的一個主要事務策略的重要元素。

 




回頁首

 

Programmatic Transaction 模型

Programmatic Transaction 模型的名稱來自這樣一個事實:開發人負責管理事務。在 Programmatic Transaction 模型中,與 Local Transaction 模型不同,您將管理事務 ,並且將與底層數據庫連接 相分離。

清單 2 中的示例相似,藉助此模型,開發人員將負責從事務管理程序獲取一個事務,啓動該事務,提交事務,以及(如果出現異常)回滾到事務。您可能已經猜到,這會產生大量易於出錯的代碼,從而對應用程序中的業務邏輯造成影響。但是,一些事務策略需要使用 Programmatic Transaction 模型。

雖然概念是相同的是,但 Programmatic Transaction 模型的實現在 Spring Framework 和 EJB 3.0 規範中是不同的。我將先使用 EJB 3.0 來演示這個模型的實現,然後使用 Spring Framework 來展示相同的數據庫更新。

使用 EJB 3.0 實現編程事務

在 EJB 3.0 中,您將從事務管理程序(換句話說,容器)獲取一個事務,方法是對 javax.transaction.UserTransaction 執行 Java Naming and Directory Interface (JNDI) 查找。獲取 UserTransaction 之後,您可以調用 begin() 方法來啓動事務,調用 commit() 方法來提交事務,並且調用 rollback() 方法以便在出現錯誤時回滾事務。在此模型中,容器不會自動提交或回滾事務;開發人員需要自己在執行數據庫更新的方法編寫此行爲。清單 3 通過 JPA 展示了使用 EJB 3.0 的 Programmatic Transaction 模型:


清單 3. 使用 EJB 3.0 實現的編程事務

	
@Stateless
@TransactionManagement(TransactionManagementType.BEAN)
public class TradingServiceImpl implements TradingService {
@PersistenceContext(unitName="trading") EntityManager em;
public void processTrade(TradeData trade) throws Exception {
InitialContext ctx = new InitialContext();
UserTransaction txn = (UserTransaction)ctx.lookup("UserTransaction");
try {
txn.begin();
em.persist(trade);
AcctData acct = em.find(AcctData.class, trade.getAcctId());
double tradeValue = trade.getPrice() * trade.getShares();
double currentBalance = acct.getBalance();
if (trade.getAction().equals("BUY")) {
acct.setBalance(currentBalance - tradeValue);
} else {
acct.setBalance(currentBalance + tradeValue);
}
txn.commit();
} catch (Exception up) {
txn.rollback();
throw up;
}
}
}

 

在帶無狀態會話 bean 的 Java EE 容器環境中使用 Programmatic Transaction 模型時,您必須通知容器您正在使用編程事務。爲此,您需要使用 @TransactionManagement 註釋並將事務類型設置爲 BEAN 。如果您未使用此註釋,則容器將假定您使用聲明式事務管理(CONTAINER ),它是 EJB 3.0 的默認事務類型。在無狀態會話 bean 上下文外部的客戶機層中使用編程事務時,您不需要設置事務類型。

使用 Spring 實現編程事務

Spring Framework 提供了兩種實現 Programmatic Transaction 模型的方法。一種是通過 Spring TransactionTemplate 來實現,另一種是直接使用 Spring 平臺事務管理程序 。由於我並不熱衷於編寫匿名內部類和難以理解的代碼,因此我將使用第二種技巧來演示 Spring 中的 Programmatic Transaction 模型。

Spring 至少有九種平臺事務管理程序。最常用的包括 DataSourceTransactionManagerHibernateTransactionManagerJpaTransactionManagerJtaTransactionManager 。我的代碼示例使用的是 JPA,因此我將展示 JpaTransactionManager 的配置。

要在 Spring 中配置 JpaTransactionManager ,只需要使用 org.springframework.orm.jpa.JpaTransactionManager 類在應用程序上下文 XML 文件中定義 bean,並添加一個到 JPA Entity Manager Factory bean 的引用。然後,假定包含應用程序邏輯的類是由 Spring 管理的,將事務管理程序注入到 bean 中,如清單 4 所示:


清單 4. 定義 Spring JPA 事務管理程序

	
<bean id="transactionManager"
         class="org.springframework.orm.jpa.JpaTransactionManager">
   <property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>

<bean id="tradingService" class="com.trading.service.TradingServiceImpl">

   <property name="txnManager" ref="transactionManager"/>
</bean>

 

如果 Spring 未管理應用程序類,那麼您可以在您的方法中引用事務管理程序,方法是在 Spring 上下文中使用 getBean() 方法。

在源代碼中,您現在可以使用平臺管理程序獲取一個事務。執行了所有更新之後,您可以調用 commit() 方法來提交事務,或者調用 rollback() 方法來回滾事務。清單 5 展示了此技巧:


清單 5. 使用 Spring JPA 事務管理程序

		
public class TradingServiceImpl  {

   @PersistenceContext(unitName="trading") EntityManager em;

   JpaTransactionManager txnManager = null;

   public void setTxnManager(JpaTransactionManager mgr) {

      txnManager = mgr;

   }

   public void processTrade(TradeData trade) throws Exception {

      TransactionStatus status =

         txnManager.getTransaction(new DefaultTransactionDefinition());

      try {

         em.persist(trade);

	 AcctData acct = em.find(AcctData.class, trade.getAcctId());

	 double tradeValue = trade.getPrice() * trade.getShares();

	 double currentBalance = acct.getBalance();

	 if (trade.getAction().equals("BUY")) {

	    acct.setBalance(currentBalance - tradeValue);

	 } else {

	    acct.setBalance(currentBalance + tradeValue);

	 }

         txnManager.commit(status);

      } catch (Exception up) {

         txnManager.rollback(status);

         throw up;

      }

   }

}

 

注意清單 5 中 Spring Framework 與 EJB 3.0 之間的差異。在 Spring 中,獲取事務(隨後啓動它)的方法是在平臺事務管理程序上調用 getTransaction() 。匿名 DefaultTransactionDefinition 類包含關於事務及其行爲的詳細信息,包括事務名稱、隔離級別、傳播模式(事務屬性)和事務超時(如果存在)。在本例中,我可以僅使用默認值,其名稱是空字符串,底層 DBMS 的默認的隔離級別通常爲 READ_COMMITTED ,事務屬性是 PROPAGATION_REQUIRED ,以及 DBMS 的默認超時。還需注意,commit()rollback() 方法是使用平臺事務管理程序調用的,而不是事務(與 DJB 的情況相同)。

 




回頁首

 

Declarative Transaction 模型

Declarative Transaction 模型,又稱作 Container Managed Transactions (CMT),是 Java 平臺中最常用的事務模型。在該模型中,容器環境負責啓動、提交和回滾事務。開發人員僅負責指定事務的行爲。本系列的 第一篇文章 所討論的大多數事務陷阱都與 Declarative Transaction 模型相關。

Spring Framework 和 EJB 3.0 都利用註釋來指定事務行爲。Spring 使用 @Transactional 註釋,而 EJB 3.0 使用 @TransactionAttribute 註釋。在使用 Declarative Transaction 模型時,容器將不會針對檢測到的異常自動回滾事務。開發人員必須指定出現異常時在何處以及何時回滾事務。在 Spring Framework 中,您通過使用 @Transactional 註釋上的 rollbackFor 屬性來指定它。在 EJB 中,您通過調用 SessionContext 上的 setRollbackOnly() 方法來指定它。

清單 6 展示了 Declarative Transaction 模型的 EJB 應用:


清單 6. 使用 EJB 3.0 的聲明式事務

@Stateless

public class TradingServiceImpl implements TradingService {

   @PersistenceContext(unitName="trading") EntityManager em;

   @Resource SessionContext ctx;

   @TransactionAttribute(TransactionAttributeType.REQUIRED)

   public void processTrade(TradeData trade) throws Exception {

      try {

         em.persist(trade);

	 AcctData acct = em.find(AcctData.class, trade.getAcctId());

	 double tradeValue = trade.getPrice() * trade.getShares();

	 double currentBalance = acct.getBalance();

	 if (trade.getAction().equals("BUY")) {

	    acct.setBalance(currentBalance - tradeValue);

	 } else {

	    acct.setBalance(currentBalance + tradeValue);

	 }

      } catch (Exception up) {

         ctx.setRollbackOnly();

         throw up;

      }

   }

}

 

清單 7 演示了 Declarative Transaction 模型的 Spring Framework 應用:


清單 7. 使用 Spring 的聲明式事務

public class TradingServiceImpl {

   @PersistenceContext(unitName="trading") EntityManager em;

   @Transactional(propagation=Propagation.REQUIRED,

                  rollbackFor=Exception.class)

   public void processTrade(TradeData trade) throws Exception {

      em.persist(trade);

      AcctData acct = em.find(AcctData.class, trade.getAcctId());

      double tradeValue = trade.getPrice() * trade.getShares();

      double currentBalance = acct.getBalance();

      if (trade.getAction().equals("BUY")) {

         acct.setBalance(currentBalance - tradeValue);

      } else {

         acct.setBalance(currentBalance + tradeValue);

      }

   }

}

 

事務屬性

除了回滾指令之外,您還必須指定事務屬性 ,這將定義事務的行爲。Java 平臺支持六種事務屬性,而與您使用的是 EJB 還是 Spring Framework 無關:

  • Required
  • Mandatory
  • RequiresNew
  • Supports
  • NotSupported
  • Never

在描述這些事務屬性時,我將使用一個假想的 methodA() 方法,事務屬性將應用於這個方法。

如果爲 methodA() 指定了 Required 事務屬性,並且在已有事務作用域內調用了 methodA() ,那麼將使用已有的事務作用域。否則,methodA() 將啓動一個新的事務。如果事務是由 methodA() 啓動的,則它也必須由 methodA() 來終止(提交或回滾)。這是最常用的事務屬性,並且是 EJB 3.0 和 Spring 的默認屬性。遺憾的是,它的使用並不正確,從而造成了數據完整性和一致性問題。對於本系列後續文章將要討論的各事務策略,我將更加詳細地闡述這個事務屬性。

如果爲 methodA() 指定了 Mandatory 事務屬性,並且在已有事務作用域內調用了 methodA() ,那麼將使用已有的事務作用域。但是,如果調用 methodA() 時沒有事務上下文,則會拋出一個 TransactionRequiredException ,指示必須在調用 methodA() 之前呈現事務。這個事務屬性應用於本文下一部分將要討論的 Client Orchestration 事務策略。

RequiresNew 事務屬性非常有趣。許多時候,我會發現這個屬性被誤用或誤解。methodA() 指定了 RequiresNew 屬性,並且在有或沒有事務上下文的情況下調用了 methodA() ,那麼新事務將始終由 methodA() 啓動(和終止)。這意味着,如果在另一個事務(比如說 Transaction1 )的上下文中調用了 methodA() ,那麼 Transaction1 將暫停,同時會啓動一個新的事務(Transaction2 )。當 methodA() 結束時,Transaction2 將提交或回滾,並且 Transaction1 將恢復。這顯然違背了事務的 ACID(atomicity、consistency、isolation 和 durability)屬性(特別是 atomicity 屬性)。換句話說,所有數據庫更新都不再包含在一個單一的工作單元中。如果 Transaction1 將進行回滾,則由 Transaction2 提交的更改將仍然被提交。如果是這種情況,那麼事務屬性有什麼好處呢?如本系列第一篇文章所述,這個事務屬性僅用於獨立於底層事務的數據庫操作(比如審計或記錄,在本例中爲 Transaction1 )。

Supports 事務屬性也是我發現大多數開發人員未完全理解或掌握的一個地方。如果爲 methodA() 指定了 Supports 事務屬性,並且在已有事務的作用域中調用了 methodA() ,則 methodA() 將在該事務的作用域中執行。但是,如果在沒有事務上下文的情況下調用了 methodA() ,則不會啓動任何事務。此屬性主要用於針對數據庫的只讀操作。如果是這種情況,爲何不指定 NotSupported 事務屬性(在下一段中討論)呢?畢竟,該屬性將確保方法在沒有事務的情況下運行。答案非常簡單。在已有事務的上下文中調用查詢操作會導致從數據庫事務日誌中讀取數據(換句話說,已更新的數據),而不在事務作用域中運行會導致查詢從表中讀取未修改的數據。舉例來說,如果您插入一個新的交易訂單到 TRADE 表中,然後(在相同的事務中)獲取所有交易訂單的列表,則未提交的交易將出現在列表中。但是,如果您使用的是 NotSupported 事務屬性,那麼它會造成數據庫查詢從表中而不是從事務日誌中讀取數據。因此,在上一個示例中,您將不會看到未提交的交易。這並不一定是一件壞事;這將由您的用例和業務邏輯決定。

NotSupported 事務屬性指定被調用的方法將不使用或啓動事務,無論是否呈現了事務。如果爲 methodA() 指定了 NotSupported 事務屬性,並且在某個事務的上下文中調用了 methodA() ,則該事務將暫停直到 methodA() 結束。當 methodA() 結束時,原始事務將被恢復。這個事務屬性只有少許用例,並且它們主要涉及數據庫存儲過程。如果您嘗試在已有事務上下文的作用域中調用數據庫存儲過程,並且數據庫存儲過程包含一個 BEGIN TRANS ,或者對於 Sybase 的情況,在未連接模式中運行,則會拋出一個異常,指示已經存在一個事務無法啓動新事務。(換句話說,內嵌事務不受支持)。幾乎所有的容器都使用 Java Transaction Service (JTS) 作爲默認的 JTA 事務實現。不支持內嵌事務的是 JTS 而不是 Java 平臺。如果您無法修改數據庫存儲過程,那麼您可以使用 NotSupported 屬性來暫停已有事務上下文,以避免這種致命的異常。但是,其影響是您不能在相同的 LUW 中對數據庫執行原子更新。這是一種權衡,但有時可以讓您迅速脫離困境。

Never 或許是最有趣的事務屬性。它的行爲類似於 NotSupported 事務屬性,但存在一個重要差異:如果使用 Never 事務屬性調用方法時已經存在某個事務上下文,則會拋出一個異常,指示您在調用該方法時不允許使用這個事務。我能想到的針對此事務屬性的唯一一個用例就是用於測試。它提供了一種簡單的方法,用於驗證當您調用特定的方法時某個事務是否存在。如果您在調用相應的方法時使用了 Never 事務屬性並且接收到了一個異常,則知道事務已經呈現。如果允許執行方法,則您知道事務未被呈現。這是確保事務策略堅固的理想方法。

 




回頁首

 

事務策略

本文描述的事務模型形成了即將介紹的事務策略的基礎。在開始構建事務策略之前,完全理解這些模型之間的差異以及它們的運行原理是非常重要的。可以在大多數業務應用程序場景中使用的主要事務策略包括:

  • Client Orchestration 事務策略
  • API Layer 事務策略
  • High Concurrency 事務策略
  • High-Speed Processing 事務策略

我將在此處簡要介紹這些策略,並將在本系列的後續文章中詳細討論它們。

當來自客戶機層的基於多服務器或基於模型的調用完成單一工作單元時,將使用 Client Orchestration 事務策略。此處的客戶機層可以表示來自 Web 框架、門戶應用程序、桌面系統或工作流產品或業務流程管理(BPM)組件的調用。從本質上說,客戶機層擁有處理流以及完成特定請求所需的 “步驟”。舉例來說,要添加一個交易訂單,假定您需要將交易插入到數據庫中,然後更新客戶的帳戶餘額以反映交易的值。如果應用程序的 API 層非常細粒度化,那麼您需要調用客戶機層中的兩個方法。在此場景中,事務工作單元必須位於客戶機層中以確保自動化的工作單元。

當粗粒度方法充當後端功能的主要入口點時,將使用 API Layer 事務策略。(如果願意,可以將它們稱作 services )。在此場景中,客戶機(基於 Web、基於 Web 服務、基於消息或者甚至桌面)向後端發起單個調用以執行特定的請求。使用上文提到的交易訂單場景,在本例中您將擁有一個入口點方法(比如說 processTrade() )由客戶機層調用。然後,這個方法將包含插入交易訂單和更新帳戶所需的業務流程。我將這個策略指定爲這個名稱的原因是,在大多數情況下,後端處理功能會通過使用接口或 API 向客戶應用程序公開。這是最常用的事務策略之一。

High Concurrency 事務策略是 API Layer 事務策略的一個變體,它用於不支持通過 API 層長時間運行事務的應用程序(通常出於性能或可伸縮性需求的原因)。顧名思義,此策略主要在從用戶的角度支持高度併發性的應用程序中使用。事務在 Java 平臺的開銷非常大。根據您所使用的數據庫,它們可以造成在數據庫中出現鎖定,獨佔資源,在吞吐量方面拖慢應用程序,並且在某些情況下甚至造成數據庫死鎖。這種事務策略內部的主要思想是縮短事務作用域,以便您能夠最大限度地減少數據庫中的死鎖,同時仍然爲任何特定的客戶機請求維持自動工作單元。在某些情況下,您可能需要重構應用程序邏輯,以支持這種事務策略。

High-Speed Processing 事務策略或許是事務策略的最極端。如果您需要從應用程序中獲取最快的處理時間(以及吞吐量),並且仍然在處理中維持一定程序的事務原子性,那麼可以使用它。雖然這種策略從數據完整性和一致的角度來說引入了一些風險,但如果正確實現,它將成爲 Java 平臺中速度可能是最快的事務策略。它可能是這四個事務策略中最難以實現的一個。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章