很多時候,程式員、設計師和架構師都混淆了事務模型和事務策略的概念。我通常會問一些架構師或者技術領導人他們的項目中使用的事務策略。我經常得到3中回答。有時是:“哦,我們的程式實際上沒有使用事務。”有些時候是:“嗯,我實際上不知道你說的是什麼意思。”通常情況下,我得到的是一個肯定的回答“我們使用聲明式事務”。但是,就像你將在本文中看到的,術語聲明式事務指的是事務模型,而不是事務策略。
Java平台支援的3種事務模型是:
這3種模型描述了在Java平台中事務的基本行為和他們怎麼實現的。但是,他們只是提供了交易處理的規則和語義,事務模型怎麼應用完全由你決定。例如,你什麼時候應該使用事務屬性REQUIRED或者MANDATORY?什麼時候以及在哪兒指定交易回復指令?什麼時候應該考慮編程式事務模型或者聲明式事務模型?怎麼為高效能系統最佳化事務?事務模型本身不能回答這些問題。而且,你必須通過開發自己的事務策略或者使用本文介紹的4種事務策略來解決這些問題。
正如你在本系列的第一篇文章中所看到的,很多常見的事務陷阱會影響事務的行為,因此降低資料的完整性和一致性。類似的,缺乏有效事務策略也會對資料的完整性和一致性產生負面影響。在本文中介紹的事務模型是開發有效事務策略的基礎。理解這些模型之間的區別以及他們怎麼工作對於理解使用他們的事務策略是至關重要的。在介紹這三種事務模型後,我將介紹適用於大部分商務應用程式的四種事務策略,從簡單的web程式到大型高速的交易處理系統。Transanction Strategies系列的後續文章將詳細介紹這些策略的細節。
本地事務模型
本地事務模型的名字來源於這樣一個事實,事務由底層的資料庫總管管理,而不是你的應用程式啟動並執行容器或者架構。這該模型中,你管理的是串連(connection)而不是事務。正如你在"Understanding transaction pitfalls,"中瞭解到的,當你使用類似於Hibernate,TopLink或者JPA等OR映射架構時,你不能使用本地事務模型。你可以在使用DAO,基於JDBC的架構或者資料庫預存程序是使用他們。
你可以以兩種方式中的一種使用本地事務模型:讓資料庫管理串連,或者由程式管理串連。通過把JDBC Connection對象的autoCommit屬性設定為true(預設值)讓資料庫管理串連。這就會告訴底層的DBMS在插入、更新或者刪除完成時提交事務,或者在失敗時復原。清單1展示了該技術,它在TRADE表中插入一條股票交易的訂單資訊。
Listing 1. Local transactions with a single update
public class TradingServiceImpl {<br /> public void processTrade(TradeData trade) throws Exception {<br /> Connection dbConnection = null;<br /> try {<br /> DataSource ds = (DataSource)<br /> (new InitialContext()).lookup("jdbc/MasterDS");<br /> dbConnection = ds.getConnection();<br /> dbConnection.setAutoCommit(true);<br /> Statement sql = dbConnection.createStatement();<br /> String stmt = "insert into TRADE ...";<br /> sql.executeUpdate(stmt1);<br /> } finally {<br /> if (dbConnection != null)<br /> dbConnection.close();<br /> }<br /> }<br />}<br />
注意,在清單1中autoCommit屬性被設定為true,告訴DBMS在每一個語句後應該提交本地事務。如果你在一個邏輯工作單元(LUW, logic unit of work)中只有單一的資料庫維護活動,這個技術可以工作的很好。但是,假設processTrade() 方法也需要更新ACCT表中的交易餘額值。在這種情況下,兩種資料庫行為是獨立的,在更新ACCT表之前插入到TRADE表中的記錄會被提交。如果更新ACCT表失敗了將沒有辦法復原插入到TRADE表中的記錄,結果就造成了資料庫中的資料不一致。
這個情況導致出現了第二種技術:由程式管理串連。在這種技術中,你將把Connection對象的autoCommit屬性設定為false,然後手動提交或者復原事務。清單2展示了該技術:
Listing 2. Local transactions with multiple updates
public class TradingServiceImpl {<br /> public void processTrade(TradeData trade) throws Exception {<br /> Connection dbConnection = null;<br /> try {<br /> DataSource ds = (DataSource)<br /> (new InitialContext()).lookup("jdbc/MasterDS");<br /> dbConnection = ds.getConnection();<br /> dbConnection.setAutoCommit(false);<br /> Statement sql = dbConnection.createStatement();<br /> String stmt1 = "insert into TRADE ...";<br /> sql.executeUpdate(stmt1);<br /> String stmt2 = "update ACCT set balance...";<br /> sql.executeUpdate(stmt2);<br /> dbConnection.commit();<br /> } catch (Exception up) {<br /> dbConnection.rollback();<br /> throw up;<br /> } finally {<br /> if (dbConnection != null)<br /> dbConnection.close();<br /> }<br /> }<br />}<br />
注意,在清單2中把autoCommit屬性設定為false,這就告訴底層DBMS串連將有代碼管理,而不是資料庫。在這種情況下,如果一切正常,你必須調用Connection對象的commit()方法 ;如果發生了異常就要調用rollback()方法。通過這種方式,你就可以把資料庫中的兩個活動組織到一個邏輯工作單元中。
儘管本地事務模型在現在看來有些過時了,但是它是本文後面描述的一種事務策略的重要元素。
編程式事務模型
編程式事務模型來源於這樣的事實,由程式員負責管理事務。不像本地事務模型,在編程式事務模型中,由你管理事務,並且它獨立於底層資料庫連接。
類似於清單2,在該模型中,有程式員負責從交易管理員中獲得一個事物,啟動事務,提交事務,或者在異常發生時復原事務。你可能會猜到,這可能會導致在你的應用程式的商務邏輯中出現大量易於出錯的代碼,儘管如此,有些事務策略需要編程式事務模型。
儘管概念相同,編程式事務模型在Spring架構和EJB3.0規範中的實現方式是不同的。我將會展示在EJB3.0中該模型的實現,然後再介紹在Spring架構中的實現。
EJB3.0中的編程式事務
在EJB3.0中,你通過JNDI尋找javax.transaction.UserTransaction從交易管理員(或者說容器)中獲得一個事務。你可以通過調用begin()方法啟動事務,調用commit()方法提交事務,或者在錯誤發生時調用rollback()方法復原事務。在該模型中,容器不會自動認可或者復原事務,而是由程式員通過編程實現。清單3展示了EJB3.0中使用JPA實現編程式事務模型的一個例子:
Listing 3. Programmatic transactions using EJB 3.0
@Stateless<br />@TransactionManagement(TransactionManagementType.BEAN)<br />public class TradingServiceImpl implements TradingService {<br /> @PersistenceContext(unitName="trading") EntityManager em;</p><p> public void processTrade(TradeData trade) throws Exception {<br /> InitialContext ctx = new InitialContext();<br /> UserTransaction txn = (UserTransaction)ctx.lookup("UserTransaction");<br /> try {<br /> txn.begin();<br /> em.persist(trade);<br /> AcctData acct = em.find(AcctData.class, trade.getAcctId());<br /> double tradeValue = trade.getPrice() * trade.getShares();<br /> double currentBalance = acct.getBalance();<br /> if (trade.getAction().equals("BUY")) {<br /> acct.setBalance(currentBalance - tradeValue);<br /> } else {<br /> acct.setBalance(currentBalance + tradeValue);<br /> }<br /> txn.commit();<br /> } catch (Exception up) {<br /> txn.rollback();<br /> throw up;<br /> }<br /> }<br />}<br />
在Java企業版容器中的無狀態會話Bean中使用編程式事務模型時,你必須告訴容器你使用的是編程式事務。這可以通過使用@TransactionManagement標記和把事務類型設定為BEAN的方法實現。如果你不適用該標記,容器將假設你使用的是EJB3.0中預設的聲明式交易管理。當在無狀態會話bean環境之外的客戶層使用編程式事務模型時,你不需要設定事務類型。
Spring中的編程式事務模型
在Spring架構中,有兩種方式實現編程式事務模型,一種是通過Spring的TransactionTemplate,另一種是直接使用Spring平台的交易管理員。因為我不是一個匿名內部類和難度代碼的推崇者,所以我使用Spring中的第二種技術展示編程式事務模型。
Spring中至少有九種交易管理員。你可能最長使用的是DataSourceTransactionManager, HibernateTransactionManager, JpaTransactionManager和JtaTransactionManager。My Code樣本使用JPA,因此我將展示JtaTransactionManager的配置。
為了在Spring中配置JtaTransactionManager,直接使用springframework.orm.jpa.JpaTransactionManager類在應用程式上下文XML檔案中定義bean,並且添加一個到JPA實體管理器工廠bean的引用。然後,如果你的商務邏輯bean由Spring管理,把交易管理員注入到bean中,就像代碼清單4所示:
Listing 4. Defining the Spring JPA transaction manager
<bean id="transactionManager"<br /> class="org.springframework.orm.jpa.JpaTransactionManager"><br /> <property name="entityManagerFactory" ref="entityManagerFactory"/><br /></bean></p><p><bean id="tradingService" class="com.trading.service.TradingServiceImpl"><br /> <property name="txnManager" ref="transactionManager"/><br /></bean><br />
如果你的應用程式類不是由Spring管理,你可以在你的方法中通過調用Spring內容相關的getBean()方法得到一個到交易管理員的引用。
在原始碼中,你可以使用管理器得到一個事物,一旦完成了所有的更新,你可以調用commit()方法提交事務或者調用rollback()方法復原事務。清單5展示了該技術:
Listing 5. Using the Spring JPA transaction manager
public class TradingServiceImpl {<br /> @PersistenceContext(unitName="trading") EntityManager em;</p><p> JpaTransactionManager txnManager = null;<br /> public void setTxnManager(JpaTransactionManager mgr) {<br /> txnManager = mgr;<br /> }</p><p> public void processTrade(TradeData trade) throws Exception {<br /> TransactionStatus status =<br /> txnManager.getTransaction(new DefaultTransactionDefinition());<br /> try {<br /> em.persist(trade);<br /> AcctData acct = em.find(AcctData.class, trade.getAcctId());<br /> double tradeValue = trade.getPrice() * trade.getShares();<br /> double currentBalance = acct.getBalance();<br /> if (trade.getAction().equals("BUY")) {<br /> acct.setBalance(currentBalance - tradeValue);<br /> } else {<br /> acct.setBalance(currentBalance + tradeValue);<br /> }<br /> txnManager.commit(status);<br /> } catch (Exception up) {<br /> txnManager.rollback(status);<br /> throw up;<br /> }<br /> }<br />}<br />
注意在清單5中Spring架構和EJB3.0.在Spring中,通過調用交易管理員的getTransaction()方法獲得事務。匿名的DefaultTransactionDefinition 類包含了事務的細節和行為,包括事務名稱、隔離等級、傳播模式和事務逾時值等。在本例中我只是簡單的使用預設值,name值是Null 字元串,底層資料庫預設的隔離等級(通常是READ_COMMITTED),事務的傳播模式是PROPAGATION_REQUIRED和資料庫的預設超市值。注意commit()和rollback()方法由交易管理員調用,而不是事務本身。
聲明式事務模型
聲明式事務模型,又稱為容器管理的事物,是Java平台中最常用的事物模型。在該模型中,容器負責啟動、提交和復原事務。程式員只負責指定事務的行為。在本系列的第一篇文章中討論的事物缺陷大多都和聲明式事務模型相關。
Spring架構和EJB3.0都使用標記指定事務的行為,Spring使用@Transactional標記,而EJB3.0使用@TransactionAttribute標記。在使用聲明式事務模型時,在發生檢查異常時,容器不會自動復原事務。程式員必須檢查異常發生時在哪兒以及何時復原事務。在Spring架構中,你可以通過在@Transactional標記上使用rollbackFor屬性來指定;在EJB中,可以通過調用SessionContext的setRollbackOnly()方法指定。 清單6展示了EJB中聲明式事務模型的用法:
Listing 6. Declarative transactions using EJB 3.0
@Stateless<br />public class TradingServiceImpl implements TradingService {<br /> @PersistenceContext(unitName="trading") EntityManager em;<br /> @Resource SessionContext ctx;</p><p> @TransactionAttribute(TransactionAttributeType.REQUIRED)<br /> public void processTrade(TradeData trade) throws Exception {<br /> try {<br /> em.persist(trade);<br /> AcctData acct = em.find(AcctData.class, trade.getAcctId());<br /> double tradeValue = trade.getPrice() * trade.getShares();<br /> double currentBalance = acct.getBalance();<br /> if (trade.getAction().equals("BUY")) {<br /> acct.setBalance(currentBalance - tradeValue);<br /> } else {<br /> acct.setBalance(currentBalance + tradeValue);<br /> }<br /> } catch (Exception up) {<br /> ctx.setRollbackOnly();<br /> throw up;<br /> }<br /> }<br />}<br />
清單7展示了Spring中聲明式事務模型的使用:
Listing 7. Declarative transactions using Spring
public class TradingServiceImpl {<br /> @PersistenceContext(unitName="trading") EntityManager em;</p><p> @Transactional(propagation=Propagation.REQUIRED,<br /> rollbackFor=Exception.class)<br /> public void processTrade(TradeData trade) throws Exception {<br /> em.persist(trade);<br /> AcctData acct = em.find(AcctData.class, trade.getAcctId());<br /> double tradeValue = trade.getPrice() * trade.getShares();<br /> double currentBalance = acct.getBalance();<br /> if (trade.getAction().equals("BUY")) {<br /> acct.setBalance(currentBalance - tradeValue);<br /> } else {<br /> acct.setBalance(currentBalance + tradeValue);<br /> }<br /> }<br />}<br />
事務屬性
除了復原指令,你還必須指定事務屬性,它定義了事務的行為。無論使用Spring架構還是EJB,Java平台都支援6種事務屬性:
Required
Mandatory
RequiresNew
Supports
NotSupported
Never
在描述每種事務屬性時,我假設有一個正在應用事務屬性的方法methodA()。
當在methodA()方法上指定事務屬性Required時,如果在已有事務中調用方法methodA(),那麼將使用已有事務。否則,methodA()將啟動一個新的事務,如果事務由方法methodA()啟動,那它必須由方法methodA()來終結(提交或復原)。這是最常使用的事物屬性,也是Spring架構和EJB3.0中的預設事務屬性。不行的是,它在很多情況下北被錯誤的使用,導致了資料完整性和一致性問題。在後續文章中將要討論的每種事務策略中,我將詳細討論該事務屬性。
如果在方法methodA()上指定了事務屬性Mandatory並且在已有事務中調用它,那麼將使用已有事務。但是,如果在沒有事務的情況下調用methodA(),將會拋出TransactionRequiredException異常,表明在調用發放methodA()之前必須存在事務。該事務屬性用在本文後面將要討論的Client Orchestration事務策略中。
我發現Supports屬性是另一個程式員沒有完全理解的事物屬性。如果在方法methodA()上指定了該屬性,當methodA()在已有事務中調用時,以後事務將被使用。如果methodA()沒有在事務環境中調用,那麼將不會啟動事務。該屬性主要用於資料庫中的唯讀操作,那麼在這種情況下為什麼不直接指定NotSupported代替它呢?畢竟,NotSupported可以確保該方法不在事務中執行。答案很簡單,在已有事務中調用查詢操作將會讀取交易記錄(也就是已被更新但未提交的資料),但是在非事務環境下只能讀取表中未被改變的資料。例如,例如,你向TRADE表中插入一條交易記錄,接著擷取所有的交易記錄列表,那麼未提交的交易記錄將出現在列表中。但是,如果你使用類似NotSupported的屬性,將會使查詢從表中而不是交易記錄中讀取記錄。在前面的例子中,將看不到新插入但為提交的交易記錄。這可能不是一件壞事情,取決於你的使用方式和商務邏輯。
NotSupported屬性工作表明在指定的方法上將不使用或者啟動事務,不管是不是已經存在一個事物。如果在methodA()上指定了NotSupported屬性並且methodA()在已有事務中調用,在method()結束之前,已有事務將被掛起,methodA()結束時,已有事務被恢複。只有很少的情況下使用該屬性,主要和調用資料庫中的預存程序有關。如果你在一個已有事務中調用預存程序並且預存程序中包含BEGIN TRANS,或者在Sybase中,運行在unchained模式,將會拋出一個異常表明不能在一個已有事務中啟動新的事務(也就是說不支援嵌套事務)。幾乎所有的把JTS作為JTA預設實現的容器---而不是java平台不支援嵌套事務。如果你不能改變資料庫中的預存程序,你可以指定NotSupported來掛起已有事務從而避免一場發生。結果是,你不在具有更新的原子性,作為平衡,它可以把你帶出困難境地。
Never屬性或許是所有屬性中最有趣的一個,除了一個重要的不同外,它和NotSupported一樣:當在已有事務中調用一個被指定為NotSupported的方法時,將會拋出一個異常表明該方法不允許有事務。我遇到的唯一需要使用它的情況是在單元測試中,當你調用一個特殊的方法時,它提供了一種簡單快捷地驗證是否已經存在事務的方法。
事務策略
本文中描述的事務模型形成了將要介紹的事務策略的基礎,在構建事務策略之前理解模型之間的差別以及它們怎麼工作是非常重要的。在大多數商務應用程式中都可以使用的主要事務策略是:
- Client Orchestration transaction strategy
- API Layer transaction strategy
- High Concurrency transaction strategy
- High-Speed Processing transaction strategy
在這兒將簡要討論下每種事務策略,在本系列後續文章中將詳細討論每一種事務策略。