一、背景

有一定分布式开发经验的朋友都知道,产品/项目/系统最初为了能够快速迭代上线,往往不太注重产品/项目/系统的高可靠性、高性能与高扩展性,采用单体应用和单实例数据库的架构方式快速迭代开发;当产品/项目/系统做到一定规模的时候,原有的系统架构则不足以支撑义务发展需要,往往相同的业务则需要重复写很多次,导致代码大量冗余,难以维护和扩展,这时不得不对原有产品/项目/系统进行拆分,引入分布式的系统架构;而对原有产品/项目/系统进行拆分的过程中,对于业务和数据的拆分和迁移则成为了最为棘手的问题,尤其是在原有业务不能下线,拆分后的业务同时上线的场景下这种问题更加突出;项目拆分后,业务被拆分为多个独立的子业务分散到多个子系统中,而原有的单一数据库则被拆分到多个数据库中,拆分后的数据库则同样又面临着让人头疼的分布式事务的问题。

本文就针对项目拆分后数据库的分布式事务问题,基于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)。调用方和提供方都需要引入依赖

<dependency>
	<groupId>org.mengyun</groupId>
	<artifactId>tcc-transaction-spring</artifactId>
	<version>1.2.4.23</version>
</dependency>

<!-- dubbo版本才需要,其他版本不需要 -->
<dependency>
	<groupId>org.mengyun</groupId>
	<artifactId>tcc-transaction-dubbo</artifactId>
	<version>1.2.4.23</version>
</dependency>

3.加载tcc-transaction.xml配置

原文中是配置在web.xml中,我个人试了一下放在dubbo web项目的web.xml中,但配置并没有被加载。该文件的意义只是希望项目启动时被加载,于是直接在dubbo中的一个spring的配置文件中引入,如下:

<import resource="classpath:tcc-transaction.xml" />
<import resource="classpath:tcc-transaction-dubbo.xml" />

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

注意:上面的是官方的sql,但是在本人实践中发现,sql经常会报缺少一个 IS_DELETE 字段,需要在上表中加上该字段

6.发布服务(重点)

6.1 dubbo接口

public interface RedPacketService {

	/**
	 * 新增红包接口
	 */
	@Compensable
	Integer insertRedPacket(Order order);

}

要注意的点:
1.对外提供服务的接口必须有@Compensable注解;
2.对应的confirm与cancel方法必须声明为接口,不能声明为private,即使是public也不行,必须有接口(本文没有实现接口,而是在实现类中直接写的confirm和cancel接口

6.2 dubbo接口实现类

@Service("redPacketServiceImpl")
public class RedPacketServiceImpl implements RedPacketService {

	@Autowired
	private RedPacketDao redPacketDao;

	@Override
	@Compensable(confirmMethod = "confirmInsertRedPacket", cancelMethod = "cancelInsertRedPacket", transactionContextEditor = DubboTransactionContextEditor.class)
	public Integer insertRedPacket(Order order) {
		RedPacket redPacket = new RedPacket();
		redPacket.setOrderId(1);
		redPacket.setNum(1);
		redPacket.setId(1);
		redPacket.setStatus(-1);

		System.out.println("redPacketInsert");

		redPacketDao.insert(redPacket);

		return redPacket.getOrderId();
	}

	@Transactional
	public void confirmInsertRedPacket(Order order) {
		System.out.println("confirmRedPacketInsert");

		RedPacket redPacket = new RedPacket();
		redPacket.setOrderId(1);
		redPacket.setNum(1);
		redPacket.setId(1);
		redPacket.setStatus(0);

		redPacketDao.update(redPacket);

	}

	@Transactional
	public void cancelInsertRedPacket(Order order) {
		System.out.println("cancelRedPacketInsert");

		RedPacket redPacket = new RedPacket();
		redPacket.setOrderId(1);
		redPacket.setNum(1);
		redPacket.setId(1);
		redPacket.setStatus(1);

		redPacketDao.delete(redPacket);
	}

}

注意点:
1.对外提供服务的接口必须有@Compensable注解,同时必须有confirmMethod、cancelMethod参数的配置,同时dubbo接口额外增加 "transactionContextEditor = DubboTransactionContextEditor.class"这个配置;
2.提供服务接口与对应另外的两个CC方法参数必须完全一致;
3.该tcc框架可嵌套调用,如上在testSaveAssetCard方法,即try阶段中调用了另一个tcc方法"assetCardService.processMongo()",理论上嵌套只应该在try阶段进行;
4.confirm、cancel需要实现幂等性,可能会被重试;5.由于网络等因素,可能导致cancel方法先执行,cancel方法一定要做好相应的判断与处理
5.confirmMethod、cancelMethod方法上的 @Transactional 不可省略(官方文档上面有说明,这里未测试省略情况下面是否可以)

6.3 上一个service调用方

@Service
@Component
public class OrderServiceImpl implements OrderService {

	@Autowired
	private OrderDao orderDao;

	@Autowired
	private RedPacketService redPacketService;

	@Override
	@Compensable(confirmMethod = "confirmInsert", cancelMethod = "cancelInsert", asyncConfirm = true)
	@Transactional
	public Integer insert(Order order) {
		Order orde = new Order();
		orde.setName("电脑");
		orde.setOrderId(1);
		orde.setStatus(-1);

		orderDao.insert(orde);

		redPacketService.insertRedPacket(orde);

		//int i = 1 / 0;

		return order.getOrderId();
	}

	public void confirmInsert(Order order) {
		System.out.println("confirmOrderInsert");

		Order orde = new Order();
		orde.setName("电脑");
		orde.setOrderId(1);
		orde.setStatus(0);

		orderDao.update(orde);

	}

	public void cancelInsert(Order order) {
		System.out.println("cancelOrderInsert");

		Order orde = new Order();
		orde.setName("电脑");
		orde.setOrderId(1);
		orde.setStatus(1);

		orderDao.delete(orde);

	}

}

6.4 controller调用方

	@RequestMapping(value = "/createOrder", produces = { "application/json;charset=UTF-8" })
	@ResponseBody
	//@Transactional
	public String createOrder() {
		Order order = new Order();
		Integer id = orderService.insert(order);

		return "订单创建成功,订单ID: " + id;
	}

注意点:
1.因为需要回滚更新等操作,所以此业务中id不能用自增,而是需要项目生成;
2.特别注意,调用方必须在事务中,也就是说必须有事务注解,或者能被事务配置切到,没有事务tcc框架调用时会抛异常。
至此,配置已经全部完成

7、测试

在orderService的createOrder中的 1/0 注释放开后,可以看见 订单表和红包表都是没有数据的,同时tcc_transaction_assert中有数据。

在 1/0 注释放开后,订单表和红包表 的status字段值都为0,是confirm执行后的状态,所以是成功的。

8、完整代码

 完整代码实现:https://blog.csdn.net/u013278314/article/details/85112891

9.事务查看

源码中提供tcc-transaction-server web项目,该项目提供界面查看事务日志,打包后部署即可,我们这里就不在作详细的描述。

Logo

开源、云原生的融合云平台

更多推荐