天天看點

tcc-transaction分布式TCC型事務架構搭建與實戰案例(基于Dubbo/Dubbox)

一、背景

有一定分布式開發經驗的朋友都知道,産品/項目/系統最初為了能夠快速疊代上線,往往不太注重産品/項目/系統的高可靠性、高性能與高擴充性,采用單體應用和單執行個體資料庫的架構方式快速疊代開發;當産品/項目/系統做到一定規模的時候,原有的系統架構則不足以支撐義務發展需要,往往相同的業務則需要重複寫很多次,導緻代碼大量備援,難以維護和擴充,這時不得不對原有産品/項目/系統進行拆分,引入分布式的系統架構;而對原有産品/項目/系統進行拆分的過程中,對于業務和資料的拆分和遷移則成為了最為棘手的問題,尤其是在原有業務不能下線,拆分後的業務同時上線的場景下這種問題更加突出;項目拆分後,業務被拆分為多個獨立的子業務分散到多個子系統中,而原有的單一資料庫則被拆分到多個資料庫中,拆分後的資料庫則同樣又面臨着讓人頭疼的分布式事務的問題。

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

二、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類似),整個流程保證資料一緻

三、項目流程

1.下載下傳1.2.x版本源碼,并可能需要修改部分代碼

因為是第三方包,是以需要自己打包到本地倉庫。但包中spring版本為3.2.12.RELEASE,如果本地項目為4.x,比如本人的項目spring版本為4.3.4.RELEASE,如果不修改tcc中的spring版本,将報錯無法啟動,是以需要對原有架構源碼進行相應的修改。

源碼修改比較簡單,如下

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

<!-- 第一處:修改版本為4.3.4  -->
<springframework.version>4.3.4.RELEASE</springframework.version>
 
<!-- 第二處:修改版本為2.2.1  -->
<dependency>
      <groupId>org.quartz-scheduler</groupId>
      <artifactId>quartz</artifactId>
      <version>2.2.1</version>
      <exclusions>
          <exclusion>
              <groupId>c3p0</groupId>
              <artifactId>c3p0</artifactId>
          </exclusion>
      </exclusions>
</dependency>
 
<!-- 第三處:修改版本為2.5.3  -->
<dependency>
       <groupId>com.alibaba</groupId>
       <artifactId>dubbo</artifactId>
       <version>2.5.3</version>
</dependency>      

1.2 修改 Java類

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

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

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

public void init() {
    try {
        MethodInvokingJobDetailFactoryBean jobDetail = new MethodInvokingJobDetailFactoryBean();
        jobDetail.setTargetObject(transactionRecovery);
        jobDetail.setTargetMethod("startRecover");
        jobDetail.setName("transactionRecoveryJob");
        jobDetail.setConcurrent(false);
        jobDetail.afterPropertiesSet();
 
        CronTriggerFactoryBean cronTrigger = new CronTriggerFactoryBean();
        cronTrigger.setBeanName("transactionRecoveryCronTrigger");
        cronTrigger.setJobDetail(jobDetail.getObject());
 
        cronTrigger.setCronExpression(transactionConfigurator.getRecoverConfig().getCronExpression());
        cronTrigger.afterPropertiesSet();
 
        scheduler.scheduleJob(jobDetail.getObject(), cronTrigger.getObject());
 
        scheduler.start();
 
    } catch (Exception e) {
        throw new SystemException(e);
    }
}      

各位也可參考如下的修改:

https://github.com/changmingxie/tcc-transaction/pull/84/files
1.3 打包并釋出

這裡我們通過Maven進行打包釋出,指令為:

mvn -Dmaven.test.skip=true install      

2.項目依賴

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

3.加載tcc-transaction.xml配置

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

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

該檔案裡面提供各種aop邏輯,項目啟動時掃描指定注解,并做增強。

4.設定TransactionRepository‘

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

mysql配置如下:

<!--tcc-->
<bean id="tccDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="${jdbc.driverClassName}" />
    <property name="url" value="${jdbc.tcc.url}" />
    <property name="username" value="${jdbc.username}" />
    <property name="password" value="${jdbc.password}" />
    <property name="initialSize" value="${dbcp.initialSize}" />
    <property name="maxActive" value="${dbcp.maxActive}" />
    <property name="maxIdle" value="${dbcp.maxIdle}" />
    <property name="maxWait" value="${dbcp.maxWait}" />
    <property name="poolPreparedStatements" value="${dbcp.poolPreparedStatements}" />
    <property name="defaultAutoCommit" value="${dbcp.defaultAutoCommit}" />
    <property name="timeBetweenEvictionRunsMillis" value="${dbcp.timeBetweenEvictionRunsMillis}" />
    <property name="minEvictableIdleTimeMillis" value="${dbcp.minEvictableIdleTimeMillis}" />
</bean>
 
<bean id="transactionRepository"
      class="org.mengyun.tcctransaction.spring.repository.SpringJdbcTransactionRepository">
    <property name="dataSource" ref="tccDataSource"/>
    <property name="domain" value="SAAS"/>
    <property name="tbSuffix" value="_ASSET"/>
</bean>
 
<bean class="org.mengyun.tcctransaction.spring.recover.DefaultRecoverConfig">
    <property name="maxRetryCount" value="30"/>
    <property name="recoverDuration" value="120"/>
    <property name="cronExpression" value="0 */1 * * * ?"/>
    <property name="delayCancelExceptions">
        <util:set>
            <value>com.alibaba.dubbo.remoting.TimeoutException</value>
        </util:set>
    </property>
</bean>
       <util:set>            <value>com.alibaba.dubbo.remoting.TimeoutException</value>        </util:set>    </property></bean>      

需要注意的點:

1.資料源必須配置新的,不能使用之前項目存在的dataSource的bean,也不能在同一庫中,不然會導緻tcc表資料與本地事務一起復原,進而無法儲存異常事務日志;

2.注意domain、tbSuffix的配置,這兩項文檔中并沒有配置,但源碼demo中配置了,用于資料庫的表名稱等,推薦配置;

3.最後的DefaultRecoverConfig項是可選的,用于恢複與重試,具體作用參考使用指南;

4.defaultAutoCommit必須為true(預設為true)

5.mysql建表腳本

根據以上的tbSufifix配置,腳本如下:

CREATE TABLE `tcc_transaction_asset` (
  `TRANSACTION_ID` int(11) NOT NULL AUTO_INCREMENT,
  `DOMAIN` varchar(100) DEFAULT NULL,
  `GLOBAL_TX_ID` varbinary(32) NOT NULL,
  `BRANCH_QUALIFIER` varbinary(32) NOT NULL,
  `CONTENT` varbinary(8000) DEFAULT NULL,
  `STATUS` int(11) DEFAULT NULL,
  `TRANSACTION_TYPE` int(11) DEFAULT NULL,
  `RETRIED_COUNT` int(11) DEFAULT NULL,
  `CREATE_TIME` datetime DEFAULT NULL,
  `LAST_UPDATE_TIME` datetime DEFAULT NULL,
  `VERSION` int(11) DEFAULT NULL,
  PRIMARY KEY (`TRANSACTION_ID`),
  UNIQUE KEY `UX_TX_BQ` (`GLOBAL_TX_ID`,`BRANCH_QUALIFIER`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8      

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

6.釋出服務(重點)

6.1 dubbo接口
/**
  * @author binghe
  * 資産相關的業務釋出Dubbo服務
  */
public interface AssetCardService {
 
    /**
     * 測試預儲存資産(狀态為待确認)
     */
    @Compensable
    int testSaveAssetCard(AssetCardModel model);
 
    /**
     * 确認儲存資産到mysql(狀态為正常)
     */
    int confirmMysqlSaveAssetCard(AssetCardModel model);
 
    /**
     * 取消儲存資産到msyql(更新狀态為删除)
     */
    int cancelMysqlSaveAssetCard(AssetCardModel model);
 
    /**
     * 預儲存資産到mongo(狀态為待确認)
     */
    @Compensable
    void processMongo(AssetCardModel model);
 
    /**
     * 确認儲存資産到mongo(狀态為正常)
     */
    void confirmMongoSaveAssetCard(AssetCardModel model);
 
    /**
     * 取消儲存資産到mongo(更新狀态為删除)
     */
    void cancelMongoSaveAssetCard(AssetCardModel model);
 
}      

1.對外提供服務的接口必須有@Compensable注解;

2.對應的confirm與cancel方法必須聲明為接口,不能聲明為private,即使是public也不行,必須有接口。

6.2 dubbo接口實作類
/**
* @author binghe
* 資産相關的業務釋出Dubbo服務的實作
*/
@Service
@Component
public class AssetCardServiceImpl implements AssetCardService{
    @Override
  @Compensable(confirmMethod = "confirmMysqlSaveAssetCard", cancelMethod = "cancelMysqlSaveAssetCard", transactionContextEditor = DubboTransactionContextEditor.class)
  @Transactional(propagation = Propagation.REQUIRED, rollbackFor = { Exception.class })
  public int testSaveAssetCard(AssetCardModel model){
 
      // 儲存mysql,data狀态為-1
      model.setDataStatus(-1);
      assetCardDao.insert(model);
 
 
      // mongo處理
      assetCardService.processMongo(model);
 
      return model.getId();
  }
 
  @Override
  public int confirmMysqlSaveAssetCard(AssetCardModel model){
      System.out.println("============================================================================");
      System.out.println("=================mysql:confirm");
      System.out.println("============================================================================");
 
      // 更新mysql data_status為0
      model.setDataStatus(0);
      assetCardDao.updateByPrimaryKey(model);
 
      return model.getId();
  }
 
  @Override
  public int cancelMysqlSaveAssetCard(AssetCardModel model){
      System.out.println("============================================================================");
      System.out.println("=================mysql:cancel");
      System.out.println("============================================================================");
 
      // 更新mysql data_status為-1
      model.setDataStatus(-1);
      assetCardDao.updateByPrimaryKey(model);
 
      return model.getId();
  }
 
  @Compensable(confirmMethod = "confirmMongoSaveAssetCard", cancelMethod = "cancelMongoSaveAssetCard", transactionContextEditor = DubboTransactionContextEditor.class)
  @Override
  public void processMongo(AssetCardModel model) {
 
      // 儲存mongo,data_statu為-1
      model.setDataStatus(-1);
      assetCardDaoWrapper.saveMongo(model);
  }
 
  @Override
  public void confirmMongoSaveAssetCard(AssetCardModel model){
      System.out.println("============================================================================");
      System.out.println("=================mongo:confirm");
      System.out.println("============================================================================");
 
      // 更新mongo data_status為0
      model.setDataStatus(0);
      assetCardDaoWrapper.updateMongo(model);
  }
 
  @Override
  public void cancelMongoSaveAssetCard(AssetCardModel model){
      System.out.println("============================================================================");
      System.out.println("=================mongo:cancel");
      System.out.println("============================================================================");
 
      // 更新mongo data_status為-1
      model.setDataStatus(-1);
      assetCardDao.updateByPrimaryKey(model);
      assetCardDaoWrapper.updateMongo(model);
  }
}      

注意點:

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 調用方

@Override
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = { Exception.class })
public long testSaveAssetCard(AssetCardModel assetCardModel) throws AssetException {
    assetCardModel.setId(IdGenerator.getId());  
    return assetCardService.testSaveAssetCard(assetCardModel);
}      

1.因為需要復原更新等操作,是以此業務中id不能用自增,而是需要項目生成;

2.特别注意,調用方必須在事務中,也就是說必須有事務注解,或者能被事務配置切到,沒有事務tcc架構調用時會抛異常。

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

7.事務檢視

源碼中提供tcc-transaction-server web項目,該項目提供界面檢視事務日志,打包後部署即可,我們這裡就不在作詳細的描述。

四、TCC執行流程

業務流程使用記錄:

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

try:

使用者金額當機

調用積分處理TCC:

try:預增加積分

confirm:更新增加積分狀态

cancel:取消增加的積分

confirm:

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

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

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

cancel:

判斷訂單支付狀态與訂單記錄支付狀态為未支付

使用者當機金額釋放