【分布式事務(wù)】tcc-transaction分布式TCC型事務(wù)框架搭建與實(shí)戰(zhàn)案例(基于Dubbo/Dubbox)

一、背景
有一定分布式開(kāi)發(fā)經(jīng)驗(yàn)的朋友都知道,產(chǎn)品/項(xiàng)目/系統(tǒng)最初為了能夠快速迭代上線,往往不太注重產(chǎn)品/項(xiàng)目/系統(tǒng)的高可靠性、高性能與高擴(kuò)展性,采用單體應(yīng)用和單實(shí)例數(shù)據(jù)庫(kù)的架構(gòu)方式快速迭代開(kāi)發(fā);當(dāng)產(chǎn)品/項(xiàng)目/系統(tǒng)做到一定規(guī)模的時(shí)候,原有的系統(tǒng)架構(gòu)則不足以支撐義務(wù)發(fā)展需要,往往相同的業(yè)務(wù)則需要重復(fù)寫很多次,導(dǎo)致代碼大量冗余,難以維護(hù)和擴(kuò)展,這時(shí)不得不對(duì)原有產(chǎn)品/項(xiàng)目/系統(tǒng)進(jìn)行拆分,引入分布式的系統(tǒng)架構(gòu);而對(duì)原有產(chǎn)品/項(xiàng)目/系統(tǒng)進(jìn)行拆分的過(guò)程中,對(duì)于業(yè)務(wù)和數(shù)據(jù)的拆分和遷移則成為了最為棘手的問(wèn)題,尤其是在原有業(yè)務(wù)不能下線,拆分后的業(yè)務(wù)同時(shí)上線的場(chǎng)景下這種問(wèn)題更加突出;項(xiàng)目拆分后,業(yè)務(wù)被拆分為多個(gè)獨(dú)立的子業(yè)務(wù)分散到多個(gè)子系統(tǒng)中,而原有的單一數(shù)據(jù)庫(kù)則被拆分到多個(gè)數(shù)據(jù)庫(kù)中,拆分后的數(shù)據(jù)庫(kù)則同樣又面臨著讓人頭疼的分布式事務(wù)的問(wèn)題。
本文就針對(duì)項(xiàng)目拆分后數(shù)據(jù)庫(kù)的分布式事務(wù)問(wèn)題,基于tcc-transaction分布式TCC型事務(wù)進(jìn)行框架的搭建,同時(shí)引入相關(guān)的實(shí)戰(zhàn)案例,來(lái)解決讓人頭疼的分布式事務(wù)問(wèn)題。
二、tcc-transaction框架介紹
介紹:tcc-transaction是開(kāi)源的TCC補(bǔ)償性分布式事務(wù)框架,Git地址:https://github.com/changmingxie/tcc-transaction
TCC為Try、Confirm、Cancel的縮寫:try階段預(yù)留資源嘗試提交,confirm階段確定提交,cancel取消提交釋放資源。
1.2.x項(xiàng)目指南地址:https://github.com/changmingxie/tcc-transaction/wiki/%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%971.2.x
本文的例子為引入一個(gè)本人實(shí)際工作中的一個(gè)開(kāi)發(fā)場(chǎng)景:創(chuàng)建資產(chǎn),將資產(chǎn)信息同時(shí)同步到Mongo與ES的流程(ES代碼不列出了,與mongo類似),整個(gè)流程保證數(shù)據(jù)一致
三、項(xiàng)目流程
1.下載1.2.x版本源碼,并可能需要修改部分代碼
因?yàn)槭堑谌桨?,所以需要自己打包到本地倉(cāng)庫(kù)。但包中spring版本為3.2.12.RELEASE,如果本地項(xiàng)目為4.x,比如本人的項(xiàng)目spring版本為4.3.4.RELEASE,如果不修改tcc中的spring版本,將報(bào)錯(cuò)無(wú)法啟動(dòng),所以需要對(duì)原有框架源碼進(jìn)行相應(yīng)的修改。
源碼修改比較簡(jiǎn)單,如下
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中已經(jīng)不存在,也是修改源碼主要修改的地方。
修改其中的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 打包并發(fā)布
這里我們通過(guò)Maven進(jìn)行打包發(fā)布,命令為:
mvn -Dmaven.test.skip=true install
2.項(xiàng)目依賴
參考1.2.x使用指南,引入兩個(gè)依賴(本人項(xiàng)目dubbo/dubbox框架,我使用并打包時(shí)版本為1.2.3.1)。調(diào)用方和提供方都需要引入依賴。
3.加載tcc-transaction.xml配置
原文中是配置在web.xml中,我個(gè)人試了一下放在dubbo web項(xiàng)目的web.xml中,但配置并沒(méi)有被加載。該文件的意義只是希望項(xiàng)目啟動(dòng)時(shí)被加載,于是直接在dubbo中的一個(gè)spring的配置文件中引入,如下:
<!-- TCC Transaction --><import resource="classpath:tcc-transaction.xml" />
該文件里面提供各種aop邏輯,項(xiàng)目啟動(dòng)時(shí)掃描指定注解,并做增強(qiáng)。
4.設(shè)置TransactionRepository‘
需要為tcc配置數(shù)據(jù)源,可以是MySQL或其他nosql,本文使用mysql,其他可參見(jiàn)原指南文檔。
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>
需要注意的點(diǎn):
1.數(shù)據(jù)源必須配置新的,不能使用之前項(xiàng)目存在的dataSource的bean,也不能在同一庫(kù)中,不然會(huì)導(dǎo)致tcc表數(shù)據(jù)與本地事務(wù)一起回滾,從而無(wú)法保存異常事務(wù)日志;
2.注意domain、tbSuffix的配置,這兩項(xiàng)文檔中并沒(méi)有配置,但源碼demo中配置了,用于數(shù)據(jù)庫(kù)的表名稱等,推薦配置;
3.最后的DefaultRecoverConfig項(xiàng)是可選的,用于恢復(fù)與重試,具體作用參考使用指南;
4.defaultAutoCommit必須為true(默認(rèn)為true)
5.mysql建表腳本
根據(jù)以上的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
如果表名稱不對(duì),啟動(dòng)過(guò)程會(huì)報(bào)錯(cuò),這時(shí),對(duì)數(shù)據(jù)表做相應(yīng)調(diào)整即可。
6.發(fā)布服務(wù)(重點(diǎn))
6.1 dubbo接口
/*** @author binghe* 資產(chǎn)相關(guān)的業(yè)務(wù)發(fā)布Dubbo服務(wù)*/public interface AssetCardService {/*** 測(cè)試預(yù)保存資產(chǎn)(狀態(tài)為待確認(rèn))*/@Compensableint testSaveAssetCard(AssetCardModel model);/*** 確認(rèn)保存資產(chǎn)到mysql(狀態(tài)為正常)*/int confirmMysqlSaveAssetCard(AssetCardModel model);/*** 取消保存資產(chǎn)到msyql(更新?tīng)顟B(tài)為刪除)*/int cancelMysqlSaveAssetCard(AssetCardModel model);/*** 預(yù)保存資產(chǎn)到mongo(狀態(tài)為待確認(rèn))*/@Compensablevoid processMongo(AssetCardModel model);/*** 確認(rèn)保存資產(chǎn)到mongo(狀態(tài)為正常)*/void confirmMongoSaveAssetCard(AssetCardModel model);/*** 取消保存資產(chǎn)到mongo(更新?tīng)顟B(tài)為刪除)*/void cancelMongoSaveAssetCard(AssetCardModel model);}
需要注意的點(diǎn):
1.對(duì)外提供服務(wù)的接口必須有@Compensable注解;
2.對(duì)應(yīng)的confirm與cancel方法必須聲明為接口,不能聲明為private,即使是public也不行,必須有接口。
6.2 dubbo接口實(shí)現(xiàn)類
/*** @author binghe* 資產(chǎn)相關(guān)的業(yè)務(wù)發(fā)布Dubbo服務(wù)的實(shí)現(xiàn)*/public class AssetCardServiceImpl implements AssetCardService{public int testSaveAssetCard(AssetCardModel model){// 保存mysql,data狀態(tài)為-1model.setDataStatus(-1);assetCardDao.insert(model);// mongo處理assetCardService.processMongo(model);return model.getId();}public int confirmMysqlSaveAssetCard(AssetCardModel model){System.out.println("============================================================================");System.out.println("=================mysql:confirm");System.out.println("============================================================================");// 更新mysql data_status為0model.setDataStatus(0);assetCardDao.updateByPrimaryKey(model);return model.getId();}public int cancelMysqlSaveAssetCard(AssetCardModel model){System.out.println("============================================================================");System.out.println("=================mysql:cancel");System.out.println("============================================================================");// 更新mysql data_status為-1model.setDataStatus(-1);assetCardDao.updateByPrimaryKey(model);return model.getId();}public void processMongo(AssetCardModel model) {// 保存mongo,data_statu為-1model.setDataStatus(-1);assetCardDaoWrapper.saveMongo(model);}public void confirmMongoSaveAssetCard(AssetCardModel model){System.out.println("============================================================================");System.out.println("=================mongo:confirm");System.out.println("============================================================================");// 更新mongo data_status為0model.setDataStatus(0);assetCardDaoWrapper.updateMongo(model);}public void cancelMongoSaveAssetCard(AssetCardModel model){System.out.println("============================================================================");System.out.println("=================mongo:cancel");System.out.println("============================================================================");// 更新mongo data_status為-1model.setDataStatus(-1);assetCardDao.updateByPrimaryKey(model);assetCardDaoWrapper.updateMongo(model);}}
注意點(diǎn):
1.對(duì)外提供服務(wù)的接口必須有@Compensable注解,同時(shí)必須有confirmMethod、cancelMethod參數(shù)的配置,同時(shí)dubbo接口額外增加 "transactionContextEditor = DubboTransactionContextEditor.class"這個(gè)配置;
2.提供服務(wù)接口與對(duì)應(yīng)另外的兩個(gè)CC方法參數(shù)必須完全一致;
3.該tcc框架可嵌套調(diào)用,如上在testSaveAssetCard方法,即try階段中調(diào)用了另一個(gè)tcc方法"assetCardService.processMongo()",理論上嵌套只應(yīng)該在try階段進(jìn)行;
4.confirm、cancel需要實(shí)現(xiàn)冪等性,可能會(huì)被重試;5.由于網(wǎng)絡(luò)等因素,可能導(dǎo)致cancel方法先執(zhí)行,cancel方法一定要做好相應(yīng)的判斷與處理
6.3 調(diào)用方
(propagation = Propagation.REQUIRED, rollbackFor = { Exception.class })public long testSaveAssetCard(AssetCardModel assetCardModel) throws AssetException {assetCardModel.setId(IdGenerator.getId());return assetCardService.testSaveAssetCard(assetCardModel);}
注意點(diǎn):
1.因?yàn)樾枰貪L更新等操作,所以此業(yè)務(wù)中id不能用自增,而是需要項(xiàng)目生成;
2.特別注意,調(diào)用方必須在事務(wù)中,也就是說(shuō)必須有事務(wù)注解,或者能被事務(wù)配置切到,沒(méi)有事務(wù)tcc框架調(diào)用時(shí)會(huì)拋異常。
至此,配置已經(jīng)全部完成。
7.事務(wù)查看
源碼中提供tcc-transaction-server web項(xiàng)目,該項(xiàng)目提供界面查看事務(wù)日志,打包后部署即可,我們這里就不在作詳細(xì)的描述。
四、TCC執(zhí)行流程
業(yè)務(wù)流程使用記錄:
前提:用戶下單,建立訂單,創(chuàng)建支付記錄,支付記錄狀態(tài)為待支付
try:
用戶金額凍結(jié)
調(diào)用積分處理TCC:
try:預(yù)增加積分
confirm:更新增加積分狀態(tài)
cancel:取消增加的積分
confirm:
訂單支付狀態(tài)更新為已支付
訂單支付記錄支付狀態(tài)更新為已支付
用戶金額扣款(以上三個(gè)操作在同一本地事務(wù))
cancel:
判斷訂單支付狀態(tài)與訂單記錄支付狀態(tài)為未支付
用戶凍結(jié)金額釋放
特別推薦一個(gè)分享架構(gòu)+算法的優(yōu)質(zhì)內(nèi)容,還沒(méi)關(guān)注的小伙伴,可以長(zhǎng)按關(guān)注一下:
長(zhǎng)按訂閱更多精彩▼
如有收獲,點(diǎn)個(gè)在看,誠(chéng)摯感謝
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問(wèn)題,請(qǐng)聯(lián)系我們,謝謝!





