tcc-transaction分散式TCC型事務框架搭建與使用記錄

追尋北極發表於2017-11-21

首先感謝框架作者。本文只介紹框架使用,並不分析原始碼。

介紹:tcc-transaction是開源的TCC補償性分散式事務框架,git地址https://github.com/changmingxie/tcc-transaction 

TCC為Try、Confirm、Cancel的縮寫:try階段預留資源嘗試提交,confirm階段確定提交,cancel取消提交釋放資源。

1.2.x專案指南地址:https://github.com/changmingxie/tcc-transaction/wiki/%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%971.2.x 


本文的例子為:建立資產,將資產資訊同時同步到Mongo與ES的流程(ES程式碼不列出了,與mongo類似),整個流程保證資料一致(本場景可能並不適合,看一下程式碼結構即可)。


程式碼與流程:

1.下載1.2.x版本原始碼,並可能需要修改部分程式碼

因為是第三方包,所以需要自己打包到本地倉庫。但包中spring版本3.2.12.RELEASE,如果本地專案為4.x,比如本人的專案spring版本為4.3.4.RELEASE,如果不修改tcc中的spring版本,將報錯無法啟動。

原始碼修改比較簡單,如下:

1.1 修改tcc-transaction總pom.xml檔案 

 

[html] view plain copy
  1. <!-- 第一處:修改版本為4.3.4  -->  
  2. <springframework.version>4.3.4.RELEASE</springframework.version>  
  3.   
  4. <!-- 第二處:修改版本為2.2.1  -->  
  5. <dependency>  
  6.       <groupId>org.quartz-scheduler</groupId>  
  7.       <artifactId>quartz</artifactId>  
  8.       <version>2.2.1</version>  
  9.       <exclusions>  
  10.           <exclusion>  
  11.               <groupId>c3p0</groupId>  
  12.               <artifactId>c3p0</artifactId>  
  13.           </exclusion>  
  14.       </exclusions>  
  15. </dependency>  
  16.   
  17. <!-- 第三處:修改版本為2.5.3  -->  
  18. <dependency>  
  19.        <groupId>com.alibaba</groupId>  
  20.        <artifactId>dubbo</artifactId>  
  21.        <version>2.5.3</version>  
  22. </dependency>  

1.2 修改 tcc-transaction-spring/src/main/java/org/mengyun/tcctransaction/spring/recover/RecoverScheduledJob.java 

該檔案中 CronTriggerBean類在4.x中已經不存在,也是修改原始碼主要修改的地方。

修改其中的init方法,修改後如下:

[java] view plain copy
  1. public void init() {  
  2.   
  3.         try {  
  4.             MethodInvokingJobDetailFactoryBean jobDetail = new MethodInvokingJobDetailFactoryBean();  
  5.             jobDetail.setTargetObject(transactionRecovery);  
  6.             jobDetail.setTargetMethod("startRecover");  
  7.             jobDetail.setName("transactionRecoveryJob");  
  8.             jobDetail.setConcurrent(false);  
  9.             jobDetail.afterPropertiesSet();  
  10.   
  11.             CronTriggerFactoryBean cronTrigger = new CronTriggerFactoryBean();  
  12.             cronTrigger.setBeanName("transactionRecoveryCronTrigger");  
  13.             cronTrigger.setJobDetail(jobDetail.getObject());  
  14.   
  15.             cronTrigger.setCronExpression(transactionConfigurator.getRecoverConfig().getCronExpression());  
  16.             cronTrigger.afterPropertiesSet();  
  17.   
  18.             scheduler.scheduleJob(jobDetail.getObject(), cronTrigger.getObject());  
  19.   
  20.             scheduler.start();  
  21.   
  22.         } catch (Exception e) {  
  23.             throw new SystemException(e);  
  24.         }  
  25.     }  
也可參考如下的修改:https://github.com/changmingxie/tcc-transaction/pull/84/files 


1.3 打包併發布

mvn -Dmaven.test.skip=true install


2.專案依賴

參考1.2.x使用指南,引入兩個依賴(本人專案使用SOA架構dubbo框架,我使用並打包時版本為1.2.3.1)。呼叫方和提供方都需要引入依賴。


3.載入tcc-transaction.xml配置

原文中是配置在web.xml中,我個人試了一下放在dubbo web專案的web.xml中,但配置並沒有被載入。該檔案的意義只是希望專案啟動時被載入,於是直接在dubbo中的一個spring的配置檔案中引入,如下:

[html] view plain copy
  1. <!-- TCC Transaction -->  
  2. <import resource="classpath:tcc-transaction.xml" />  
該檔案裡面提供各種aop邏輯,專案啟動時掃描指定註解,並做增強。


4.設定TransactionRepository

需要為tcc配置資料來源,可以是mysql或其他nosql,本文使用msyql,其他可參見原指南文件。

mysql配置如下:

[html] view plain copy
  1. <!--tcc-->  
  2. <bean id="tccDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">  
  3.     <property name="driverClassName" value="${jdbc.driverClassName}" />  
  4.     <property name="url" value="${jdbc.tcc.url}" />  
  5.     <property name="username" value="${jdbc.username}" />  
  6.     <property name="password" value="${jdbc.password}" />  
  7.     <property name="initialSize" value="${dbcp.initialSize}" />  
  8.     <property name="maxActive" value="${dbcp.maxActive}" />  
  9.     <property name="maxIdle" value="${dbcp.maxIdle}" />  
  10.     <property name="maxWait" value="${dbcp.maxWait}" />  
  11.     <property name="poolPreparedStatements" value="${dbcp.poolPreparedStatements}" />  
  12.     <property name="defaultAutoCommit" value="${dbcp.defaultAutoCommit}" />  
  13.     <property name="timeBetweenEvictionRunsMillis" value="${dbcp.timeBetweenEvictionRunsMillis}" />  
  14.     <property name="minEvictableIdleTimeMillis" value="${dbcp.minEvictableIdleTimeMillis}" />  
  15. </bean>  
  16.   
  17. <bean id="transactionRepository"  
  18.       class="org.mengyun.tcctransaction.spring.repository.SpringJdbcTransactionRepository">  
  19.     <property name="dataSource" ref="tccDataSource"/>  
  20.     <property name="domain" value="SAAS"/>  
  21.     <property name="tbSuffix" value="_ASSET"/>  
  22. </bean>  
  23.   
  24. <bean class="org.mengyun.tcctransaction.spring.recover.DefaultRecoverConfig">  
  25.     <property name="maxRetryCount" value="30"/>  
  26.     <property name="recoverDuration" value="120"/>  
  27.     <property name="cronExpression" value="0 */1 * * * ?"/>  
  28.     <property name="delayCancelExceptions">  
  29.         <util:set>  
  30.             <value>com.alibaba.dubbo.remoting.TimeoutException</value>  
  31.         </util:set>  
  32.     </property>  
  33. </bean>  
需要注意的點:1.資料來源必須配置新的,不能使用之前專案存在的dataSource的bean,也不能在同一庫中,不然會導致tcc表資料與本地事務一起回滾,從而無法儲存異常事務日誌;2.注意domain、tbSuffix的配置,這兩項文件中並沒有配置,但原始碼demo中配置了,用於資料庫的表名稱等,推薦配置;3.最後的DefaultRecoverConfig項是可選的,用於恢復與重試,具體作用參考使用指南;4.defaultAutoCommit必須為true(預設為true)


5.mysql建表指令碼

根據以上的tbSufifix配置,指令碼如下:

[sql] view plain copy
  1. CREATE TABLE `tcc_transaction_asset` (  
  2.   `TRANSACTION_ID` int(11) NOT NULL AUTO_INCREMENT,  
  3.   `DOMAIN` varchar(100) DEFAULT NULL,  
  4.   `GLOBAL_TX_ID` varbinary(32) NOT NULL,  
  5.   `BRANCH_QUALIFIER` varbinary(32) NOT NULL,  
  6.   `CONTENT` varbinary(8000) DEFAULT NULL,  
  7.   `STATUS` int(11) DEFAULT NULL,  
  8.   `TRANSACTION_TYPE` int(11) DEFAULT NULL,  
  9.   `RETRIED_COUNT` int(11) DEFAULT NULL,  
  10.   `CREATE_TIME` datetime DEFAULT NULL,  
  11.   `LAST_UPDATE_TIME` datetime DEFAULT NULL,  
  12.   `VERSION` int(11) DEFAULT NULL,  
  13.   PRIMARY KEY (`TRANSACTION_ID`),  
  14.   UNIQUE KEY `UX_TX_BQ` (`GLOBAL_TX_ID`,`BRANCH_QUALIFIER`)  
  15. ) ENGINE=InnoDB DEFAULT CHARSET=utf8  

如果表名稱不對,啟動過程會報錯,做相應調整即可。


6.釋出服務(重點)

6.1 dubbo介面

[java] view plain copy
  1. public interface AssetCardService {  
  2.   
  3.     /** 
  4.      * 測試預儲存資產(狀態為待確認) 
  5.      */  
  6.     @Compensable  
  7.     int testSaveAssetCard(AssetCardModel model);  
  8.   
  9.     /** 
  10.      * 確認儲存資產到mysql(狀態為正常) 
  11.      */  
  12.     int confirmMysqlSaveAssetCard(AssetCardModel model);  
  13.   
  14.     /** 
  15.      * 取消儲存資產到msyql(更新狀態為刪除) 
  16.      */  
  17.     int cancelMysqlSaveAssetCard(AssetCardModel model);  
  18.   
  19.     /** 
  20.      * 預儲存資產到mongo(狀態為待確認) 
  21.      */  
  22.     @Compensable  
  23.     void processMongo(AssetCardModel model);  
  24.   
  25.     /** 
  26.      * 確認儲存資產到mongo(狀態為正常) 
  27.      */  
  28.     void confirmMongoSaveAssetCard(AssetCardModel model);  
  29.   
  30.     /** 
  31.      * 取消儲存資產到mongo(更新狀態為刪除) 
  32.      */  
  33.     void cancelMongoSaveAssetCard(AssetCardModel model);  
  34.   
  35. }  

需要注意的點:1.對外提供服務的介面必須有@Compensable註解;2.對應的confirm與cancel方法必須宣告為介面,不能宣告為private,即使是public也不行,必須有介面。


6.2 dubbo介面實現類

[java] view plain copy
  1. @Override  
  2. @Compensable(confirmMethod = "confirmMysqlSaveAssetCard", cancelMethod = "cancelMysqlSaveAssetCard", transactionContextEditor = DubboTransactionContextEditor.class)  
  3. @Transactional(propagation = Propagation.REQUIRED, rollbackFor = { Exception.class })  
  4. public int testSaveAssetCard(AssetCardModel model){  
  5.   
  6.     // 儲存mysql,data狀態為-1  
  7.     model.setDataStatus(-1);  
  8.     assetCardDao.insert(model);  
  9.   
  10.   
  11.     // mongo處理  
  12.     assetCardService.processMongo(model);  
  13.   
  14.     return model.getId();  
  15. }  
  16.   
  17. @Override  
  18. public int confirmMysqlSaveAssetCard(AssetCardModel model){  
  19.     System.out.println("============================================================================");  
  20.     System.out.println("=================mysql:confirm");  
  21.     System.out.println("============================================================================");  
  22.   
  23.     // 更新mysql data_status為0  
  24.     model.setDataStatus(0);  
  25.     assetCardDao.updateByPrimaryKey(model);  
  26.   
  27.     return model.getId();  
  28. }  
  29.   
  30. @Override  
  31. public int cancelMysqlSaveAssetCard(AssetCardModel model){  
  32.     System.out.println("============================================================================");  
  33.     System.out.println("=================mysql:cancel");  
  34.     System.out.println("============================================================================");  
  35.   
  36.     // 更新mysql data_status為-1  
  37.     model.setDataStatus(-1);  
  38.     assetCardDao.updateByPrimaryKey(model);  
  39.   
  40.     return model.getId();  
  41. }  
  42.   
  43. @Compensable(confirmMethod = "confirmMongoSaveAssetCard", cancelMethod = "cancelMongoSaveAssetCard", transactionContextEditor = DubboTransactionContextEditor.class)  
  44. @Override  
  45. public void processMongo(AssetCardModel model) {  
  46.   
  47.     // 儲存mongo,data_statu為-1  
  48.     model.setDataStatus(-1);  
  49.     assetCardDaoWrapper.saveMongo(model);  
  50. }  
  51.   
  52. @Override  
  53. public void confirmMongoSaveAssetCard(AssetCardModel model){  
  54.     System.out.println("============================================================================");  
  55.     System.out.println("=================mongo:confirm");  
  56.     System.out.println("============================================================================");  
  57.   
  58.     // 更新mongo data_status為0  
  59.     model.setDataStatus(0);  
  60.     assetCardDaoWrapper.updateMongo(model);  
  61. }  
  62.   
  63. @Override  
  64. public void cancelMongoSaveAssetCard(AssetCardModel model){  
  65.     System.out.println("============================================================================");  
  66.     System.out.println("=================mongo:cancel");  
  67.     System.out.println("============================================================================");  
  68.   
  69.     // 更新mongo data_status為-1  
  70.     model.setDataStatus(-1);  
  71.     assetCardDao.updateByPrimaryKey(model);  
  72.     assetCardDaoWrapper.updateMongo(model);  
  73. }  


說明與注意點:1.對外提供服務的介面必須有@Compensable註解,同時必須有confirmMethod、cancelMethod引數的配置,同時dubbo介面額外增加 "transactionContextEditor = DubboTransactionContextEditor.class"這個配置;2.提供服務介面與對應另外的兩個CC方法引數必須完全一致;3.該tcc框架可巢狀呼叫,如上在testSaveAssetCard方法,即try階段中呼叫了另一個tcc方法"assetCardService.processMongo()",理論上巢狀只應該在try階段進行;4.confirm、cancel需要實現冪等性,可能會被重試;5.由於網路等因素,可能導致cancel方法先執行,cancel方法一定要做好相應的判斷與處理


6.3 呼叫方

[java] view plain copy
  1. @Override  
  2. @Transactional(propagation = Propagation.REQUIRED, rollbackFor = { Exception.class })  
  3. public long testSaveAssetCard(AssetCardModel assetCardModel) throws AssetException {  
  4.       
  5.     assetCardModel.setId(IdGenerator.getId());    
  6.     return assetCardService.testSaveAssetCard(assetCardModel);  
  7. }  
注意點:1.因為需要回滾更新等操作,所以此業務中id不能用自增,而是需要專案生成;2.特別注意,呼叫方必須在事務中,也就是說必須有事務註解,或者能被事務配置切到,沒有事務tcc框架呼叫時會拋異常。

至此,配置已經全部完成。


7.事務檢視

原始碼中提供tcc-transaction-server web專案,該專案提供介面檢視事務日誌,修改裡面配置檔案,打包後部署即可,不再贅述。


備:

業務流程使用記錄:

前提:使用者下單,建立訂單,建立支付記錄,支付記錄狀態為待支付

try:

    使用者金額凍結

    呼叫積分處理TCC:

    try:預增加積分

    confirm:更新增加積分狀態

    cancel:取消增加的積分

 confirm:

    訂單支付狀態更新為已支付

    訂單支付記錄支付狀態更新為已支付 

    使用者金額扣款(以上三個操作在同一本地事務)

cancel:

    判斷訂單支付狀態與訂單記錄支付狀態為未支付
    使用者凍結金額釋放

相關文章