程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 事務策略: 了解事務陷阱-在Java平台中實現事務時要注意的常見錯誤

事務策略: 了解事務陷阱-在Java平台中實現事務時要注意的常見錯誤

編輯:關於JAVA

簡介:事務處理的目標應該是實現數據的高度完整性和一致性。本文是為 Java 平台開發有效事務策 略 系列文章 的第一篇,介紹了一些妨礙您實現此目標的常見事務陷阱。本系列作者 Mark Richards 通 過使用 Spring Framework 和企業 JavaBeans(Enterprise JavaBeans,EJB)3.0 規范中的代碼示例解 釋了這些極其常見的錯誤。

在應用程序中使用事務常常是為了維護高度的數據完整性和一致性。如果不關心數據的質量,就不必 使用事務。畢竟,Java 平台中的事務支持會降低性能,引發鎖定問題和數據庫並發性問題,而且會增加 應用程序的復雜性。

關於本系列

事務提高了數據的質量、完整性和一致性,使應用程序更健壯。在 Java 應用程序中實現成功的事務 處理不是一件容易的事,設計和編碼幾乎一樣重要。在這份新的 系列文章 中,Mark Richards 將帶領您 設計一個有效的事務策略,適合從簡單應用程序到高性能事務處理等各種用例。

但是不關心事務的開發人員就會遇到麻煩。幾乎所有與業務相關的應用程序都需要高度的數據質量。 金融投資行業在失敗的交易上浪費數百億美元,不好的數據是導致這種結果的第二大因素。盡然缺少事務 支持只是導致壞數據的一個因素(但是是主要的因素),但是完全可以這樣認為,在金融投資行業浪費掉 數十億美元是由於缺少事務支持或事務支持不充分。

忽略事務支持是導致問題的另一個原因。我常常聽到 “我們的應用程序中不需要事務支持,因為這些 應用程序從來不會失敗” 之類的說法。是的,我知道有些應用程序極少或從來不會拋出異常。這些應用 程序基於編寫良好的代碼、編寫良好的驗證例程,並經過了充分的測試,有代碼覆蓋支持,可以避免性能 損耗和與事務處理有關的復雜性。這種類型的應用程序只需考慮事務支持的一個特性:原子性。原子性確 保所有更新被當作一個單獨的單元,要麼全部提交,要麼回滾。但是回滾或同時更新不是事務支持的惟一 方面。另一方面,隔離性 將確保某一工作單元獨立於其他工作單元。沒有適當的事務隔離性,其他工作 單元就可以訪問某一活動工作單元所做的更新,即使該工作單元還未完成。這樣,就會基於部分數據作出 業務決策,而這會導致失敗的交易或產生其他負面(或代價昂貴的)結果。

遲做總比不做好

我是在 2000 年年初開始關注事務處理問題的,當時我正在研究一個客戶端站點,我發現項目計劃中 有一項內容優先於系統測試任務。它稱為實現事務支持。當然,在某個主要應用程序差不多准備好進行系 統測試時,給它添加事務支持是非常簡單的。遺憾的是,這種方法實在太普通。至少這個項目(與大多數 項目不同)確實 實現了事務支持,盡管是在開發周期快結束時。

因此,考慮到壞數據的高成本和負面影響,以及事務的重要性(和必須性)這些基本常識,您需要使 用事務處理並學習如何處理可能出現的問題。您在應用程序中添加事務支持後常常會出現很多問題。事務 在 Java 平台中並不總是如預想的那樣工作。本文會探討其中的原因。我將借助代碼示例,介紹一些我在 該領域中不斷看到的和經歷的常見事務陷阱,大部分是在生產環境中。

雖然本文中的大多數代碼示例使用的是 Spring Framework(version 2.5),但事務概念與 EJB 3.0 規范中的是相同的。在大多數情況下,用 EJB 3.0 規范中的 _cnnew1@TransactionAttribute 注釋替換 Spring Framework @Transactional 注釋即可。如果這兩種框架使用了不同的概念和技術,我將同時給出 Spring Framework 和 EJB 3.0 源代碼示例。

本地事務陷阱

最好先從最簡單的場景開始,即使用本地事務,一般也稱為數據庫事務。在數據庫持久性的早期(例 如 JDBC),我們一般會將事務處理委派給數據庫。畢竟這是數據庫應該做的。本地事務很適合執行單一 插入、更新或刪除語句的邏輯工作單元(LUW)。例如,考慮清單 1 中的簡單 JDBC 代碼,它向 TRADE 表插入一份股票交易訂單:

清單 1. 使用 JDBC 的簡單數據庫插入

@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 代碼沒有包含任何事務邏輯,它只是在數據庫中保存 TRADE 表中的交易訂單。在 本例中,數據庫處理事務邏輯。

在 LUW 中,這是一個不錯的單個數據庫維護操作。但是如果需要在向數據庫插入交易訂單的同時更新 帳戶余款呢?如清單 2 所示:

清單 2. 在同一方法中執行多次表更新

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() 方法結束後,數據庫保存(並提交了)交易訂單。如果 updateAcct() 方法由於任意原因失敗,交易訂單 仍然會在 placeTrade() 方法結束時保存在 TRADE 表內,這會導致數據庫出現不一致的數據。如果 placeTrade() 方法使用了事務,這兩個活動都會包含在一個 LUW 中,如果帳戶更新失敗,交易訂單就會 回滾。

隨著 Java 持久性框架的不斷普及,如 Hibernate、TopLink 和 Java 持久性 API(Java Persistence API,JPA),我們很少再會去編寫簡單的 JDBC 代碼。更常見的情況是,我們使用更新的對 象關系映射(ORM)框架來減輕工作,即用幾個簡單的方法調用替換所有麻煩的 JDBC 代碼。例如,要插 入 清單 1 中 JDBC 代碼示例的交易訂單,使用帶有 JPA 的 Spring Framework,就可以將 TradeData 對象映射到 TRADE 表,並用清單 3 中的 JPA 代碼替換所有 JDBC 代碼:

清單 3. 使用 JPA 的簡單插入

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

注意,清單 3 在 EntityManager 上調用了 persist() 方法來插入交易訂單。很簡單,是吧?其實不 然。這段代碼不會像預期那樣向 TRADE 表插入交易訂單,也不會拋出異常。它只是返回一個值 0 作為交 易訂單的鍵,而不會更改數據庫。這是事務處理的主要陷阱之一:基於 ORM 的框架需要一個事務來觸發 對象緩存與數據庫之間的同步。這通過一個事務提交完成,其中會生成 SQL 代碼,數據庫會執行需要的 操作(即插入、更新、刪除)。沒有事務,就不會觸發 ORM 去生成 SQL 代碼和保存更改,因此只會終止 方法 — 沒有異常,沒有更新。如果使用基於 ORM 的框架,就必須利用事務。您不再依賴數據庫來管理 連接和提交工作。

這些簡單的示例應該清楚地說明,為了維護數據完整性和一致性,必須使用事務。不過對於在 Java 平台中實現事務的復雜性和陷阱而言,這些示例只是涉及了冰山一角。

Spring Framework @Transactional 注釋陷阱

您將測試 清單 3 中的代碼,發現 persist() 方法在沒有事務的情況下不能工作。因此,您通過簡單 的網絡搜索查看幾個鏈接,發現如果使用 Spring Framework,就需要使用 @Transactional 注釋。於是 您在代碼中添加該注釋,如清單 4 所示:

清單 4. 使用 @Transactional 注釋

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 配置文件中添加以下代碼行:

<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

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

當執行清單 5 中的 insertTrade() 方法時,猜一猜會得到下面哪一種結果:

拋出一個只讀連接異常

正確插入交易訂單並提交數據

什麼也不做,因為傳播級別被設置為 SUPPORTS

是哪一個呢?正確答案是 B。交易訂單會被正確地插入到數據庫中,即使只讀標志被設置為 true,且 事務傳播模式被設置為 SUPPORTS。但這是如何做到的呢?由於傳播模式被設置為 SUPPORTS,所以不會啟 動任何事物,因此該方法有效地利用了一個本地(數據庫)事務。只讀標志只在事務啟動時應用。在本例 中,因為沒有啟動任何事務,所以只讀標志被忽略。

如果是這樣的話,清單 6 中的 @Transactional 注釋在設置了只讀標志且傳播模式被設置為 REQUIRED 時,它的作用是什麼呢?

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

@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

@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

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

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

A.啟動一個事務,獲取交易訂單,然後提交事務

B.獲取交易訂單,但不啟動事務

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

清單 9. 使用只讀標志和 SUPPORTS 傳播模式進行選擇操作

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

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

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

public TradeData getTrade(long tradeId) throws Exception {
return em.find(TradeData.class, tradeId);
}

REQUIRES_NEW 事務屬性陷阱

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

清單 11. 使用 REQUIRES_NEW 事務屬性

@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 事務屬性的多次更新

@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. 沒有回滾支持

@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 受控異常,表示郵件無法發送。如果受控異常引起自動回滾,整個圖書訂單就會由 於電子郵件服務器的關閉全部回滾。通過禁止自動回滾受控異常,您可以捕獲該異常並執行某種糾正操作 (如向掛起隊列發送消息),然後提交剩余的訂單。

使用 Declarative 事務模式(本系列的第 2 部分將進行更加詳細的描述)時,必須指定容器或框架 應該如何處理受控異常。在 Spring Framework 中,通過 @Transactional 注釋中的 rollbackFor 參數 進行指定,如清單 14 所示:

清單 14. 添加事務回滾支持 — Spring

@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

@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 事務屬性。

結束語

用於在 Java 平台中實現事務的代碼不是太復雜;但是,如何使用以及如何配置它就有一些復雜了。 在 Java 平台中實現事務支持有許多陷阱(包括一些我未在本文中討論的、不是很常見的陷阱)。大多數 陷阱最大的問題是,不會有任何編譯器警告或運行時錯誤告訴您事務實現是不正確的。而且,與本文開頭 的 “遲做總比不做好” 部分的內容相反,實現事務支持不僅僅是一個編碼工作。開發一個完整的事務策 略涉及大量的設計工作。事務策略 系列的其余部分將指導您如何設計針對從簡單應用程序到高性能事務 處理用例的有效事務策略。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved