簡介: 在 J2EE 應用中,事務是一個不可或缺的元件模型,它保證了使用者操作的 ACID(即原子、一致、隔離、持久)屬性。對於只操作單一資料來源的應用,可以通過本地資源介面實現事務管理;對於跨資料來源(例如多個資料庫,或者資料庫與 JMS)的大型應用,則必須使用全域性事務 JTA (Java Transaction API)。JTA 為 J2EE 平臺提供了分散式事務服務,它隔離了事務與底層的資源,實現了透明的事務管理方式。本文將深入探尋 JTA 的體系架構,並通過詳細的程式碼介紹其實現機制。
利用 JTA 處理事務
什麼是事務處理
事務是計算機應用中不可或缺的元件模型,它保證了使用者操作的原子性 ( Atomicity )、一致性 ( Consistency )、隔離性 ( Isolation ) 和永續性 ( Durabilily )。關於事務最經典的示例莫過於信用卡轉賬:將使用者 A 賬戶中的 500 元人民幣轉移到使用者 B 的賬戶中,其操作流程如下
1. 將 A 賬戶中的金額減少 500
2. 將 B 賬戶中的金額增加 500
這兩個操作必須保正 ACID 的事務屬性:即要麼全部成功,要麼全部失敗;假若沒有事務保障,使用者的賬號金額將可能發生問題:
假如第一步操作成功而第二步失敗,那麼使用者 A 賬戶中的金額將就減少 500 元而使用者 B 的賬號卻沒有任何增加(不翼而飛);同樣如果第一步出錯 而第二步成功,那麼使用者 A 的賬戶金額不變而使用者 B 的賬號將增加 500 元(憑空而生)。上述任何一種錯誤都會產生嚴重的資料不一致問題,事務的缺失對於一個穩定的生產系統是不可接受的。
J2EE 事務處理方式
1. 本地事務:緊密依賴於底層資源管理器(例如資料庫連線 ),事務處理侷限在當前事務資源內。此種事務處理方式不存在對應用伺服器的依賴,因而部署靈活卻無法支援多資料來源的分散式事務。在資料庫連線中使用本地事務示例如下:
清單 1. 本地事務處理例項
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
public void transferAccount() { Connection conn = null; Statement stmt = null; try{ conn = getDataSource().getConnection(); // 將自動提交設定為 false, //若設定為 true 則資料庫將會把每一次資料更新認定為一個事務並自動提交 <strong>conn.setAutoCommit(false);</strong> stmt = conn.createStatement(); // 將 A 賬戶中的金額減少 500 stmt.execute("\ update t_account set amount = amount - 500 where account_id = 'A'"); // 將 B 賬戶中的金額增加 500 stmt.execute("\ update t_account set amount = amount + 500 where account_id = 'B'"); // 提交事務 <strong>conn.commit();</strong> // 事務提交:轉賬的兩步操作同時成功 } catch(SQLException sqle){ try{ // 發生異常,回滾在本事務中的操做 <strong>conn.rollback();</strong> // 事務回滾:轉賬的兩步操作完全撤銷 stmt.close(); conn.close(); }catch(Exception ignore){ } sqle.printStackTrace(); } } |
2. 分散式事務處理 : Java 事務程式設計介面(JTA:Java Transaction API)和 Java 事務服務 (JTS;Java Transaction Service) 為 J2EE 平臺提供了分散式事務服務。分散式事務(Distributed Transaction)包括事務管理器(Transaction Manager)和一個或多個支援 XA 協議的資源管理器 ( Resource Manager )。我們可以將資源管理器看做任意型別的持久化資料儲存;事務管理器承擔著所有事務參與單元的協調與控制。JTA 事務有效的遮蔽了底層事務資源,使應用可以以透明的方式參入到事務處理中;但是與本地事務相比,XA 協議的系統開銷大,在系統開發過程中應慎重考慮是否確實需要分散式事務。若確實需要分散式事務以協調多個事務資源,則應實現和配置所支援 XA 協議的事務資源,如 JMS、JDBC 資料庫連線池等。使用 JTA 處理事務的示例如下(注意:connA 和 connB 是來自不同資料庫的連線)
清單 2. JTA 事務處理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
public void transferAccount() { UserTransaction userTx = null; Connection connA = null; Statement stmtA = null; Connection connB = null; Statement stmtB = null; try{ // 獲得 Transaction 管理物件 userTx = (UserTransaction)getContext().lookup("\ java:comp/UserTransaction"); // 從資料庫 A 中取得資料庫連線 connA = getDataSourceA().getConnection(); // 從資料庫 B 中取得資料庫連線 connB = getDataSourceB().getConnection(); // 啟動事務 userTx.begin(); // 將 A 賬戶中的金額減少 500 stmtA = connA.createStatement(); stmtA.execute(" update t_account set amount = amount - 500 where account_id = 'A'"); // 將 B 賬戶中的金額增加 500 stmtB = connB.createStatement(); stmtB.execute("\ update t_account set amount = amount + 500 where account_id = 'B'"); // 提交事務 userTx.commit(); // 事務提交:轉賬的兩步操作同時成功(資料庫 A 和資料庫 B 中的資料被同時更新) } catch(SQLException sqle){ try{ // 發生異常,回滾在本事務中的操縱 userTx.rollback(); // 事務回滾:轉賬的兩步操作完全撤銷 //( 資料庫 A 和資料庫 B 中的資料更新被同時撤銷) stmt.close(); conn.close(); ... }catch(Exception ignore){ } sqle.printStackTrace(); } catch(Exception ne){ e.printStackTrace(); } } |
JTA 實現原理
很多開發人員都會對 JTA 的內部工作機制感興趣:我編寫的程式碼沒有任何與事務資源(如資料庫連線)互動的程式碼,但是我的操作(資料庫更新)卻實實在在的被包含在了事務中,那 JTA 究竟是通過何種方式來實現這種透明性的呢? 要理解 JTA 的實現原理首先需要了解其架構:它包括事務管理器(Transaction Manager)和一個或多個支援 XA 協議的資源管理器 ( Resource Manager ) 兩部分, 我們可以將資源管理器看做任意型別的持久化資料儲存;事務管理器則承擔著所有事務參與單元的協調與控制。 根據所物件導向的不同,我們可以將 JTA 的事務管理器和資源管理器理解為兩個方面:面向開發人員的使用介面(事務管理器)和麵向服務提供商的實現介面(資源管理器)。其中開發介面的主要部分即為上述示例中引用的 UserTransaction 物件,開發人員通過此介面在資訊系統中實現分散式事務;而實現介面則用來規範提供商(如資料庫連線提供商)所提供的事務服務,它約定了事務的資源管理功能,使得 JTA 可以在異構事務資源之間執行協同溝通。以資料庫為例,IBM 公司提供了實現分散式事務的資料庫驅動程式,Oracle 也提供了實現分散式事務的資料庫驅動程式, 在同時使用 DB2 和 Oracle 兩種資料庫連線時, JTA 即可以根據約定的介面協調者兩種事務資源從而實現分散式事務。正是基於統一規範的不同實現使得 JTA 可以協調與控制不同資料庫或者 JMS 廠商的事務資源,其架構如下圖所示:
圖 1. JTA 體系架構
開發人員使用開發人員介面,實現應用程式對全域性事務的支援;各提供商(資料庫,JMS 等)依據提供商介面的規範提供事務資源管理功能;事務管理器( TransactionManager )將應用對分散式事務的使用對映到實際的事務資源並在事務資源間進行協調與控制。 下面,本文將對包括 UserTransaction、Transaction 和 TransactionManager 在內的三個主要介面以及其定義的方法進行介紹。
面向開發人員的介面為 UserTransaction (使用方法如上例所示),開發人員通常只使用此介面實現 JTA 事務管理,其定義瞭如下的方法:
●begin()- 開始一個分散式事務,(在後臺 TransactionManager 會建立一個 Transaction 事務物件並把此物件通過 ThreadLocale 關聯到當前執行緒上 )
●commit()- 提交事務(在後臺 TransactionManager 會從當前執行緒下取出事務物件並把此物件所代表的事務提交)
●rollback()- 回滾事務(在後臺 TransactionManager 會從當前執行緒下取出事務物件並把此物件所代表的事務回滾)
●getStatus()- 返回關聯到當前執行緒的分散式事務的狀態 (Status 物件裡邊定義了所有的事務狀態,感興趣的讀者可以參考 API 文件 )
●setRollbackOnly()- 標識關聯到當前執行緒的分散式事務將被回滾
面向提供商的實現介面主要涉及到 TransactionManager 和 Transaction 兩個物件
Transaction 代表了一個物理意義上的事務,在開發人員呼叫 UserTransaction.begin() 方法時 TransactionManager 會建立一個 Transaction 事務物件(標誌著事務的開始)並把此物件通過 ThreadLocale 關聯到當前執行緒。UserTransaction 介面中的 commit()、rollback(),getStatus() 等方法都將最終委託給 Transaction 類的對應方法執行。Transaction 介面定義瞭如下的方法:
●commit()- 協調不同的事務資源共同完成事務的提交
●rollback()- 協調不同的事務資源共同完成事務的回滾
●setRollbackOnly()- 標識關聯到當前執行緒的分散式事務將被回滾
●getStatus()- 返回關聯到當前執行緒的分散式事務的狀態
●enListResource(XAResource xaRes, int flag)- 將事務資源加入到當前的事務中(在上述示例中,在對資料庫 A 操作時 其所代表的事務資源將被關聯到當前事務中,同樣,在對資料庫 B 操作時其所代表的事務資源也將被關聯到當前事務中)
●delistResourc(XAResource xaRes, int flag)- 將事務資源從當前事務中刪除
●registerSynchronization(Synchronization sync)- 回撥介面,Hibernate 等 ORM 工具都有自己的事務控制機制來保證事務, 但同時它們還需要一種回撥機制以便在事務完成時得到通知從而觸發一些處理工作,如清除快取等。這就涉及到了 Transaction 的回撥介面 registerSynchronization。工具可以通過此介面將回撥程式注入到事務中,當事務成功提交後,回撥程式將被啟用。
TransactionManager 本身並不承擔實際的事務處理功能,它更多的是充當使用者介面和實現介面之間的橋樑。下面列出了 TransactionManager 中定義的方法,可以看到此介面中的大部分事務方法與 UserTransaction 和 Transaction 相同。 在開發人員呼叫 UserTransaction.begin() 方法時 TransactionManager 會建立一個 Transaction 事務物件(標誌著事務的開始)並把此物件通過 ThreadLocale 關聯到當前執行緒上;同樣 UserTransaction.commit() 會呼叫 TransactionManager.commit(), 方法將從當前執行緒下取出事務物件 Transaction 並把此物件所代表的事務提交, 即呼叫 Transaction.commit()
●begin()- 開始事務
●commit()- 提交事務
●rollback()- 回滾事務
●getStatus()- 返回當前事務狀態
●setRollbackOnly()
●getTransaction()- 返回關聯到當前執行緒的事務
●setTransactionTimeout(int seconds)- 設定事務超時時間
●resume(Transaction tobj)- 繼續當前執行緒關聯的事務
●suspend()- 掛起當前執行緒關聯的事務
在系統開發過程中會遇到需要將事務資源暫時排除的操作,此時就需要呼叫 suspend() 方法將當前的事務掛起:在此方法後面所做的任何操作將不會被包括在事務中,在非事務性操作完成後呼叫 resume()以繼續事務(注: 要進行此操作需要獲得 TransactionManager 物件, 其獲得方式在不同的 J2EE 應用伺服器上是不一樣的)
下面將通過具體的程式碼向讀者介紹 JTA 實現原理。下圖列出了示例實現中涉及到的 Java 類,其中 UserTransactionImpl 實現了 UserTransaction 介面,TransactionManagerImpl 實現了 TransactionManager 介面,TransactionImpl 實現了 Transaction 介面
圖 2. JTA 實現類圖
清單 3. 開始事務 – UserTransactionImpl implenments UserTransaction
1 2 3 4 |
public void begin() throws NotSupportedException, SystemException { // 將開始事務的操作委託給 TransactionManagerImpl TransactionManagerImpl.singleton().begin(); } |
清單 4. 開始事務 – TransactionManagerImpl implements TransactionManager
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// 此處 transactionHolder 用於將 Transaction 所代表的事務物件關聯到執行緒上 private static ThreadLocal<TransactionImpl> transactionHolder = new ThreadLocal<TransactionImpl>(); //TransacationMananger 必須維護一個全域性物件,因此使用單例項模式實現 private static TransactionManagerImpl singleton = new TransactionManagerImpl(); private TransactionManagerImpl(){ } public static TransactionManagerImpl singleton(){ return singleton; } public void begin() throws NotSupportedException, SystemException { //XidImpl 實現了 Xid 介面,其作用是唯一標識一個事務 XidImpl xid = new XidImpl(); // 建立事務物件,並將物件關聯到執行緒 TransactionImpl tx = new TransactionImpl(xid); transactionHolder.set(tx); } |
現在我們就可以理解 Transaction 介面上沒有定義 begin 方法的原因了:Transaction 物件本身就代表了一個事務,在它被建立的時候就表明事務已經開始,因此也就不需要額外定義 begin() 方法了。
清單 5. 提交事務 – UserTransactionImpl implenments UserTransaction
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public void commit() throws RollbackException, HeuristicMixedException, HeuristicRollbackException, SecurityException, IllegalStateException, SystemException { // 檢查是否是 Roll back only 事務,如果是回滾事務 if(rollBackOnly){ rollback(); return; } else { // 將提交事務的操作委託給 TransactionManagerImpl TransactionManagerImpl.singleton().commit(); } } |
清單 6. 提交事務 – TransactionManagerImpl implenments TransactionManager
1 2 3 4 5 6 7 |
public void commit() throws RollbackException, HeuristicMixedException, HeuristicRollbackException, SecurityException, IllegalStateException, SystemException { // 取得當前事務所關聯的事務並通過其 commit 方法提交 TransactionImpl tx = transactionHolder.get(); tx.commit(); |
同理, rollback、getStatus、setRollbackOnly 等方法也採用了與 commit() 相同的方式實現。 UserTransaction 物件不會對事務進行任何控制, 所有的事務方法都是通過 TransactionManager 傳遞到實際的事務資源即 Transaction 物件上。
上述示例演示了 JTA 事務的處理過程,下面將為您展示事務資源(資料庫連線,JMS)是如何以透明的方式加入到 JTA 事務中的。首先需要明確的一點是,在 JTA 事務 程式碼中獲得的資料庫源 ( DataSource ) 必須是支援分散式事務的。在如下的程式碼示例中,儘管所有的資料庫操作都被包含在了 JTA 事務中,但是因為 MySql 的資料庫連線是通過本地方式獲得的,對 MySql 的任何更新將不會被自動包含在全域性事務中。
清單 7. JTA 事務處理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
public void transferAccount() { UserTransaction userTx = null; Connection mySqlConnection = null; Statement mySqlStat = null; Connection connB = null; Statement stmtB = null; try{ // 獲得 Transaction 管理物件 userTx = (UserTransaction)getContext().lookup("java:comp/UserTransaction"); // 以本地方式獲得 mySql 資料庫連線 mySqlConnection = DriverManager.getConnection("localhost:1111"); // 從資料庫 B 中取得資料庫連線, getDataSourceB 返回應用伺服器的資料來源 connB = getDataSourceB().getConnection(); // 啟動事務 <strong>userTx.begin();</strong> // 將 A 賬戶中的金額減少 500 //mySqlConnection 是從本地獲得的資料庫連線,不會被包含在全域性事務中 mySqlStat = mySqlConnection.createStatement(); mySqlStat.execute(" update t_account set amount = amount - 500 where account_id = 'A'"); //connB 是從應用伺服器得的資料庫連線,會被包含在全域性事務中 stmtB = connB.createStatement(); stmtB.execute(" update t_account set amount = amount + 500 where account_id = 'B'"); // 事務提交:connB 的操作被提交,mySqlConnection 的操作不會被提交 <strong>userTx.commit();</strong> } catch(SQLException sqle){ // 處理異常程式碼 } catch(Exception ne){ e.printStackTrace(); } } |
為什麼必須從支援事務的資料來源中獲得的資料庫連線才支援分散式事務呢?其實支援事務的資料來源與普通的資料來源是不同的,它實現了額外的 XADataSource 介面。我們可以簡單的將 XADataSource 理解為普通的資料來源(繼承了 java.sql.PooledConnection),只是它為支援分散式事務而增加了 getXAResource 方法。另外,由 XADataSource 返回的資料庫連線與普通連線也是不同的,此連線除了實現 java.sql.Connection 定義的所有功能之外還實現了 XAConnection 介面。我們可以把 XAConnection 理解為普通的資料庫連線,它支援所有 JDBC 規範的資料庫操作,不同之處在於 XAConnection 增加了對分散式事務的支援。通過下面的類圖讀者可以對這幾個介面的關係有所瞭解:
圖 3. 事務資源類圖
應用程式從支援分散式事務的資料來源獲得的資料庫連線是 XAConnection 介面的實現,而由此資料庫連線建立的會話(Statement)也為了支援分散式事務而增加了功能,如下程式碼所示:
清單 8. JTA 事務資源處理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
public void transferAccount() { UserTransaction userTx = null; Connection conn = null; Statement stmt = null; try{ // 獲得 Transaction 管理物件 userTx = (UserTransaction)getContext().lookup(" java:comp/UserTransaction"); // 從資料庫中取得資料庫連線, getDataSourceB 返回支援分散式事務的資料來源 conn = getDataSourceB().getConnection(); // 會話 stmt 已經為支援分散式事務進行了功能增強 stmt = conn.createStatement(); // 啟動事務 userTx.begin(); stmt.execute("update t_account ... where account_id = 'A'"); userTx.commit(); } catch(SQLException sqle){ // 處理異常程式碼 } catch(Exception ne){ e.printStackTrace(); } } |
我們來看一下由 XAConnection 資料庫連線建立的會話(Statement)部分的程式碼實現(不同的 JTA 提供商會有不同的實現方式,此處程式碼示例只是向您演示事務資源是如何被自動加入到事務中)。 我們以會話物件的 execute 方法為例,通過在方法開始部分增加對 associateWithTransactionIfNecessary 方法的呼叫,即可以保證在 JTA 事務期間,對任何資料庫連線的操作都會被透明的加入到事務中。
清單 9. 將事務資源自動關聯到事務物件 – XAStatement implements Statement
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
public void execute(String sql) { // 對於每次資料庫操作都檢查此會話所在的資料庫連線是否已經被加入到事務中 associateWithTransactionIfNecessary(); try{ // 處理資料庫操作的程式碼 .... } catch(SQLException sqle){ // 處理異常程式碼 } catch(Exception ne){ e.printStackTrace(); } } public void associateWithTransactionIfNecessary(){ // 獲得 TransactionManager TransactionManager tm = getTransactionManager(); Transaction tx = tm.getTransaction(); // 檢查當前執行緒是否有分散式事務 if(tx != null){ // 在分散式事務內,通過 tx 物件判斷當前資料連線是否已經被包含在事務中, //如果不是那麼將此連線加入到事務中 Connection conn = this.getConnection(); //tx.hasCurrentResource, xaConn.getDataSource() 不是標準的 JTA // 介面方法,是為了實現分散式事務而增加的自定義方法 if(!tx.hasCurrentResource(conn)){ XAConnection xaConn = (XAConnection)conn; XADataSource xaSource = xaConn.getDataSource(); // 呼叫 Transaction 的介面方法,將資料庫事務資源加入到當前事務中 tx.enListResource(xaSource.getXAResource(), 1); } } } |
XAResource 與 Xid: XAResource 是 Distributed Transaction Processing: The XA Specification 標準的 Java 實現,它是對底層事務資源的抽象,定義了分散式事務處理過程中事務管理器和資源管理器之間的協議,各事務資源提供商(如 JDBC 驅動,JMS)將提供此介面的實現。使用此介面,開發人員可以通過自己的程式設計實現分散式事務處理,但這些通常都是由應用伺服器實現的(伺服器自帶實現更加高效,穩定) 為了說明,我們將舉例說明他的使用方式。
在使用分散式事務之前,為了區分事務使之不發生混淆,必須實現一個 Xid 類用來標識事務,可以把 Xid 想象成事務的一個標誌符,每次在新事務建立是都會為事務分配一個 Xid,Xid 包含三個元素:formatID、gtrid(全域性事務識別符號)和 bqual(分支修飾詞識別符號)。 formatID 通常是零,這意味著你將使用 OSI CCR(Open Systems Interconnection Commitment, Concurrency 和 Recovery 標準)來命名;如果你要使用另外一種格式,那麼 formatID 應該大於零,-1 值意味著 Xid 為無效。
gtrid 和 bqual 分別包含 64 個位元組二進位制碼來分別標識全域性事務和分支事務, 唯一的要求是 gtrid 和 bqual 必須是全域性唯一的。
XAResource 介面中主要定義瞭如下方法:
●commit()- 提交事務
●isSameRM(XAResource xares)- 檢查當前的 XAResource 與引數是否同一事務資源
●prepare()- 通知資源管理器準備事務的提交工作
●rollback()- 通知資源管理器回滾事務
在事務被提交時,Transaction 物件會收集所有被當前事務包含的 XAResource 資源,然後呼叫資源的提交方法,如下程式碼所示:
清單 10. 提交事務 – TransactionImpl implements Transaction
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public void commit() throws RollbackException, HeuristicMixedException, HeuristicRollbackException, SecurityException, IllegalStateException, SystemException { // 得到當前事務中的所有事務資源 List<XAResource> list = getAllEnlistedResouces(); // 通知所有的事務資源管理器,準備提交事務 // 對於生產級別的實現,此處需要進行額外處理以處理某些資源準備過程中出現的異常 for(XAResource xa : list){ xa.prepare(); } // 所有事務性資源,提交事務 for(XAResource xa : list){ xa.commit(); } } |
結束語
通過如上介紹相信讀者對 JTA 的原理已經有所瞭解,本文中的示例程式碼都是理想情況下的假設實現。一款完善成熟的 JTA 事務實現需要考慮與處理的細節非常多,如效能(提交事務的時候使用多執行緒方式併發提交事務)、容錯(網路,系統異常)等, 其成熟也需要經過較長時間的積累。感興趣的讀者可以閱讀一些開源 JTA 實現以進一步深入學習。