Java Transaction Service 是 J2EE 架構的關鍵元素。它與 Java Transaction API 結合在一起,使我們能夠構建對于各種系統和網絡故障都非常健壯的分布式應用程序。事務是可靠應用程序的基本構建塊 —— 如果沒有事務的支持,編寫可靠的分布式應用程序將是非常困難的。幸運的是,JTS 執行的大部分工作對于程序員都是透明的;J2EE 容器使事務劃分和資源征用對程序員來說幾乎是不可見的。這個由三個部分組成的系列文章的第一期講述了一些基礎知識,包括什么是事務,以及事務對于構建可靠的分布式應用程序來說至關重要的原因。<!--START RESERVED FOR FUTURE USE INCLUDE FILES--><!-- include java script once we verify teams wants to use this and it will work on dbcs and cyrillic characters --><!--END RESERVED FOR FUTURE USE INCLUDE FILES-->
如果您閱讀過任何有關 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 部分中,我們將討論這些問題并提供一些關于如何構造應用程序的建議。
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
