转载请注明出处: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文件
-
-
<springframework.version>4.3.4.RELEASE</springframework.version>
-
-
-
-
<groupId>org.quartz-scheduler</groupId>
-
<artifactId>quartz</artifactId>
-
-
-
-
-
<artifactId>c3p0</artifactId>
-
-
-
-
-
-
-
<groupId>com.alibaba</groupId>
-
<artifactId>dubbo</artifactId>
-
-
1.2 修改 tcc-transaction-spring/src/main/java/org/mengyun/tcctransaction/spring/recover/RecoverScheduledJob.java
该文件中 CronTriggerBean类在4.x中已经不存在,也是修改源码主要修改的地方。
修改其中的init方法,修改后以下:spring
-
-
-
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());
-
-
-
-
-
throw new SystemException(e);
-
-
各位也可参考以下的修改: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的配置文件中引入,以下:网络
-
-
<import resource="classpath:tcc-transaction.xml" />
该文件里面提供各类aop逻辑,项目启动时扫描指定注解,并作加强。
4.设置TransactionRepository
须要为tcc配置数据源,能够是MySQL或其余nosql,本文使用mysql,其余可参见原指南文档。
mysql配置以下:
-
-
<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 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 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">
-
-
<value>com.alibaba.dubbo.remoting.TimeoutException</value>
-
-
-
须要注意的点:
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 {
-
-
-
-
-
-
int testSaveAssetCard(AssetCardModel model);
-
-
-
-
-
int confirmMysqlSaveAssetCard(AssetCardModel model);
-
-
-
-
-
int cancelMysqlSaveAssetCard(AssetCardModel model);
-
-
-
-
-
-
void processMongo(AssetCardModel model);
-
-
-
-
-
void confirmMongoSaveAssetCard(AssetCardModel model);
-
-
-
-
-
void cancelMongoSaveAssetCard(AssetCardModel model);
-
-
须要注意的点:
1.对外提供服务的接口必须有@Compensable注解;
2.对应的confirm与cancel方法必须声明为接口,不能声明为private,即便是public也不行,必须有接口。
6.2 dubbo接口实现类
-
-
-
-
-
-
-
public class AssetCardServiceImpl implements AssetCardService{
-
-
@Compensable(confirmMethod = "confirmMysqlSaveAssetCard", cancelMethod = "cancelMysqlSaveAssetCard", transactionContextEditor = DubboTransactionContextEditor.class)
-
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = { Exception.class })
-
public int testSaveAssetCard(AssetCardModel model){
-
-
-
model.setDataStatus(-
1);
-
assetCardDao.insert(model);
-
-
-
-
assetCardService.processMongo(model);
-
-
-
-
-
-
public int confirmMysqlSaveAssetCard(AssetCardModel model){
-
System.out.println(
"============================================================================");
-
System.out.println(
"=================mysql:confirm");
-
System.out.println(
"============================================================================");
-
-
-
-
assetCardDao.updateByPrimaryKey(model);
-
-
-
-
-
-
public int cancelMysqlSaveAssetCard(AssetCardModel model){
-
System.out.println(
"============================================================================");
-
System.out.println(
"=================mysql:cancel");
-
System.out.println(
"============================================================================");
-
-
-
model.setDataStatus(-
1);
-
assetCardDao.updateByPrimaryKey(model);
-
-
-
-
-
@Compensable(confirmMethod = "confirmMongoSaveAssetCard", cancelMethod = "cancelMongoSaveAssetCard", transactionContextEditor = DubboTransactionContextEditor.class)
-
-
public void processMongo(AssetCardModel model) {
-
-
-
model.setDataStatus(-
1);
-
assetCardDaoWrapper.saveMongo(model);
-
-
-
-
public void confirmMongoSaveAssetCard(AssetCardModel model){
-
System.out.println(
"============================================================================");
-
System.out.println(
"=================mongo:confirm");
-
System.out.println(
"============================================================================");
-
-
-
-
assetCardDaoWrapper.updateMongo(model);
-
-
-
-
public void cancelMongoSaveAssetCard(AssetCardModel model){
-
System.out.println(
"============================================================================");
-
System.out.println(
"=================mongo:cancel");
-
System.out.println(
"============================================================================");
-
-
-
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 调用方
-
-
@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项目,该项目提供界面查看事务日志,打包后部署便可,咱们这里就不在做详细的描述。
4、TCC执行流程
业务流程使用记录: 前提:用户下单,创建订单,建立支付记录,支付记录状态为待支付 try: 用户金额冻结 调用积分处理TCC: try:预增长积分 confirm:更新增长积分状态 cancel:取消增长的积分 confirm: 订单支付状态更新为已支付 订单支付记录支付状态更新为已支付 用户金额扣款(以上三个操做在同一本地事务) cancel: 判断订单支付状态与订单记录支付状态为未支付 用户冻结金额释放