分佈式事務之——tcc-transaction分佈式TCC型事務框架搭建與實戰案例(基於Dubbo/Dubbox)

轉載請註明出處:http://blog.csdn.net/l1028386804/article/details/73731363java

1、背景

有必定分佈式開發經驗的朋友都知道,產品/項目/系統最初爲了可以快速迭代上線,每每不太注重產品/項目/系統的高可靠性、高性能與高擴展性,採用單體應用和單實例數據庫的架構方式快速迭代開發;當產品/項目/系統作到必定規模的時候,原有的系統架構則不足以支撐義務發展須要,每每相同的業務則須要重複寫不少次,致使代碼大量冗餘,難以維護和擴展,這時不得不對原有產品/項目/系統進行拆分,引入分佈式的系統架構;而對原有產品/項目/系統進行拆分的過程當中,對於業務和數據的拆分和遷移則成爲了最爲棘手的問題,尤爲是在原有業務不能下線,拆分後的業務同時上線的場景下這種問題更加突出;項目拆分後,業務被拆分爲多個獨立的子業務分散到多個子系統中,而原有的單一數據庫則被拆分到多個數據庫中,拆分後的數據庫則一樣又面臨着讓人頭疼的分佈式事務的問題。mysql

本文就針對項目拆分後數據庫的分佈式事務問題,基於tcc-transaction分佈式TCC型事務進行框架的搭建,同時引入相關的實戰案例,來解決讓人頭疼的分佈式事務問題。git

2、tcc-transaction框架介紹

介紹: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相似),整個流程保證數據一致github

3、項目流程

1.下載1.2.x版本源碼,並可能須要修改部分代碼

由於是第三方包,因此須要本身打包到本地倉庫。但包中spring版本爲3.2.12.RELEASE,若是本地項目爲4.x,好比本人的項目spring版本爲4.3.4.RELEASE,若是不修改tcc中的spring版本,將報錯沒法啓動,因此須要對原有框架源碼進行相應的修改。
源碼修改比較簡單,以下web

1.1 修改tcc-transaction總pom.xml文件
  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方法,修改後以下:spring

  1. public void init() {
  2. try {
  3. MethodInvokingJobDetailFactoryBean jobDetail = new MethodInvokingJobDetailFactoryBean();
  4. jobDetail.setTargetObject(transactionRecovery);
  5. jobDetail.setTargetMethod( "startRecover");
  6. jobDetail.setName( "transactionRecoveryJob");
  7. jobDetail.setConcurrent( false);
  8. jobDetail.afterPropertiesSet();
  9.  
  10. CronTriggerFactoryBean cronTrigger = new CronTriggerFactoryBean();
  11. cronTrigger.setBeanName( "transactionRecoveryCronTrigger");
  12. cronTrigger.setJobDetail(jobDetail.getObject());
  13.  
  14. cronTrigger.setCronExpression(transactionConfigurator.getRecoverConfig().getCronExpression());
  15. cronTrigger.afterPropertiesSet();
  16.  
  17. scheduler.scheduleJob(jobDetail.getObject(), cronTrigger.getObject());
  18.  
  19. scheduler.start();
  20.  
  21. } catch (Exception e) {
  22. throw new SystemException(e);
  23. }
  24. }

各位也可參考以下的修改:https://github.com/changmingxie/tcc-transaction/pull/84/files sql

1.3 打包併發布

這裏咱們經過Maven進行打包發佈,命令爲:數據庫

mvn -Dmaven.test.skip=true install

2.項目依賴

參考1.2.x使用指南,引入兩個依賴(本人項目dubbo/dubbox框架,我使用並打包時版本爲1.2.3.1)。調用方和提供方都須要引入依賴。apache

3.加載tcc-transaction.xml配置

原文中是配置在web.xml中,我我的試了一下放在dubbo web項目的web.xml中,但配置並無被加載。該文件的意義只是但願項目啓動時被加載,因而直接在dubbo中的一個spring的配置文件中引入,以下:網絡

  1. <!-- TCC Transaction -->
  2. <import resource="classpath:tcc-transaction.xml" />

該文件裏面提供各類aop邏輯,項目啓動時掃描指定註解,並作加強。

4.設置TransactionRepository

須要爲tcc配置數據源,能夠是MySQL或其餘nosql,本文使用mysql,其餘可參見原指南文檔。
mysql配置以下:

  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配置,腳本以下:

  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接口
  1. /**
  2. * @author liuyazhuang
  3. * 資產相關的業務發佈Dubbo服務
  4. */
  5. public interface AssetCardService {
  6.  
  7. /**
  8. * 測試預保存資產(狀態爲待確認)
  9. */
  10. @Compensable
  11. int testSaveAssetCard(AssetCardModel model);
  12.  
  13. /**
  14. * 確認保存資產到mysql(狀態爲正常)
  15. */
  16. int confirmMysqlSaveAssetCard(AssetCardModel model);
  17.  
  18. /**
  19. * 取消保存資產到msyql(更新狀態爲刪除)
  20. */
  21. int cancelMysqlSaveAssetCard(AssetCardModel model);
  22.  
  23. /**
  24. * 預保存資產到mongo(狀態爲待確認)
  25. */
  26. @Compensable
  27. void processMongo(AssetCardModel model);
  28.  
  29. /**
  30. * 確認保存資產到mongo(狀態爲正常)
  31. */
  32. void confirmMongoSaveAssetCard(AssetCardModel model);
  33.  
  34. /**
  35. * 取消保存資產到mongo(更新狀態爲刪除)
  36. */
  37. void cancelMongoSaveAssetCard(AssetCardModel model);
  38.  
  39. }

須要注意的點:
1.對外提供服務的接口必須有@Compensable註解;
2.對應的confirm與cancel方法必須聲明爲接口,不能聲明爲private,即便是public也不行,必須有接口。

6.2 dubbo接口實現類
  1. /**
  2. * @author liuyazhuang
  3. * 資產相關的業務發佈Dubbo服務的實現
  4. */
  5. @Service
  6. @Component
  7. public class AssetCardServiceImpl implements AssetCardService{
  8. @Override
  9. @Compensable(confirmMethod = "confirmMysqlSaveAssetCard", cancelMethod = "cancelMysqlSaveAssetCard", transactionContextEditor = DubboTransactionContextEditor.class)
  10. @Transactional(propagation = Propagation.REQUIRED, rollbackFor = { Exception.class })
  11. public int testSaveAssetCard(AssetCardModel model){
  12.  
  13. // 保存mysql,data狀態爲-1
  14. model.setDataStatus(- 1);
  15. assetCardDao.insert(model);
  16.  
  17.  
  18. // mongo處理
  19. assetCardService.processMongo(model);
  20.  
  21. return model.getId();
  22. }
  23.  
  24. @Override
  25. public int confirmMysqlSaveAssetCard(AssetCardModel model){
  26. System.out.println( "============================================================================");
  27. System.out.println( "=================mysql:confirm");
  28. System.out.println( "============================================================================");
  29.  
  30. // 更新mysql data_status爲0
  31. model.setDataStatus( 0);
  32. assetCardDao.updateByPrimaryKey(model);
  33.  
  34. return model.getId();
  35. }
  36.  
  37. @Override
  38. public int cancelMysqlSaveAssetCard(AssetCardModel model){
  39. System.out.println( "============================================================================");
  40. System.out.println( "=================mysql:cancel");
  41. System.out.println( "============================================================================");
  42.  
  43. // 更新mysql data_status爲-1
  44. model.setDataStatus(- 1);
  45. assetCardDao.updateByPrimaryKey(model);
  46.  
  47. return model.getId();
  48. }
  49.  
  50. @Compensable(confirmMethod = "confirmMongoSaveAssetCard", cancelMethod = "cancelMongoSaveAssetCard", transactionContextEditor = DubboTransactionContextEditor.class)
  51. @Override
  52. public void processMongo(AssetCardModel model) {
  53.  
  54. // 保存mongo,data_statu爲-1
  55. model.setDataStatus(- 1);
  56. assetCardDaoWrapper.saveMongo(model);
  57. }
  58.  
  59. @Override
  60. public void confirmMongoSaveAssetCard(AssetCardModel model){
  61. System.out.println( "============================================================================");
  62. System.out.println( "=================mongo:confirm");
  63. System.out.println( "============================================================================");
  64.  
  65. // 更新mongo data_status爲0
  66. model.setDataStatus( 0);
  67. assetCardDaoWrapper.updateMongo(model);
  68. }
  69.  
  70. @Override
  71. public void cancelMongoSaveAssetCard(AssetCardModel model){
  72. System.out.println( "============================================================================");
  73. System.out.println( "=================mongo:cancel");
  74. System.out.println( "============================================================================");
  75.  
  76. // 更新mongo data_status爲-1
  77. model.setDataStatus(- 1);
  78. assetCardDao.updateByPrimaryKey(model);
  79. assetCardDaoWrapper.updateMongo(model);
  80. }
  81. }

注意點:
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 調用方
  1. @Override
  2. @Transactional(propagation = Propagation.REQUIRED, rollbackFor = { Exception.class })
  3. public long testSaveAssetCard(AssetCardModel assetCardModel) throws AssetException {
  4. assetCardModel.setId(IdGenerator.getId());
  5. return assetCardService.testSaveAssetCard(assetCardModel);
  6. }

注意點:
1.由於須要回滾更新等操做,因此此業務中id不能用自增,而是須要項目生成;
2.特別注意,調用方必須在事務中,也就是說必須有事務註解,或者能被事務配置切到,沒有事務tcc框架調用時會拋異常。
至此,配置已經所有完成。

7.事務查看

源碼中提供tcc-transaction-server web項目,該項目提供界面查看事務日誌,打包後部署便可,咱們這裏就不在做詳細的描述。

4、TCC執行流程

業務流程使用記錄: 前提:用戶下單,創建訂單,建立支付記錄,支付記錄狀態爲待支付 try: 用戶金額凍結 調用積分處理TCC: try:預增長積分 confirm:更新增長積分狀態 cancel:取消增長的積分 confirm: 訂單支付狀態更新爲已支付 訂單支付記錄支付狀態更新爲已支付 用戶金額扣款(以上三個操做在同一本地事務) cancel: 判斷訂單支付狀態與訂單記錄支付狀態爲未支付 用戶凍結金額釋放

相關文章
相關標籤/搜索