分散式事務之——tcc-transaction分散式TCC型事務框架搭建與實戰案例【轉】
一、背景
有一定分散式開發經驗的朋友都知道,產品/專案/系統最初為了能夠快速迭代上線,往往不太注重產品/專案/系統的高可靠性、高效能與高擴充套件性,採用單體應用和單例項資料庫的架構方式快速迭代開發;當產品/專案/系統做到一定規模的時候,原有的系統架構則不足以支撐義務發展需要,往往相同的業務則需要重複寫很多次,導致程式碼大量冗餘,難以維護和擴充套件,這時不得不對原有產品/專案/系統進行拆分,引入分散式的系統架構;而對原有產品/專案/系統進行拆分的過程中,對於業務和資料的拆分和遷移則成為了最為棘手的問題,尤其是在原有業務不能下線,拆分後的業務同時上線的場景下這種問題更加突出;專案拆分後,業務被拆分為多個獨立的子業務分散到多個子系統中,而原有的單一資料庫則被拆分到多個數據庫中,拆分後的資料庫則同樣又面臨著讓人頭疼的分散式事務的問題。
本文就針對專案拆分後資料庫的分散式事務問題,基於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 修改 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>
需要注意的點:
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介面
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介面實現類
@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:
判斷訂單支付狀態與訂單記錄支付狀態為未支付
使用者凍結金額釋放