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