Java Transaction Service 是 J2EE 架構的關鍵元素。它與 Java Transaction API 結合在一起,使我們能夠構建對於各種系統和網絡故障都非常 健壯的分布式應用程序。事務是可靠應用程序的基本構建塊 —— 如果沒有事務 的支持,編寫可靠的分布式應用程序將是非常困難的。幸運的是,JTS 執行的大 部分工作對於程序員都是透明的;J2EE 容器使事務劃分和資源征用對程序員來 說幾乎是不可見的。這個由三個部分組成的系列文章的第一期講述了一些基礎知 識,包括什麼是事務,以及事務對於構建可靠的分布式應用程序來說至關重要的 原因。
如果您閱讀過任何有關 J2EE 的介紹性文章或者書籍,那麼就會發現,只有 一小部分資料是專門針對 Java Transaction Service(JTS)或 Java Transaction API(JTA)的。這並不是因為 JTS 是 J2EE 中不重要的部分或者 可選部分 —— 恰恰相反。JTS 受到的關注之所以會比 EJB 技術少,是因為它 為應用程序提供的服務非常透明 —— 很多開發人員甚至沒有注意到在他們的應 用程序中事務在哪裡開始和結束。在某種意義上,JTS 的默默無聞恰恰是它的成 功:因為它非常有效地隱藏了事務管理的很多細節,因此,我們沒有聽說過或者 談論過很多關於它的內容。但是,您可能想了解它在幕後都為您執行什麼功能。
毫不誇張地說,沒有事務就不能編寫可靠的分布式應用程序。事務允許采用 某種控制方式修改應用程序的持久性狀態,以便使應用程序對於各種各樣的系統 故障(包括系統崩潰、網絡故障、電源故障甚至自然災害)更加健壯。事務是構 建容錯、高可靠性以及高可用性應用程序所需的基本構建塊之一。
事務的動機
假設您正在從一個賬戶向另一個賬戶進行轉賬個賬戶差額由數據庫表中的某 一行來表示。如果您想從賬戶 A 轉賬到賬戶 B,則可能執行如下這些 SQL 代碼 :
SELECT accountBalance INTO aBalance
FROM Accounts WHERE accountId=aId;
IF (aBalance >= transferAmount) THEN
UPDATE Accounts
SET accountBalance = accountBalance - transferAmount
WHERE accountId = aId;
UPDATE Accounts
SET accountBalance = accountBalance + transferAmount
WHERE accountId = bId;
INSERT INTO AccountJournal (accountId, amount)
VALUES (aId, -transferAmount);
INSERT INTO AccountJournal (accountId, amount)
VALUES (bId, transferAmount);
else
FAIL "Insufficient funds in account";
END if
到目前為止,這段代碼看起來非常簡單易懂。如果手頭的資金充足,則從一 個賬戶中減去資金,並添加到另一個賬戶中。但是,如果出現系統電源故障或者 崩潰,又會發生什麼情況呢?表示賬戶 A 和賬戶 B 的行可能不會存儲在同一個 磁盤塊中,這意味著要完成轉賬需要進行多個磁盤 IO。如果在已寫入第一個磁 盤塊之後,在寫入第二個磁盤塊之前,系統發生故障,又會發生什麼情況呢?A 賬戶中的資金已經劃走,但是沒有出現在賬戶 B 中(A 和 B 客戶都不會願意) ,或者資金將出現在賬戶 B 中,但是沒有記入賬戶 A 的借出賬中(銀行不會願 意)。如果賬戶已正確更新,而賬戶日記賬沒有更新,又會發生什麼情況呢?那 麼賬戶 A 和賬戶 B 的每月銀行結賬單將與它們賬戶的余額不一致。
不僅不可能同時將多個數據塊寫入磁盤,而且每當進行修改時馬上將每個數 據塊寫入磁盤,也對系統性能有不利影響。將磁盤寫入延遲到比較適宜的時間可 能會大大改善應用程序的吞吐量,但是,需要采用不損害數據完整性的方式執行 。
甚至在系統沒有發生故障時,上面討論的代碼還有另一種風險 —— 並發性 。如果賬戶 A 中有 100 美元,但是卻同時開始向它的兩個不同的賬戶分別轉賬 100 美元,那麼會發生什麼情況呢?如果時間上湊巧,並且沒有適當的鎖定機制 ,兩次轉賬都可能成功,從而使賬戶 A 的余額為負值。
這些情況似乎都是非常可能發生的,因此希望企業數據系統能夠解決這些問 題是理所應當的。我們希望在發生火災、洪水、電源故障、磁盤以及系統出現故 障時,銀行都能夠保持正確的賬戶記錄。可以通過冗余(冗余的磁盤、計算機以 及數據中心)來提供容錯,但是事務 使得構建容錯的軟件應用程序成為可能。 事務提供了一個框架,用於在系統或組件發生故障時保持數據一致性和完整性。
什麼是事務?
那麼到底什麼是事務呢?在定義這個術語之前,我們首先定義應用程序狀態 的概念。應用程序的狀態包含影響應用程序操作的所有內存和磁盤中的數據項目 —— 應用程序 “知道” 的所有內容。應用程序狀態可以存儲在內存、文件或 者數據庫中。如果系統發生故障,例如應用程序、網絡或者計算機系統崩潰,則 我們想確保當重新啟動系統時,可以恢復應用程序的狀態。
現在,我們將事務 定義為對應用程序狀態的相關操作的集合。事務具有原子 性、一致性、隔離性 以及持久性 這幾個屬性。這些屬性統稱為 ACID 屬性。
原子性 意味著要麼所有事務操作都應用於應用程序狀態,要麼都不應用;事 務是不可拆分的工作單元。
一致性 意味著事務代表應用程序狀態的正確轉換 —— 即事務不能違反應用 程序中固有的任何完整性限制。實際上,一致性的概念是特定於應用程序的。例 如,在記賬應用程序中,一致性可能包括所有資產賬戶的總和始終等於所有負債 賬戶的總和這個不變式。在本系列的第 3 部分中討論事務劃分時,我們將詳細 討論這個需求。
隔離性 意味著一個事務的效果不影響正在同時執行的其他事務。從事務的角 度講,它意味著事務按順序執行而不是並行執行。在數據庫系統中,通常通過使 用鎖機制來實現隔離性。為了使應用程序獲得最佳性能,有時也會對某些事務放 松隔離性的要求。
持久性 意味著一旦成功完成某個事務,對應用程序狀態所做的更改將 “經 得起失敗”。
什麼是 “經得起失敗”呢?它由什麼組成?這取決於系統,一個設計良好的 系統將明確地標識可以從哪些故障中恢復過來。在我的桌面工作站上運行的事務 數據庫,對於系統崩潰和電源故障非常穩定健壯,但是對於我的辦公大樓發生大 火災卻沒有任何作用。銀行可能不僅僅在數據中心具有冗余的磁盤、網絡以及系 統,而且還可能在別的城市有冗余的數據中心,該冗余數據中心通過冗余的通信 鏈路連接,目的是允許從嚴重的故障(如自然災害)中進行恢復。軍用的數據系 統甚至可能有更嚴格的容錯要求。
事務的剖析
典型事務有幾個參與者 —— 應用程序、事務監視器(TPM)以及一個或多個 資源管理器(RM)。資源管理器存儲應用程序狀態,常常是數據庫,但也可能是 消息隊列服務器(在 J2EE 應用程序中,它們將是 JMS 提供者)或其他事務性 資源。TPM 協調 RM 的活動,以確保事務 “要麼全有要麼全無” 屬性。
當應用程序請求容器或事務監視器啟動新的事務時,事務開始。由於應用程 序訪問各種各樣的 RM,因此,在事務中對它們進行征用。RM 必須使對應用程序 狀態所做的任何更改與請求更改的事務相關聯。
當發生以下事件之一或者兩個事件都發生時,事務結束:事務應用程序提交 該事務;通過應用程序或者由於其中一個 RM 失敗,回滾 該事務。如果事務成 功提交,則將寫入與該事務相關聯的更改,以使更改持久化並使其對於新的事務 可見。如果事務被回滾,則該事務所做的所有更改都將被丟棄;就好像該事務從 來沒有發生過一樣。
事務日志 —— 持久性的關鍵
事務 RM 通過在一個事務日志中記錄多個事務的結果,獲得持久性以及可接 受的性能。事務日志存儲為連續的磁盤文件(有時存儲在原始分區中),並且一 般只是用於寫入而不用於讀取,回滾或恢復的情況例外。在我們的銀行賬戶示例 中,與賬戶 A 和賬戶 B 相關聯的余額將在內存中進行更新,新的余額和舊的余 額將被寫入到事務日志中。編寫事務日志的更新記錄不需要將全部數據都寫入磁 盤(只需要寫入已更改的數據,而不需要寫入全部磁盤塊),而且所需的磁盤尋 道時間也會更少(原因是所有更改都包含在日志中連續的磁盤塊中)。此外,與 多個並發事務關聯的更改可以合並到一起,一次寫入事務日志,這意味著每次磁 盤寫入時我們可以處理多個事務,而不需要每個事務進行幾次磁盤寫入。之後, RM 將根據所更改的數據更新實際的磁盤塊。
重新啟動時進行恢復
如果系統出現故障,重新啟動時要做的第一件事就是重新應用所有已提交事 務的作用,所有這些已提交的事務都位於日志中,但是它們的數據塊尚未更新。 采用這種方式,日志保證了故障之間的持久性,而且還能夠減少所執行的磁盤 IO 操作的數量,或者至少使它們延遲到對系統性能影響更小的時間。
兩階段提交
很多事務只涉及一個 RM —— 通常是數據庫。在這種情況下,RM 通常執行 提交或回滾事務所需的大部分工作。(幾乎所有事務 RM 都有它們自己的內置的 事務管理器,這個管理器可以處理本地事務 —— 只涉及該 RM 的事務)。但是 ,如果事務涉及兩個 RM 或多個 RM —— 可能是兩個單獨的數據庫,或者是一 個數據庫和一個 JMS 隊列,或者是兩個單獨的 JMS 提供者 —— 我們想確保 “要麼全有要麼全無” 的語義不僅僅應用於這個 RM 中,而且還應用於事務中 的所有 RM。在這種情況下,TPM 將組織一個兩階段提交。在兩階段提交中,TPM 首先向每個 RM 發送一個 “准備” 消息,詢問它是否准備就緒以及是否能夠提 交事務;如果它收到來自所有 RM 的確認應答,則將事務在其自己的事務日志中 標記為已提交,然後指示所有 RM 提交事務。如果某個 RM 失敗,則重新啟動時 它將向 TPM 詢問有關失敗時未處理的所有事務的狀態,並提交它們或者對它們 執行回滾操作。
兩個階段提交類似於社會上的結婚典禮 —— 牧師或神父詢問雙方 “您願意 讓這個男人/女人作為您的丈夫/妻子嗎?” 如果雙方都回答是,則將宣布他們 成為夫妻;否則,雙方不能結婚。不管雙方中的哪一方首先說 “我願意”,在 一方沒有回答時,另一方決不能完成結婚。
事務作為處理異常的機制
您可能觀察到,事務向對塊進行同步的應用程序數據提供很多與內存中數據 相同的功能 —— 保證原子性、更改的可見性以及顯而易見的排序。但是,當同 步主要是並發控制的機制時,則事務主要是處理異常的機制。如果在一個磁盤不 會發生故障、系統和軟件不會崩潰以及電源是百分百可靠的世界中,我們將不需 要事務。事務在企業應用程序中所起的作用與合同法在社會上所起的作用一樣 —— 它們規定,如果一方不能履行他那一部分合同,則交易將失效。當我們編 寫合同時,我們通常希望它是多余的,令人感到欣慰的是大部分時候都是如此。
與比較簡單的 Java 程序進行類比,事務在應用程序級別所提供的一些優勢 與 catch 和 finally 塊在方法級別所提供的優勢相同;它們使我們不用編寫很 多錯誤復原代碼,即可執行可靠的錯誤復原。考慮下面這個方法,該方法將一個 文件復制到另一個文件:
public boolean copyFile(String inFile, String outFile) {
InputStream is = null;
OutputStream os = null;
byte[] buffer;
boolean success = true;
try {
is = new FileInputStream(inFile);
os = new FileOutputStream(outFile);
buffer = new byte[is.available()];
is.read(buffer);
os.write(buffer);
}
catch {IOException e) {
success = false;
}
catch (OutOfMemoryError e) {
success = false;
}
finally {
if (is != null)
is.close();
if (os != null)
os.close();
}
return success;
}
忽略為整個文件分配一個緩沖區是一個不好的想法,但是在這個方法中哪裡 錯了呢?有很多東西。輸入文件可能不存在,或者該用戶可能沒有這個文件的讀 權限。用戶可能沒有輸出文件的寫權限,或者該文件被另一個用戶鎖定。可能沒 有足夠的磁盤空間來完成該文件的寫操作,或者由於沒有足夠的內存可用,分配 緩沖區可能失敗。幸運的是,所有這些都由 finally 語句來處理,該語句釋放 了 copyFile() 所使用的所有資源。
如果您使用原來的C語言編寫這個方法,則對於每個操作(打開輸入、打開 輸出、malloc、讀、寫),必須測試返回狀態,如果操作失敗,則取消以前成功 的所有操作,並返回適當的狀態代碼。由於需要這麼多錯誤處理代碼,該代碼可 能更大,因此更難閱讀。同時在錯誤處理代碼(它也是最難測試的部分)中很容 易出錯,比如不能釋放資源、對一個資源釋放兩次或者釋放尚未分配的資源。更 復雜的方法可能涉及更多資源,而不僅僅是兩個文件和一個緩沖區,這使得問題 變得更加復雜。在大量錯誤復原代碼中,很難發現實際的程序邏輯。
現在,假設您正在執行一個復雜的操作,該操作涉及在多個數據庫中插入或 更新多個行,其中一個操作違反了完整性約束並失敗了。如果您管理自己的錯誤 復原,則必須跟蹤已經執行的操作,並知道在隨後的操作失敗的情況下如何取消 每個操作。如果工作單元分布在多個方法或組件上,則會更加困難。借助事務來 構造應用程序,就可以將所有這些清理工作委托給數據庫(即進行 ROLLBACK) ,並取消自從事務開始所執行的所有操作。
結束語
通過借助事務構造應用程序,我們定義一組正確的應用程序狀態轉換,並確 保應用程序始終處於正確的狀態,甚至在系統或組件發生故障之後也是如此。事 務使我們能夠將很多異常處理和恢復工作委托給 TPM 和 RM,從而簡化了我們的 代碼,並使我們能夠空出更多時間來考慮應用程序邏輯。
在此系列的第 2 部分中,我們將探討這對於 J2EE 應用程序意味著什麼 — — J2EE 如何使我們能夠將事務語義告知 J2EE 組件(EJB 組件、servlet 以及 JSP 頁面);它如何使資源征用對應用程序(甚至對於 bean 管理的事務)完全 透明;單個事務如何透明地遵循從一個 EJB 組件到另一個 EJB 組件,或者從一 個 servlet 到一個 EJB 組件,甚至跨越多個系統的控制流程。
盡管 J2EE 提供了相當透明的對象事務服務,但是應用程序設計者仍然必須 仔細考慮在哪裡劃分事務,以及如何在應用程序中使用事務資源 —— 不正確的 事務劃分可能會使應用程序處於不一致的狀態,而不正確地使用事務資源可能會 造成非常嚴重的性能問題。在此系列的第 3 部分中,我們將討論這些問題並提 供一些關於如何構造應用程序的建議。