suspend()- 掛起當前線程關聯的事務在系統開發過程中會遇到需要將事務資源暫時排除的操作,此時就需要調用 suspend() 方法將當前的事務掛起:在此方法後面所做的任何操作將不會被包括在事務中,在非事務性操作完成後調用 resume()以繼續事務(註: 要進行此操作需要獲得 TransactionManager 對象, 其獲得方式在不同的 J2EE 應用伺服器上是不一樣的)
下面將通過具體的代碼向讀者介紹 JTA 實現原理。列出了樣本實現中涉及到的 Java 類,其中 UserTransactionImpl 實現了 UserTransaction 介面,TransactionManagerImpl 實現了 TransactionManager 介面,TransactionImpl 實現了 Transaction 介面
圖 2. JTA 實作類別圖清單 3. 開始事務 - UserTransactionImpl implenments UserTransactionpublic void begin() throws NotSupportedException, SystemException { // 將開始事務的操作委託給 TransactionManagerImpl TransactionManagerImpl.singleton().begin(); }
清單 4. 開始事務 - TransactionManagerImpl implements TransactionManager// 此處 transactionHolder 用於將 Transaction 所代表的事務對象關聯到線程上private static ThreadLocal transactionHolder = new ThreadLocal(); //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 UserTransactionpublic void commit() throws RollbackException, HeuristicMixedException, HeuristicRollbackException, SecurityException, IllegalStateException, SystemException { // 檢查是否是 Roll back only 事務,如果是復原事務 if(rollBackOnly){ rollback(); return; } else { // 將提交事務的操作委託給 TransactionManagerImpl TransactionManagerImpl.singleton().commit(); } }
清單 6. 提交事務 - TransactionManagerImpl implenments TransactionManagerpublic 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 交易處理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(); // 啟動事務 userTx.begin(); // 將 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 的操作不會被提交 userTx.commit(); } 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 事務資源處理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 Statementpublic 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 Transactionpublic void commit() throws RollbackException, HeuristicMixedException, HeuristicRollbackException, SecurityException, IllegalStateException, SystemException { // 得到當前事務中的所有事務資源 List list = getAllEnlistedResouces(); // 通知所有的事務資源管理員,準備提交事務 // 對於生產層級的實現,此處需要進行額外處理以處理某些資源準備過程中出現的異常 for(XAResource xa : list){ xa.prepare(); } // 所有事務性資源,提交事務 for(XAResource xa : list){ xa.commit(); } }
結束語通過如上介紹相信讀者對 JTA 的原理已經有所瞭解,本文中的範例程式碼都是理想情況下的假設實現。一款完善成熟的 JTA 事務實現需要考慮與處理的細節非常多,如效能(提交事務的時候使用多線程方式並發提交事務)、容錯(網路,系統異常)等, 其成熟也需要經過較長時間的積累。感興趣的讀者可以閱讀一些開源 JTA 實現以進一步深入學習。