在這個關於 Java 事務服務(JTS)的系列文章前兩部分中,Brian Goetz 已經奠定了基礎:什麼是事務、事務的 ACID 特性,以及 J2EE 容器如何讓事務服務對 EJB 組件變得透明。雖然宣告式事務管理極大地提升了企業應用的配置彈性,但在應用組裝階段若做出不當決策,卻可能嚴重損害應用的效能與穩定性。在這最後一部分,我們將探討 J2EE 提供的事務劃分與隔離管理工具,並給出高效使用這些工具的實戰指導。
事務本質上是一種異常處理機制。就像商業合同在日常業務中提供法律保障一樣,事務在程式出錯時提供恢復機制。但既然大多數時間系統都正常運作,我們就必須將事務的開銷降到最低。應用程式如何使用事務,直接決定了其效能與擴展性的上限。
J2EE 容器提供兩種事務劃分機制:
UserTransaction.begin() 與 commit() 顯式控制邊界。無論採用哪種方式,核心原則只有一條:事務越短越好。
事務提供並發控制,資源管理器會在事務期間持有資料項的鎖。事務越長,其他事務等待鎖釋放的時間越長,吞吐量隨之暴跌。因此,絕對不要在事務中等待使用者輸入——使用者可能去吃午餐、甚至下班,導致資料庫鎖定數小時,應用程式徹底癱瘓。
所有 I/O 操作(網路呼叫、檔案讀寫、使用者互動)都應移至事務邊界之外。
雖然短事務有利擴展性,但將邏輯上不可分割的操作拆散到多個事務會破壞一致性。以銀行轉帳為例:扣款、入帳、寫入流水帳必須在同一事務中完成,否則系統故障時會出現「錢憑空消失」或「雙重入帳」的災難性後果。
綜合規則 1 與 3,最佳實務是:把業務邏輯上不可分割的操作放入同一事務,把無關操作拆分到不同事務。這要求開發者精準界定「業務邊界」。
CMT 透過 trans-attribute 定義方法的事務行為,J2EE 規範定義了六種模式:
| 模式 | 支援 Bean 類型 | 已有事務時 | 無事務時 |
|---|---|---|---|
| Required | Session、Entity、MDB | 加入現有事務 | 建立新事務 |
| RequiresNew | Session、Entity | 掛起現有事務,建立新事務 | 建立新事務 |
| Mandatory | Session、Entity | 加入現有事務 | 拋出例外 |
| Supports | Session、MDB | 加入現有事務 | 無事務執行 |
| NotSupported | Session、MDB | 掛起現有事務,無事務執行 | 無事務執行 |
| Never | Session、MDB | 拋出例外 | 無事務執行 |
Required,確保每次呼叫都在事務中,同時允許成為大事務一部分。Required 或 Mandatory(開發期可及早發現無事務呼叫)。RequiresNew,極易導致資料不一致。隔離意味著一個事務的中間狀態對其他並發事務不可見。資料庫通常支援四級標準隔離:
| 隔離級別 | 髒讀 | 不可重複讀 | 虛讀 |
|---|---|---|---|
| Read Uncommitted | 是 | 是 | 是 |
| Read Committed | 否 | 是 | 是 |
| Repeatable Read | 否 | 否 | 是 |
| Serializable | 否 | 否 | 否 |
Read Committed 是大多數資料庫的預設值,適合典型的短事務(查詢報表、新增資料)。Repeatable Read 適用於「查詢後更新」模式(如檢查餘額再扣款)。Serializable 則用於審計等絕對不能容忍虛讀的場景。Read Uncommitted 僅用於近似統計(如估算當日訂單總額)。
隔離級別越高,鎖爭用越嚴重,擴展性越差。開發階段就應考慮隔離需求,編寫能容忍較低隔離級別的程式碼,為後續效能調優留有餘地。
事務劃分與隔離本質上是安全性與效能的權衡。正確的平衡點取決於系統停機成本、資料敏感度與組織風險承受力。沒有銀彈,只有基於業務場景的審慎抉擇。