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. 本地事務處理實例
 public void transferAccount() { 
		 Connection conn = null; 
		 Statement stmt = null; 
		 try{ 
			 conn = getDataSource().getConnection(); 
			 // 將自動提交設置爲 false,
			 //若設置爲 true 則數據庫將會把每一次數據更新認定爲一個事務並自動提交
			 conn.setAutoCommit(false);
			
			 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'");
			
			 // 提交事務
		     conn.commit();
			 // 事務提交:轉賬的兩步操作同時成功
		 } catch(SQLException sqle){ 			
			 try{ 
				 // 發生異常,回滾在本事務中的操做
                conn.rollback();
				 // 事務回滾:轉賬的兩步操作完全撤銷
                 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 事務處理
 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 體系架構
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 實現類圖
Transaction 類圖
清單 3. 開始事務 - UserTransactionImpl implenments UserTransaction
public void begin() throws NotSupportedException, SystemException { 
   // 將開始事務的操作委託給 TransactionManagerImpl 
   TransactionManagerImpl.singleton().begin(); 
     }
清單 4. 開始事務 - TransactionManagerImpl implements TransactionManager
// 此處 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
     	 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
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 事務處理
 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. 事務資源類圖
Transaction 類圖

應用程序從支持分佈式事務的數據源獲得的數據庫連接是 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 Statement
 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
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 實現以進一步深入學習。


來源 http://www.ibm.com/developerworks/cn/java/j-lo-jta/

發佈了158 篇原創文章 · 獲贊 75 · 訪問量 50萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章