什麼是 JTS?
JTS 是一個 組件事務監視器(component transaction monitor)。這是什麼意思?我們將介紹事務處理監視器(TPM)這個概念,TPM 是一個程序,它代表應用程序協調分布式事務的執行。TPM 與數據庫出現的時間長短差不多;在 60 年代後期,IBM 首先開發了 CICS,至今人們仍在使用。經典的(或者說 程序化)TPM 管理被程序化定義為針對事務性資源(比如數據庫)的操作序列的事務。隨著分布式對象協議,如 CORBA、DCOM 和 RMI 的出現,人們希望看到事務更面向對象的前景。將事務性語義告知面向對象的組件要求對 TPM 模型進行擴展 ― 在這個模型中事務是按照事務性對象的調用方法定義的。JTS 只是一個組件事務監視器(有時也稱為 對象事務監視器(object transaction monitor)),或稱為 CTM。
JTS 和 J2EE 的事務支持設計受 CORBA 對象事務服務(CORBA Object Transaction Service,OTS)的影響很大。實際上,JTS 實現 OTS 並充當 Java 事務 API(Java Transaction API)― 一種用來定義事務邊界的低級 API ― 和 OTS 之間的接口。使用 OTS 代替創建一個新對象事務協議遵循了現有標准,並使 J2EE 和 CORBA 能夠互相兼容。
乍一看,從程序化事務監視器到 CTM 的轉變好像只是術語名稱改變了一下。然而,差別不止這一點。當 CTM 中的事務提交或回滾時,與事務相關的對象所做的全部更改都一起被提交或取消。但 CTM 怎麼知道對象在事務期間做了什麼事?象 EJB 組件之類的事務性組件並沒有 commit() 或 rollback() 方法,它們也沒向事務監視器注冊自己做了什麼事。那麼 J2EE 組件執行的操作如何變成事務的一部分呢?
透明的資源征用
當應用程序狀態被組件操縱時,它仍然存儲在事務性資源管理器(例如,數據庫和消息隊列服務器)中,這些事務性資源管理器可以注冊為分布式事務中的資源管理器。在第 1 部分中,我們討論了如何在單個事務中征用多個資源管理器,事務管理器如何協調這些資源管理器。資源管理器知道如何把應用程序狀態中的變化與特定的事務關聯起來。
但這只是把問題的焦點從組件轉移到了資源管理器 ― 容器如何斷定什麼資源與該事務有關,可以供它征用?請考慮下面的代碼,在典型的 EJB 會話 bean 中您可能會發現這樣的代碼:
清單 1. bean 管理的事務的透明資源征用
1 InitialContext ic = new InitialContext();
2 UserTransaction ut = ejbContext.getUserTransaction();
3 ut.begin();
4 DataSource db1 = (DataSource) ic.lookup("Java:comp/env/OrdersDB");
5 DataSource db2 = (DataSource) ic.lookup("Java:comp/env/InventoryDB");
6 Connection con1 = db1.getConnection();
7 Connection con2 = db2.getConnection();
8 // perform updates to OrdersDB using connection con1
9 // perform updates to InventoryDB using connection con2
10 ut.commit();
11
注意,這個示例中沒有征用當前事務中 JDBC 連接的代碼 ― 容器會為我們完成這個任務。我們來看一下它是如何發生的。
資源管理器的三種類型
當一個 EJB 組件想訪問數據庫、消息隊列服務器或者其它一些事務性資源時,它需要到資源管理器的連接(通常是使用 JNDI)。而且,J2EE 規范只認可三種類型的事務性資源 ― JDBC 數據庫、JMS 消息隊列服務器和“其它通過 JCA 訪問的事務性服務”。後面一種服務(比如 ERP 系統)必須通過 JCA(J2EE Connector Architecture,J2EE 連接器體系結構)訪問。對於這些類型資源中的每一種,容器或提供者都會幫我們把資源征調到事務中。
在清單 1 中, con1 和 con2 好象是普通的 JDBC 連接,比如那些從 DriverManager.getConnection() 返回的連接。我們從一個 JDBC DataSource 得到這些連接,JDBC DataSource 可以通過查找 JNDI 中的數據源名稱得到。EJB 組件中被用來查找數據源( Java:comp/env/OrdersDB )的名稱是特定於組件的;組件的部署描述符的 resource-ref 部分將其映射為容器管理的一些應用程序級 DataSource 的 JNDI 名稱。
隱藏的 JDBC 驅動器
每個 J2EE 容器都可以創建有事務意識的池態 DataSource 對象,但 J2EE 規范並不向您展示如何創建,因為這不在 J2EE 規范內。浏覽 J2EE 文檔時,您找不到任何關於如何創建 JDBC 數據源的內容。相反,您不得不為您的容器查閱該文檔。創建一個數據源可能需要向屬性或配置文件添加一個數據源定義,或者也可以通過 GUI 管理工具完成,這取決於您的容器。
每個容器(或連接池管理器,如 PoolMan)都提供它自己的創建 DataSource 機制,JTA 魔術就隱藏在這個機制中。連接池管理器從指定的 JDBC 驅動器得到一個 Connection ,但在將它返回到應用程序之前,將它與一個也實現 Connection 的虛包包在一起,將自己置入應用程序和底層連接之間。當創建連接或者執行 JDBC 操作時,包裝器詢問事務管理器當前線程是不是正在事務的上下文中執行,如果事務中有 Connection 的話,就自動征用它。
其它類型的事務性資源,JMS 消息隊列和 JCA 連接器,依靠相似的機制將資源征用隱藏起來,使用戶看不到。如果要使 JMS 隊列在部署時對 J2EE 應用程序可用,您就要再次使用特定於提供者的機制來創建受管 JMS 對象(隊列連接工廠和目標),然後在 JNDI 名稱空間內發布這些對象。提供者創建的受管對象包含與 JDBC 包裝器(由容器提供的連接池管理器添加)相似的自動征用代碼。
透明的事務控制
兩種類型的 J2EE 事務 ― 容器管理的和 bean 管理的 ― 在如何啟動和結束事務上是不同的。事務啟動和結束的地方被稱為 事務劃分(transaction demarcation)。清單 1 中的示例代碼演示了 bean 管理的事務(有時也稱為 編程(programmatic)事務)。Bean 管理的事務是由組件使用 UserTransaction 類顯式啟動和結束的。通過 ejbContext 使 UserTransaction 對 EJB 組件可用,通過 JNDI 使其對其它 J2EE 組件可用。
容器根據組件的部署描述符中的事務屬性代表應用程序透明地啟動和結束容器管理的事務(或稱為 宣告式事務(declarative transaction))。通過將 transaction-type 屬性設置為 Container 或 Bean 您可以指出 EJB 組件是使用 bean 管理的事務性支持還是容器管理的事務性支持。
使用容器管理的事務,您可以在 EJB 類或方法級別上指定事務性屬性;您可以為 EJB 類指定缺省的事務性屬性,如果不同的方法會有不同的事務性語義,您還可以為每個方法指定屬性。這些事務性屬性在裝配描述符(assembly descriptor)的 container-transaction 部分被指定。清單 2 顯示了一個裝配描述符示例。 trans-attribute 的受支持的值有:
Supports
Required
RequiresNew
Mandatory
NotSupported
Never
trans-attribute 決定方法是否支持在事務內部執行、當在事務內部調用方法時容器會執行什麼操作以及在事務外部調用方法時容器會執行什麼操作。最常用的容器管理的事務屬性是 Required 。如果設置了 Required ,過程中的事務將在該事務中征用您的 bean,但如果沒有正在運行的事務,容器將為您啟動一個。在這個系列的第 3 部分,當您可能想使用每個事務屬性時,我們將研究各個事務屬性之間的區別。
清單 2. EJB 裝配描述符樣本
1
2 ...
3
4
5
6
7
8 Required
9
10
11
12
13
14
15 RequiresNew
16
17 ...
18
19
功能強大,但很危險
與清單 1 中的示例不同,由於有宣告式事務劃分,這段組件代碼中沒有事務管理代碼。這不僅使結果組件代碼更加易讀(因為它不與事務管理代碼混在一起),而且它還有另一個更重要的優點 ― 不必修改,甚至不必訪問組件的源代碼,就可以在應用程序裝配時改變組件的事務性語義。
盡管能夠指定與代碼分開的事務劃分是一種非常強大的功能,但在裝配時做出不好的決定會使應用程序變得不穩定,或者嚴重影響它的性能。對容器管理的事務進行正確分界的責任由組件開發者和應用程序裝配人員共同擔當。組件開發者需要提供足夠的文檔說明組件是做什麼的,這樣應用程序部署者就能夠明智地決定如何構建應用程序的事務。應用程序裝配人員需要理解應用程序中的組件是怎樣相互作用的,這樣就可以用一種既強制應用程序保持一致又不削弱性能的方法對事務進行分界。在這個系列的第 3 部分中我們將討論這些問題。
透明的事務傳播
在任何類型的事務中,資源征用都是透明的;容器自動將事務過程中使用的任意事務性資源征調到當前事務中。這個過程不僅擴展到事務性方法使用的資源(比如在清單 1 中獲得的數據庫連接),還擴展到它調用的方法(甚至遠程方法)使用的資源。我們來看一下這是如何發生的。
容器用線程與事務相關聯
我們假設對象 A 的 methodA() 啟動一個事務,然後調用對象 B 的 methodB() (對象 B 將得到一個 JDBC 連接並更新數據庫)。 B 獲得的連接將被自動征調到 A 創建的事務中。容器怎麼知道要做這件事?
當事務啟動時,事務上下文與執行線程關聯在一起。當 A 創建事務時, A 在其中執行的線程與該事務關聯在一起。由於本地方法調用與主調程序(caller)在同一個線程內執行,所以 A 調用的每個方法也都在該事務的上下文中。
櫥中骸骨
如果對象 B 其實是在另一個線程,甚至另一個 JVM 中執行的 EJB 組件的存根,情況會怎樣?令人吃驚的是,遠程對象 B 訪問的資源仍將在當前事務中被征用。EJB 對象存根(在主調程序的上下文中執行的那部分)、EJB 協議(IIOP 上的 RMI)和遠端的骨架對象協力要使其透明地發生。存根確定調用者是不是正在執行一個事務。如果是,事務標識,或者說 Xid,被作為 IIOP 調用的一部分與方法參數一起傳播到遠程對象。(IIOP 是 CORBA 遠程-調用協議,它為傳播執行上下文(比如事務上下文和安全性上下文)的各種元素而備;關於 RMI over IIOP 的更多信息,請參閱 參考資料。)如果調用是事務的一部分,那麼遠程系統上的骨架對象自動設置遠程線程的事務上下文,這樣,當調用實際的遠程方法時,它已經是事務的一部分了。(存根和骨架對象還負責開始和提交容器管理的事務。)
事務可以由任何 J2EE 組件來啟動 ― 一個 EJB 組件、一個 servlet 或者一個 JSP 頁面(如果容器支持的話,還可以是一個應用程序客戶機)。這意味著,應用程序可以在請求到達時在 servlet 或者 JSP 頁面中啟動事務、在 servlet 或者 JSP 頁面中執行一些處理、作為頁面邏輯的一部分訪問多個服務器上的實體 bean 和會話 bean 並使所有這些工作透明地成為一個事務的一部分。圖 1 演示了事務上下文怎樣遵守從 servlet 到 EJB,再到 EJB 的執行路徑。
最優化
讓容器來管理事務允許容器為我們做出某些最優化決定。在圖 1 中,我們看到一個 servlet 和多個 EJB 組件在單個事務的上下文中訪問一個數據庫。每個組件都獲得到數據庫的 Connection ;很可能每個組件都在訪問同一個數據庫。即使多個連接是從不同的組件到同一個資源,JTS 也可以檢測出多個資源是否和事務有關,並最優化該事務的執行。您可以從第 1 部分回憶起來,單個事務要包含多個資源管理器需要使用兩階段提交協議,這比單個資源管理器使用的單階段提交代價要高。JTS 能夠確定事務中是不是只征用了一個資源管理器。如果它檢測出所有與事務相關的資源都一樣,它可以跳過兩階段提交並讓資源管理器自己來處理事務。
結束語
這個慮及透明事務控制、資源征用和透明傳播的魔術不是 JTS 的一部分,而是 J2EE 容器如何在幕後代表 J2EE 應用程序使用 JTA 和 JTS 服務的一部分。在幕後有許多實體合力使這個魔術透明地發生;EJB 存根和骨架、容器廠商提供的 JDBC 驅動器包裝器、數據庫廠商提供的 JDBC 驅動器、JMS 提供器和 JCA 連接器。所有這些實體都與事務管理器進行交互,於是應用程序代碼就不必與之交互了。