事務處理的核心目標在於確保數據的高度完整性與一致性。對於 Java 平台開發者而言,正確實現事務策略並非易事——設計與編碼同樣關鍵。本文為「事務策略」系列首篇,將深入剖析 Java 平台中實現事務時常見的陷阱,並透過 Spring Framework 與 EJB 3.0 代碼示例,幫助開發者避開這些「隱形殺手」。
若不關心數據質量,大可不必使用事務。畢竟事務支持會帶來性能開銷、鎖定爭用、數據庫並發問題,並增加應用複雜度。然而,幾乎所有業務關鍵型應用都要求極高的數據質量。金融投資行業因失敗交易損失數百億美元,壞數據是第二大禍首;而缺乏事務支持或事務支持不足,正是導致壞數據的主因之一。
更危險的是忽視事務支持的心態:「我們的應用從不失敗,不需要事務」。即便代碼經過嚴格測試、覆蓋率極高,仍無法規避並發場景下的隔離性問題。沒有適當的事務隔離,其他工作單元可能讀取到未提交的部分更新,導致基於髒數據的業務決策,引發昂貴後果。
許多專案將「實現事務支持」排在系統測試之前,看似合理,實則隱患重重。事務策略不僅是編碼任務,更涉及大量設計決策。盡早納入事務設計,才能避免後期重構的高昂成本。
早期 JDBC 時代,事務邏輯委派給數據庫,單一邏輯工作單元(LUW)如單次 INSERT/UPDATE/DELETE 運作良好。但當業務需求擴展為「插入交易訂單同時更新帳戶餘額」時,若缺乏事務邊界,前者成功提交、後者失敗將導致數據不一致。
引入 Hibernate、JPA 等 ORM 框架後,開發者常誤以為 em.persist(entity) 會立即寫入數據庫。實則不然:基於 ORM 的框架需要事務提交來觸發對象緩存與數據庫的同步。無事務則無 SQL 生成、無數據持久化,方法靜默返回而不拋異常,極難排查。
@PersistenceContext(unitName="trading")
EntityManager em;
public long insertTrade(TradeData trade) {
em.persist(trade); // 無事務時不會真正寫入數據庫
return trade.getTradeId(); // 返回 0
}
僅在方法上加 @Transactional 不夠,必須在 Spring 配置中啟用註解驅動:
<tx:annotation-driven transaction-manager="transactionManager"/>
否則註解會被忽略,代碼以無事務模式運行。
裸用 @Transactional 等同於:
REQUIREDfalseREAD_COMMITTED這最後一點是最大陷阱之一,下文詳述。
@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public long insertTrade(TradeData trade) { ... }
因 SUPPORTS 不會啟動事務,readOnly=true 不生效,寫操作反而成功——這違背直覺,極易誤導開發者。
@Transactional(readOnly = true, propagation = Propagation.REQUIRED)
public long insertTrade(TradeData trade) { ... }
此時事務啟動,連接設置為只讀,執行寫操作拋出「只讀連接異常」。
同樣配置下,JPA/Hibernate 僅將 readOnly 視為提示,設置 FlushMode=NEVER;但 REQUIRED 會覆蓋此行為,允許寫入成功。結論:在 ORM 中 readOnly 基本無用,甚至有害(啟動不必要事務、增加鎖開銷)。
// 方案一:SUPPORTS 避免啟動事務
@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public TradeData getTrade(long tradeId) { ... }
// 方案二:直接移除註解(推薦)
public TradeData getTrade(long tradeId) { ... }
REQUIRES_NEW 總是啟動新事務,掛起現有事務。若在同一邏輯工作單元中多個方法均使用此屬性,會導致部分提交、部分回滾的數據不一致:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public long insertTrade(TradeData trade) { ... }
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateAcct(TradeData trade) { ... }
// 呼叫端
insertTrade(trade);
updateAcct(trade); // 若此處拋異常,insertTrade 已提交無法回滾
原則:除非有明確理由(如審計日誌必須持久化,無論主事務成敗),否則堅持使用 REQUIRED 或 MANDATORY。
這是生產環境中最常見、破壞力最強的陷阱。Spring 與 EJB 默認僅對非受檢異常回滾。若業務邏輯拋出受檢異常(如 FundsNotAvailableException),事務將提交已執行操作,導致「訂單創建但帳戶未扣款」的不一致狀態。
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public TradeData placeTrade(TradeData trade) { ... }
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public TradeData placeTrade(TradeData trade) {
try {
insertTrade(trade);
updateAcct(trade);
return trade;
} catch (Exception up) {
sessionCtx.setRollbackOnly(); // 標記回滾
throw up;
}
}
Java 事務代碼不複雜,但正確配置與使用極易出錯。最大的問題在於:編譯器不報錯、運行時不拋錯,數據卻已損壞。建立完整的事務策略,需從設計階段著手,而非編碼後期補救。本系列後續文章將指導如何針對不同場景設計有效的事務策略。