前沿(筆者加):事務(Transaction)是每一個與數(shù)據(jù)庫有關的系統(tǒng)開發(fā)與設計人員都會接觸到的東西,在Java中,傳統(tǒng)的直接使用JDBC的事務開始、提交、回滾的方式已經(jīng)隨著各種應用開發(fā)框架(尤其是Spring)的出現(xiàn)變得對開發(fā)人員更加簡單,開發(fā)人員直接將事務的處理交由spring處理,只關心具體業(yè)務,但是實際上Spring對事務的控制有自己的一套體系(筆者在Spring配置中transactionAttributes的意義一文中,曾介紹spring關于事務的部分配置及說明),如果開發(fā)設計人員對其沒有深入了解,很容易出現(xiàn)事務沒有正常提交,甚至出現(xiàn)事務不提交的情況,今天發(fā)現(xiàn)本文對于事務及其在Spring、EJB中的應用,詳細介紹了如何正確使用事務,希望可以對讀者有幫助。
事務策略: 了解事務陷阱
在 Java 平臺中實現(xiàn)事務時要注意的常見錯誤
級別: 中級
Mark Richards, 主管和高級技術架構師, Collaborative Consulting, LLC
2009 年 3 月 06 日
事務處理的目標應該是實現(xiàn)數(shù)據(jù)的高度完整性和一致性。本文是為 Java 平臺開發(fā)有效事務策略 系列文章 的第一篇,介紹了一些妨礙您實現(xiàn)此目標的常見事務陷阱。本系列作者 Mark Richards 通過使用 Spring Framework 和企業(yè) JavaBeans(Enterprise JavaBeans,EJB)3.0 規(guī)范中的代碼示例解釋了這些極其常見的錯誤。
在應用程序中使用事務常常是為了維護高度的數(shù)據(jù)完整性和一致性。如果不關心數(shù)據(jù)的質(zhì)量,就不必使用事務。畢竟,Java 平臺中的事務支持會降低性能,引發(fā)鎖定問題和數(shù)據(jù)庫并發(fā)性問題,而且會增加應用程序的復雜性。
但是不關心事務的開發(fā)人員就會遇到麻煩。幾乎所有與業(yè)務相關的應用程序都需要高度的數(shù)據(jù)質(zhì)量。金融投資行業(yè)在失敗的交易上浪費數(shù)百億美元,不好的數(shù)據(jù)是導致這種結果的第二大因素(請參閱 參考資料)。盡然缺少事務支持只是導致壞數(shù)據(jù)的一個因素(但是是主要的因素),但是完全可以這樣認為,在金融投資行業(yè)浪費掉數(shù)十億美元是由于缺少事務支持或事務支持不充分。
忽略事務支持是導致問題的另一個原因。我常常聽到 “我們的應用程序中不需要事務支持,因為這些應用程序從來不會失敗” 之類的說法。是的,我知道有些應用程序極少或從來不會拋出異常。這些應用程序基于編寫良好的代碼、編寫良好的驗證例程,并經(jīng)過了充分的測試,有代碼覆蓋支持,可以避免性能損耗和與事務處理有關的復雜性。這種類型的應用程序只需考慮事務支持的一個特性:原子性。原子性確保所有更新被當作一個單獨的單元,要么全部提交,要么回滾。但是回滾或同時更新不是事務支持的惟一方面。另一方面,隔離性 將確保某一工作單元獨立于其他工作單元。沒有適當?shù)氖聞崭綦x性,其他工作單元就可以訪問某一活動工作單元所做的更新,即使該工作單元還未完成。這樣,就會基于部分數(shù)據(jù)作出業(yè)務決策,而這會導致失敗的交易或產(chǎn)生其他負面(或代價昂貴的)結果。
因此,考慮到壞數(shù)據(jù)的高成本和負面影響,以及事務的重要性(和必須性)這些基本常識,您需要使用事務處理并學習如何處理可能出現(xiàn)的問題。您在應用程序中添加事務支持后常常會出現(xiàn)很多問題。事務在 Java 平臺中并不總是如預想的那樣工作。本文會探討其中的原因。我將借助代碼示例,介紹一些我在該領域中不斷看到的和經(jīng)歷的常見事務陷阱,大部分是在生產(chǎn)環(huán)境中。
雖然本文中的大多數(shù)代碼示例使用的是 Spring Framework(version 2.5),但事務概念與 EJB 3.0 規(guī)范中的是相同的。在大多數(shù)情況下,用 EJB 3.0 規(guī)范中的 _cnnew1@TransactionAttribute 注釋替換 Spring Framework @Transactional 注釋即可。如果這兩種框架使用了不同的概念和技術,我將同時給出 Spring Framework 和 EJB 3.0 源代碼示例。
本地事務陷阱
最好先從最簡單的場景開始,即使用本地事務,一般也稱為數(shù)據(jù)庫事務。在數(shù)據(jù)庫持久性的早期(例如 JDBC),我們一般會將事務處理委派給數(shù)據(jù)庫。畢竟這是數(shù)據(jù)庫應該做的。本地事務很適合執(zhí)行單一插入、更新或刪除語句的邏輯工作單元(LUW)。例如,考慮清單 1 中的簡單 JDBC 代碼,它向 TRADE 表插入一份股票交易訂單:
清單 1. 使用 JDBC 的簡單數(shù)據(jù)庫插入
view plaincopy to clipboardprint?
@Stateless
public class TradingServiceImpl implements TradingService {
@Resource SessionContext ctx;
@Resource(mappedName="java:jdbc/tradingDS") DataSource ds;
public long insertTrade(TradeData trade) throws Exception {
Connection dbConnection = ds.getConnection();
try {
Statement sql = dbConnection.createStatement();
String stmt =
"INSERT INTO TRADE (ACCT_ID, SIDE, SYMBOL, SHARES, PRICE, STATE)"
+ "VALUES ("
+ trade.getAcct() + "','"
+ trade.getAction() + "','"
+ trade.getSymbol() + "',"
+ trade.getShares() + ","
+ trade.getPrice() + ",'"
+ trade.getState() + "')";
sql.executeUpdate(stmt, Statement.RETURN_GENERATED_KEYS);
ResultSet rs = sql.getGeneratedKeys();
if (rs.next()) {
return rs.getBigDecimal(1).longValue();
} else {
throw new Exception("Trade Order Insert Failed");
}
} finally {
if (dbConnection != null) dbConnection.close();
}
}
}
@Stateless
public class TradingServiceImpl implements TradingService {
@Resource SessionContext ctx;
@Resource(mappedName="java:jdbc/tradingDS") DataSource ds;
public long insertTrade(TradeData trade) throws Exception {
Connection dbConnection = ds.getConnection();
try {
Statement sql = dbConnection.createStatement();
String stmt =
"INSERT INTO TRADE (ACCT_ID, SIDE, SYMBOL, SHARES, PRICE, STATE)"
+ "VALUES ("
+ trade.getAcct() + "','"
+ trade.getAction() + "','"
+ trade.getSymbol() + "',"
+ trade.getShares() + ","
+ trade.getPrice() + ",'"
+ trade.getState() + "')";
sql.executeUpdate(stmt, Statement.RETURN_GENERATED_KEYS);
ResultSet rs = sql.getGeneratedKeys();
if (rs.next()) {
return rs.getBigDecimal(1).longValue();
} else {
throw new Exception("Trade Order Insert Failed");
}
} finally {
if (dbConnection != null) dbConnection.close();
}
}
}
清單 1 中的 JDBC 代碼沒有包含任何事務邏輯,它只是在數(shù)據(jù)庫中保存 TRADE 表中的交易訂單。在本例中,數(shù)據(jù)庫處理事務邏輯。
在 LUW 中,這是一個不錯的單個數(shù)據(jù)庫維護操作。但是如果需要在向數(shù)據(jù)庫插入交易訂單的同時更新帳戶余款呢?如清單 2 所示:
清單 2. 在同一方法中執(zhí)行多次表更新
view plaincopy to clipboardprint?
public TradeData placeTrade(TradeData trade) throws Exception {
try {
insertTrade(trade);
updateAcct(trade);
return trade;
} catch (Exception up) {
//log the error
throw up;
}
}
public TradeData placeTrade(TradeData trade) throws Exception {
try {
insertTrade(trade);
updateAcct(trade);
return trade;
} catch (Exception up) {
//log the error
throw up;
}
}
在本例中,insertTrade() 和 updateAcct() 方法使用不帶事務的標準 JDBC 代碼。insertTrade() 方法結束后,數(shù)據(jù)庫保存(并提交了)交易訂單。如果 updateAcct() 方法由于任意原因失敗,交易訂單仍然會在 placeTrade() 方法結束時保存在 TRADE 表內(nèi),這會導致數(shù)據(jù)庫出現(xiàn)不一致的數(shù)據(jù)。如果 placeTrade() 方法使用了事務,這兩個活動都會包含在一個 LUW 中,如果帳戶更新失敗,交易訂單就會回滾。
隨著 Java 持久性框架的不斷普及,如 Hibernate、TopLink 和 Java 持久性 API(Java Persistence API,JPA),我們很少再會去編寫簡單的 JDBC 代碼。更常見的情況是,我們使用更新的對象關系映射(ORM)框架來減輕工作,即用幾個簡單的方法調(diào)用替換所有麻煩的 JDBC 代碼。例如,要插入 清單 1 中 JDBC 代碼示例的交易訂單,使用帶有 JPA 的 Spring Framework,就可以將 TradeData 對象映射到 TRADE 表,并用清單 3 中的 JPA 代碼替換所有 JDBC 代碼:
清單 3. 使用 JPA 的簡單插入
view plaincopy to clipboardprint?
public class TradingServiceImpl {
@PersistenceContext(unitName="trading") EntityManager em;
public long insertTrade(TradeData trade) throws Exception {
em.persist(trade);
return trade.getTradeId();
}
}
public class TradingServiceImpl {
@PersistenceContext(unitName="trading") EntityManager em;
public long insertTrade(TradeData trade) throws Exception {
em.persist(trade);
return trade.getTradeId();
}
}
注意,清單 3 在 EntityManager 上調(diào)用了 persist() 方法來插入交易訂單。很簡單,是吧?其實不然。這段代碼不會像預期那樣向 TRADE 表插入交易訂單,也不會拋出異常。它只是返回一個值 0 作為交易訂單的鍵,而不會更改數(shù)據(jù)庫。這是事務處理的主要陷阱之一:基于 ORM 的框架需要一個事務來觸發(fā)對象緩存與數(shù)據(jù)庫之間的同步。這通過一個事務提交完成,其中會生成 SQL 代碼,數(shù)據(jù)庫會執(zhí)行需要的操作(即插入、更新、刪除)。沒有事務,就不會觸發(fā) ORM 去生成 SQL 代碼和保存更改,因此只會終止方法 — 沒有異常,沒有更新。如果使用基于 ORM 的框架,就必須利用事務。您不再依賴數(shù)據(jù)庫來管理連接和提交工作。
這些簡單的示例應該清楚地說明,為了維護數(shù)據(jù)完整性和一致性,必須使用事務。不過對于在 Java 平臺中實現(xiàn)事務的復雜性和陷阱而言,這些示例只是涉及了冰山一角。
Spring Framework @Transactional 注釋陷阱
您將測試 清單 3 中的代碼,發(fā)現(xiàn) persist() 方法在沒有事務的情況下不能工作。因此,您通過簡單的網(wǎng)絡搜索查看幾個鏈接,發(fā)現(xiàn)如果使用 Spring Framework,就需要使用 @Transactional 注釋。于是您在代碼中添加該注釋,如清單 4 所示:
清單 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();
}
}
現(xiàn)在重新測試代碼,您發(fā)現(xiàn)上述方法仍然不能工作。問題在于您必須告訴 Spring Framework,您正在對事務管理應用注釋。除非您進行充分的單元測試,否則有時候很難發(fā)現(xiàn)這個陷阱。這通常只會導致開發(fā)人員在 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 注釋時沒有指定任何額外的注釋參數(shù)。我發(fā)現(xiàn)許多開發(fā)人員在使用 @Transactional 注釋時并沒有花時間理解它的作用。例如,像我一樣在清單 4 中單獨使用 @Transactional 注釋時,事務傳播模式被設置成什么呢?只讀標志被設置成什么呢?事務隔離級別的設置是怎樣的?更重要的是,事務應何時回滾工作?理解如何使用這個注釋對于確保在應用程序中獲得合適的事務支持級別非常重要。回答我剛才提出的問題:在單獨使用不帶任何參數(shù)的 @Transactional 注釋時,傳播模式要設置為 REQUIRED,只讀標志設置為 false,事務隔離級別設置為 READ_COMMITTED,而且事務不會針對受控異常(checked exception)回滾。
@Transactional 只讀標志陷阱
我在工作中經(jīng)常碰到的一個常見陷阱是 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...
}
當執(zhí)行清單 5 中的 insertTrade() 方法時,猜一猜會得到下面哪一種結果:
拋出一個只讀連接異常
正確插入交易訂單并提交數(shù)據(jù)
什么也不做,因為傳播級別被設置為 SUPPORTS
是哪一個呢?正確答案是 B。交易訂單會被正確地插入到數(shù)據(jù)庫中,即使只讀標志被設置為 true,且事務傳播模式被設置為 SUPPORTS。但這是如何做到的呢?由于傳播模式被設置為 SUPPORTS,所以不會啟動任何事物,因此該方法有效地利用了一個本地(數(shù)據(jù)庫)事務。只讀標志只在事務啟動時應用。在本例中,因為沒有啟動任何事務,所以只讀標志被忽略。
如果是這樣的話,清單 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...
}
執(zhí)行清單 6 中的 insertTrade() 方法會得到下面哪一種結果呢:
拋出一個只讀連接異常
正確插入交易訂單并提交數(shù)據(jù)
什么也不做,因為只讀標志被設置為 true
根據(jù)前面的解釋,這個問題應該很好回答。正確的答案是 A。會拋出一個異常,表示您正在試圖對一個只讀連接執(zhí)行更新。因為啟動了一個事務(REQUIRED),所以連接被設置為只讀。毫無疑問,在試圖執(zhí)行 SQL 語句時,您會得到一個異常,告訴您該連接是一個只讀連接。
關于只讀標志很奇怪的一點是:要使用它,必須啟動一個事務。如果只是讀取數(shù)據(jù),需要事務嗎?答案是根本不需要。啟動一個事務來執(zhí)行只讀操作會增加處理線程的開銷,并會導致數(shù)據(jù)庫發(fā)生共享讀取鎖定(具體取決于使用的數(shù)據(jù)庫類型和設置的隔離級別)。總的來說,在獲取基于 JDBC 的 Java 持久性時,使用只讀標志有點毫無意義,并會啟動不必要的事務而增加額外的開銷。
使用基于 ORM 的框架會怎樣呢?按照上面的測試,如果在結合使用 JPA 和 Hibernate 時調(diào)用 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() 方法會得到下面哪一種結果:
拋出一個只讀連接異常
正確插入交易訂單并提交數(shù)據(jù)
什么也不做,因為 readOnly 標志被設置為 true
正確的答案是 B。交易訂單會被準確無誤地插入數(shù)據(jù)庫中。請注意,上一示例表明,在使用 REQUIRED 傳播模式時,會拋出一個只讀連接異常。使用 JDBC 時是這樣。使用基于 ORM 的框架時,只讀標志只是對數(shù)據(jù)庫的一個提示,并且一條基于 ORM 框架的指令(本例中是 Hibernate)將對象緩存的 flush 模式設置為 NEVER,表示在這個工作單元中,該對象緩存不應與數(shù)據(jù)庫同步。不過,REQUIRED 傳播模式會覆蓋所有這些內(nèi)容,允許事務啟動并工作,就好像沒有設置只讀標志一樣。
這令我想到了另一個我經(jīng)常碰到的主要陷阱。閱讀了前面的所有內(nèi)容后,您認為如果只對 @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() 方法會執(zhí)行以下哪一種操作?
啟動一個事務,獲取交易訂單,然后提交事務
獲取交易訂單,但不啟動事務
正確的答案是 A。一個事務會被啟動并提交。不要忘了,@Transactional 注釋的默認傳播模式是 REQUIRED。這意味著事務會在不必要的情況下啟動。根據(jù)使用的數(shù)據(jù)庫,這會引起不必要的共享鎖,可能會使數(shù)據(jù)庫中出現(xiàn)死鎖的情況。此外,啟動和停止事務將消耗不必要的處理時間和資源??偟膩碚f,在使用基于 ORM 的框架時,只讀標志基本上毫無用處,在大多數(shù)情況下會被忽略。但如果您堅持使用它,請記得將傳播模式設置為 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);
}
另外,在執(zhí)行讀取操作時,避免使用 @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 事務屬性都會得到不好的結果并導致數(shù)據(jù)損壞和不一致。REQUIRES_NEW 事務屬性總是會在啟動方法時啟動一個新的事務。許多開發(fā)人員都錯誤地使用 REQUIRES_NEW 屬性,認為它是確保事務啟動的正確方法。考慮清單 11 中的兩個方法:
清單 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 中的兩個方法都是公共方法,這意味著它們可以單獨調(diào)用。當使用 REQUIRES_NEW 屬性的幾個方法通過服務間通信或編排在同一邏輯工作單元內(nèi)調(diào)用時,該屬性就會出現(xiàn)問題。例如,假設在清單 11 中,您可以獨立于一些用例中的任何其他方法來調(diào)用 updateAcct() 方法,但也有在 insertTrade() 方法中調(diào)用 updateAcct() 方法的情況。現(xiàn)在如果調(diào)用 updateAcct() 方法后拋出異常,交易訂單就會回滾,但帳戶更新將會提交給數(shù)據(jù)庫,如清單 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!
...
}
之所以會發(fā)生這種情況是因為 updateAcct() 方法中啟動了一個新事務,所以在 updateAcct() 方法結束后,事務將被提交。使用 REQUIRES_NEW 事務屬性時,如果存在現(xiàn)有事務上下文,當前的事務會被掛起并啟動一個新事務。方法結束后,新的事務被提交,原來的事務繼續(xù)執(zhí)行。
由于這種行為,只有在被調(diào)用方法中的數(shù)據(jù)庫操作需要保存到數(shù)據(jù)庫中,而不管覆蓋事務的結果如何時,才應該使用 REQUIRES_NEW 事務屬性。比如,假設嘗試的所有股票交易都必須被記錄在一個審計數(shù)據(jù)庫中。出于驗證錯誤、資金不足或其他原因,不管交易是否失敗,這條信息都需要被持久化。如果沒有對審計方法使用 REQUIRES_NEW 屬性,審計記錄就會連同嘗試執(zhí)行的交易一起回滾。使用 REQUIRES_NEW 屬性可以確保不管初始事務的結果如何,審計數(shù)據(jù)都會被保存。這里要注意的一點是,要始終使用 MANDATORY 或 REQUIRED 屬性,而不是 REQUIRES_NEW,除非您有足夠的理由來使用它,類似審計示例中的那些理由。
事務回滾陷阱
我將最常見的事務陷阱留到最后來講。遺憾的是,我在生產(chǎn)代碼中多次遇到這個錯誤。我首先從 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),那么交易訂單會保存在數(shù)據(jù)庫中嗎?還是整個邏輯工作單元將執(zhí)行回滾?答案出乎意料:根據(jù)受控異常(不管是在 Spring Framework 中還是在 EJB 中),事務會提交它還未提交的所有工作。使用清單 13,這意味著,如果在執(zhí)行 updateAcct() 方法期間拋出受控異常,就會保存交易訂單,但不會更新帳戶來反映交易情況。
這可能是在使用事務時出現(xiàn)的主要數(shù)據(jù)完整性和一致性問題了。運行時異常(即非受控異常)自動強制執(zhí)行整個邏輯工作單元的回滾,但受控異常不會。因此,清單 13 中的代碼從事務角度來說毫無用處;盡管看上去它使用事務來維護原子性和一致性,但事實上并沒有。
盡管這種行為看起來很奇怪,但這樣做自有它的道理。首先,不是所有受控異常都是不好的;它們可用于事件通知或根據(jù)某些條件重定向處理。但更重要的是,應用程序代碼會對某些類型的受控異常采取糾正操作,從而使事務全部完成。例如,考慮下面一種場景:您正在為在線書籍零售商編寫代碼。要完成圖書的訂單,您需要將電子郵件形式的確認函作為訂單處理的一部分發(fā)送。如果電子郵件服務器關閉,您將發(fā)送某種形式的 SMTP 受控異常,表示郵件無法發(fā)送。如果受控異常引起自動回滾,整個圖書訂單就會由于電子郵件服務器的關閉全部回滾。通過禁止自動回滾受控異常,您可以捕獲該異常并執(zhí)行某種糾正操作(如向掛起隊列發(fā)送消息),然后提交剩余的訂單。
使用 Declarative 事務模式(本系列的第 2 部分將進行更加詳細的描述)時,必須指定容器或框架應該如何處理受控異常。在 Spring Framework 中,通過 @Transactional 注釋中的 rollbackFor 參數(shù)進行指定,如清單 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 參數(shù)。這個參數(shù)接受一個單一異常類或一組異常類,您也可以使用 rollbackForClassName 參數(shù)將異常的名稱指定為 Java String 類型。還可以使用此屬性的相反形式(noRollbackFor)指定除某些異常以外的所有異常應該強制回滾。通常大多數(shù)開發(fā)人員指定 Exception.class 作為值,表示該方法中的所有異常應該強制回滾。
在回滾事務這一點上,EJB 的工作方式與 Spring Framework 稍微有點不同。EJB 3.0 規(guī)范中的 @TransactionAttribute 注釋不包含指定回滾行為的指令。必須使用 SessionContext.setRollbackOnly() 方法將事務標記為執(zhí)行回滾,如清單 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;
}
}
調(diào)用 setRollbackOnly() 方法后,就不能改變主意了;惟一可能的結果是在啟動事務的方法完成后回滾事務。本系列后續(xù)文章中描述的事務策略將介紹何時、何處使用回滾指令,以及何時使用 REQUIRED 與 MANDATORY 事務屬性。
結束語
用于在 Java 平臺中實現(xiàn)事務的代碼不是太復雜;但是,如何使用以及如何配置它就有一些復雜了。在 Java 平臺中實現(xiàn)事務支持有許多陷阱(包括一些我未在本文中討論的、不是很常見的陷阱)。大多數(shù)陷阱最大的問題是,不會有任何編譯器警告或運行時錯誤告訴您事務實現(xiàn)是不正確的。而且,與本文開頭的 “遲做總比不做好” 部分的內(nèi)容相反,實現(xiàn)事務支持不僅僅是一個編碼工作。開發(fā)一個完整的事務策略涉及大量的設計工作。事務策略 系列的其余部分將指導您如何設計針對從簡單應用程序到高性能事務處理用例的有效事務策略。
參考資料
學習
您可以參閱本文在 developerWorks 全球網(wǎng)站上的 英文原文。
Straight Through Processing for Financial Service Firms (Hal McIntyre,Summit Group 出版社,2004):了解在金融服務事務處理中,更多有關造成壞數(shù)據(jù)的原因及其成本的信息。
Java Transaction Design Strategies (Mark Richards,C4Media 出版,2006):本書深入討論了 Java 平臺中的事務。
Java Transaction Processing (Mark Little,Prentice Hall,2004):本書是另一本比較好的關于事務的參考書。
第 9 章. 事務管理:在 Spring Framework 2.5 文檔的這部分中,可以找到更多有關 Spring 事務處理的信息。
Enterprise JavaBeans 3.0 資源站點:在此可以找到有關 EJB 3.0 規(guī)范的文檔。
瀏覽 技術書店,查閱有關本文所述主題及其他技術主題的圖書。
developerWorks Java 技術專區(qū):提供了幾百篇有關 Java 編程的各個方面的文章。
本文出處:http://www.ibm.com/developerworks/cn/java/j-ts1.html