冬日魔幻之旅-seata+dubbo+nacos+springboot解决分布式事务的全网段唯一实践之作(下)

开篇

上篇中我们讲述了seata的基于2PC的AT事物实战篇。在下篇中我们将会非常详细的描述一下如何利用seata来实现TCC事务补偿机制的原理。

目前网上所有的对于seata的TCC讲解只有一篇阿里原本的seata-tcc,它原本自带的这个例子有如下几个缺点:

  1. 若干个provider混在一起
  2. provider和consumer混在一个项目
  3. 不支持nacos连接
  4. 不支持注解

然后网上所有的博客全部是围绕着这篇helloworld级别的例子而讲,其实很多都是抄袭,没有一篇融入了自己的领会与思想,也没有去把原本的例子按照生产级去做分离,这显然会误导很多读者。

因此,我们这次就在原本阿里官方的例子上做生产级别的增强,使得它可以适应你正要准备做的生产环境全模拟。

例子-跨行转款问题

冬日魔幻之旅-seata+dubbo+nacos+springboot解决分布式事务的全网段唯一实践之作(下)_第1张图片

还记得我们在上曾经出现过这么一个例子用于详细描述TCC描述事务的原理吧?

今天我们就会围绕着这个例子来进一步用代码演示它。所有代码我已经上传到了我的GIT上了,地址在这:https://github.com/mkyuangithub/mkyuangithub

我们假设有这么一个业务场景:

你的公司是一家叫moneyking的第三方支付公司,连接着几个主要的银行支付渠道;

现在有一个帐户A要通过工行向另一个位于招商银行的B帐户转帐;

转帐要么成功要么失败;

于是我们结合着例子创建了3个项目:

  1. tcc-bank-cmb
  2. tcc-bank-icbc
  3. tcc-money-king

tcc-bank-cmb和tcc-bank-icbc都是dubbo provider,它们分别连接着自己的数据库(不同的url)。

冬日魔幻之旅-seata+dubbo+nacos+springboot解决分布式事务的全网段唯一实践之作(下)_第2张图片

两个不同的schema,一个schema叫bank_icbc,一个schema叫bank_cmb,每个schema中的表结构相同。

我们下面给出相关的建表语句,每个业务表内的undo_log请各位看上篇冬日魔幻之旅-seata+dubbo+nacos+springboot解决分布式事务的全网段唯一实践之作(上)中所介绍的(内含有undo_log表建表语句)。

CREATE TABLE `bank_account` (
  `account_id` varchar(32) COLLATE utf8_bin NOT NULL,
  `amount` double(11,2) DEFAULT '0.00',
  `freezed_amount` double(11,2) DEFAULT '0.00',
  PRIMARY KEY (`account_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

tcc-bank-icbc和tcc-bank-cmb分别连接着这2个schema。而tcc-money-king就是一个consumer,它来模拟你所在的那家第三方支付公司,所有的客户都是通过tcc-money-king来进行转帐的。

工程详细讲解

如上篇中一样,我们在讲述具体的代码前先要把tcc如何在seata中实现的一些个坑给“填了”。

全局事务

和seata中的AT模式不同TCC的全局事务不需要你设置datasourceProxy代理,它只需要把事务范围和事务组申明好就可以了。

我们这边的事务组如下所示:

  • 事务组:demo-tx-grp
  • 事务边界:tcc-bank-sample,此处这个边界就是指的是tcc-money-king项目的dubbo.application.name。

冬日魔幻之旅-seata+dubbo+nacos+springboot解决分布式事务的全网段唯一实践之作(下)_第3张图片

在我们的tcc-money-king中有一个业务方法,在这个业务方法中只需要如此使用@GlobalTransaction申明即可启用seata的tcc机制

Spring Boot工程中不可以使用注解来申明dubbo的坑

没错!截止发稿稿为止seata-1.0GA的tcc不支持@Service, @Reference这样注解方式的dubbo发布,它虽然不会出错可是会使得整个tcc事务失效(AT模式中是完全可以使用注解模式的,TCC模式目前还不支持),只有那些使用普通的spring的.xml配置来申明的provider和reference才能享受tcc的“盛餐”。

那么这对于我们的spring boot工程来说岂不是很“恶心”的一件事?不要急,笔者已经探索出来了一条“熊掌与鱼兼得”法,即混用springboot和普通spring .xml文件配置。

即,只对dubbo bean进行.xml配置而对其它我们坚持可以使用spring boot的全注解方法来搭建整个项目的框架,见下例。

冬日魔幻之旅-seata+dubbo+nacos+springboot解决分布式事务的全网段唯一实践之作(下)_第4张图片

这边除了dubbo和一个比较特殊的transactionTemplate需要使用.xml,其它我们照样可以使用spring boot的全注解配置yyaa,只需要在我们的XxxConfig文件内写上这么一句即可:

@Configuration
@ImportResource(locations = { "spring/spring-bean.xml", "spring/dubbo-bean.xml" })
public class TccBankConfig {

然后在你使用到的地方比如说我们在tcc-money-king中使用了.xml文件配置一个dubbo的引用,那么此时你只需要在你要Reference的Service方法内@Autowired一下即可,如下例:

冬日魔幻之旅-seata+dubbo+nacos+springboot解决分布式事务的全网段唯一实践之作(下)_第5张图片

冬日魔幻之旅-seata+dubbo+nacos+springboot解决分布式事务的全网段唯一实践之作(下)_第6张图片

冬日魔幻之旅-seata+dubbo+nacos+springboot解决分布式事务的全网段唯一实践之作(下)_第7张图片

开始讲解所有工程中的代码

整个tcc它围绕着try, confirm, cancel这3个方法来运作的。这使得你需要使用tcc事务的话就必须对原有代码有侵入性。可是seata在这方面做的很好,它j 通过远程调用、AOP来做的全局事务切入进而实现这一过程的。

所以在seata tcc编程中最最重要的有这么几个元素:

  1. Global Transactional
  2. Transaction Template
  3. Transaction Manager

下面我们就以实例来感受seata tcc是如何做到尽量少侵入业务代码、又能做到性能最优、同时做到数据的最终一致性吧

tcc-bank-icbc

冬日魔幻之旅-seata+dubbo+nacos+springboot解决分布式事务的全网段唯一实践之作(下)_第8张图片

pom.xml


	4.0.0
	
		org.sky.demo
		nacos-parent
		0.0.1-SNAPSHOT
	
	org.sky.demo
	tcc-bank-icbc
	0.0.1
	tcc-bank-icbc
	Demo project Dubbo+Nacos+SeataTCC
	
		UTF-8
	
	
		
			org.mybatis
			mybatis
		
		
			org.mybatis
			mybatis-spring
		
		
			org.springframework.boot
			spring-boot-starter-jdbc
			
				
					org.springframework.boot
					spring-boot-starter-logging
				
			
		
		
			org.apache.dubbo
			dubbo
		
		
			org.apache.curator
			curator-framework
		
		
			org.apache.curator
			curator-recipes
		
		
			mysql
			mysql-connector-java
		
		
			com.alibaba
			druid
		
		
			org.springframework.boot
			spring-boot-starter-test
			test
		
		
			org.spockframework
			spock-core
			test
		
		
			org.spockframework
			spock-spring
		
		
			org.springframework.boot
			spring-boot-configuration-processor
			true
		

		
			org.springframework.boot
			spring-boot-starter-log4j2
		
		
			org.springframework.boot
			spring-boot-starter-web
			
				
					org.springframework.boot
					spring-boot-starter-logging
				
				
					org.springframework.boot
					spring-boot-starter-tomcat
				
			
		
		
			org.aspectj
			aspectjweaver
		
		
			com.lmax
			disruptor
		
		
			redis.clients
			jedis
		
		
			com.google.guava
			guava
		
		
			com.alibaba
			fastjson
		
		
		
			org.apache.dubbo
			dubbo-registry-nacos
		
		
			com.alibaba.nacos
			nacos-client
		
		
			org.sky.demo
			skycommon
			${skycommon.version}
		
		
			io.seata
			seata-all
		
		
			com.alibaba.boot
			nacos-config-spring-boot-starter
			
				
					nacos-client
					com.alibaba.nacos
				
			
		
		
			io.netty
			netty-all
		
		
			org.projectlombok
			lombok
		
	
	
		${project.artifactId}
		src/main/java
		src/test/java
		
			
				org.apache.maven.plugins
				maven-compiler-plugin
			
			
				org.springframework.boot
				spring-boot-maven-plugin
				
					-Dfile.encoding=UTF-8
				
				
					
						
							repackage
						
					
				
			
			
				org.apache.maven.plugins
				maven-war-plugin
				2.6
				
					false
				
			
		
		
			
				src/main/resources
				
					application*.properties
				
			
			
				src/main/webapp
				META-INF/resources
				
					**/**
				
			
			
				src/main/resources
				true
				
					application.properties
				
			
		
	

application.properties

spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.56.101:3306/bank_icbc?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=icbc
spring.datasource.password=111111
spring.datasource.initialize=false
spring.datasource.initialSize=5
spring.datasource.minIdle=5
spring.datasource.maxActive: 20
spring.datasource.maxWait: 30000
spring.datasource.validationQuery=SELECT 1 FROM DUAL
spring.datasource.testWhileIdle=true
spring.datasource.testOnBorrow=false
spring.datasource.testOnReturn=false
spring.datasource.poolPreparedStatements=true
spring.datasource.maxPoolPreparedStatementPerConnectionSize=128
logging.config=classpath:log4j2.xml

由于我们对于dubbo需要使用.xml文件的方式配置,因此我们的application.properties文件内容相对简单

TccBankConfig.java

package org.sky.tcc.bank.icbc.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import com.alibaba.druid.pool.DruidDataSource;

import io.seata.spring.annotation.GlobalTransactionScanner;

@Configuration
@ImportResource(locations = { "spring/spring-bean.xml", "spring/dubbo-bean.xml" })
public class TccBankConfig {

	@Bean
	@ConfigurationProperties(prefix = "spring.datasource")
	public DruidDataSource druidDataSource() {
		return new DruidDataSource();

	}

	@Bean
	public DataSourceTransactionManager transactionManager(DruidDataSource druidDataSource) {
		return new DataSourceTransactionManager(druidDataSource);
	}

	@Bean
	public JdbcTemplate jdbcTemplate(DruidDataSource druidDataSource) {
		return new JdbcTemplate(druidDataSource);
	}

	@Bean
	public GlobalTransactionScanner globalTransactionScanner() {
		return new GlobalTransactionScanner("tcc-bank-icbc", "demo-tx-grp");
	}

}

这个,就是我们的全局配置类,在这个配置类内对于datasource, transaction manager, global transactional我们使用的是全注解。

我们在spring/spring-bean.xml文件内申明了transactional template

spring/spring-bean.xml





	
		
			PROPAGATION_REQUIRES_NEW
		
		
			
		
	

对于dubbo我们使用的是spring/dubbo-bean.xml来配置的

spring/dubbo-bean.xml







	
	
	
	

	

	

	
	

	

我们可以看到在这个dubbo-bean.xml文件中我们配置了一个核心的org.sky.tcc.bank.icbc.dubbo.MinusMoneyAction,我们先来看这个MinusMoneyAction。

因为我们是从:

工行划款;

招行打款;

因此我们相应的在tcc-bank-cmb中还有一个核心的dubbo叫PlusMoneyAction。

MinusMoneyAction的接口类,注意此接口类为一个“残根”即“被调用者”,因此我们把它放置于了skycommon工程内了。

package org.sky.tcc.bank.icbc.dubbo;

import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;

public interface MinusMoneyAction {

	public String sayHello() throws RuntimeException;

	/**
	 * 一阶段从from帐户扣钱
	 * 
	 * @param businessActionContext
	 * @param accountNo
	 * @param amount
	 */
	@TwoPhaseBusinessAction(name = "minusMoneyAction", commitMethod = "commit", rollbackMethod = "rollback")
	public boolean prepareMinus(BusinessActionContext businessActionContext,
			@BusinessActionContextParameter(paramName = "accountNo") String accountNo,
			@BusinessActionContextParameter(paramName = "amount") double amount);

	/**
	 * 二阶段提交
	 * 
	 * @param businessActionContext
	 * @return
	 */
	public boolean commit(BusinessActionContext businessActionContext);

	/**
	 * 二阶段回滚
	 * 
	 * @param businessActionContext
	 * @return
	 */
	public boolean rollback(BusinessActionContext businessActionContext);
}

我们可以通过实现类看到它其实是事先了tcc的3个阶段:

  • prepareMinus方法对try
  • commit方法对confirm
  • rollback方法对cancel

这3个方法的实现就是让我们在尽量少破坏业务代码的方法下实现tcc补偿式事务的。这3个方法是相当特殊的,它们的调用为“被seata server端全自动异步回调”,即不需要你try if xxx catch exception rollback的,你要做的只是告诉业务方法在何种状态它应该要rollback;何种状态属于调用成功即自动commit。一切都是自动的。

而这边的commit也不是我们传统意义的数据库层面的commit。

让我们来一起看一下它的实现类

MinusMoneyActionImpl.java

package org.sky.tcc.bank.icbc.dubbo;

import org.sky.service.BaseService;
import org.sky.tcc.bank.icbc.dao.TransferMoneyDAO;
import org.sky.tcc.bean.AccountBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;

import io.seata.core.context.RootContext;
import io.seata.rm.tcc.api.BusinessActionContext;

public class MinusMoneyActionImpl extends BaseService implements MinusMoneyAction {
	/**
	 * 扣钱账户 DAO
	 */
	@Autowired
	private TransferMoneyDAO transferMoneyDAO;

	/**
	 * 扣钱数据源事务模板
	 */
	@Autowired
	private TransactionTemplate transactionTemplate;

	@Override
	public String sayHello() throws RuntimeException {
		return "hi I am icbc-dubbo";
	}

	@Override
	public boolean prepareMinus(BusinessActionContext businessActionContext, String accountNo, double amount) {
		logger.info("==========into prepareMinus");
		// 分布式事务ID
		final String xid = RootContext.getXID();

		return transactionTemplate.execute(new TransactionCallback() {

			@Override
			public Boolean doInTransaction(TransactionStatus status) {
				try {
					// 校验账户余额
					AccountBean account = transferMoneyDAO.getAccountForUpdate(accountNo);
					if (account == null) {
						throw new RuntimeException("账户不存在");
					}
					if (account.getAmount() - amount < 0) {
						throw new RuntimeException("余额不足");
					}
					// 冻结转账金额
					double freezedAmount = account.getFreezedAmount() + amount;
					account.setFreezedAmount(freezedAmount);
					transferMoneyDAO.updateFreezedAmount(account);
					logger.info(String.format("======>prepareMinus account[%s] amount[%f], dtx transaction id: %s.",
							accountNo, amount, xid));
					return true;
				} catch (Throwable t) {
					logger.error("======>error occured in MinusMoneyActionImpl.prepareMinus: " + t.getMessage(),
							t.getCause());
					status.setRollbackOnly();
					return false;
				}
			}
		});
	}

	@Override
	public boolean commit(BusinessActionContext businessActionContext) {
		logger.info("======>into MinusMoneyActionImpl.commit() method");
		// 分布式事务ID
		final String xid = RootContext.getXID();
		// 账户ID
		final String accountNo = String.valueOf(businessActionContext.getActionContext("accountNo"));
		// 转出金额
		final double amount = Double.valueOf(String.valueOf(businessActionContext.getActionContext("amount")));
		return transactionTemplate.execute(new TransactionCallback() {

			@Override
			public Boolean doInTransaction(TransactionStatus status) {
				try {
					AccountBean account = transferMoneyDAO.getAccountForUpdate(accountNo);
					// 扣除账户余额
					double newAmount = account.getAmount() - amount;
					if (newAmount < 0) {
						throw new RuntimeException("余额不足");
					}
					account.setAmount(newAmount);
					// 释放账户 冻结金额
					account.setFreezedAmount(account.getFreezedAmount() - amount);
					transferMoneyDAO.updateAmount(account);
					logger.info(String.format("======>minus account[%s] amount[%f], dtx transaction id: %s.", accountNo,
							amount, xid));
					return true;
				} catch (Throwable t) {
					logger.error("======>error occured in MinusMoneyActionImpl.commit: " + t.getMessage(),
							t.getCause());
					status.setRollbackOnly();
					return false;
				}
			}
		});
	}

	@Override
	public boolean rollback(BusinessActionContext businessActionContext) {
		logger.info("======>into MinusMoneyActionImpl.rollback() method");
		// 分布式事务ID
		final String xid = RootContext.getXID();
		// 账户ID
		final String accountNo = String.valueOf(businessActionContext.getActionContext("accountNo"));
		// 转出金额
		final double amount = Double.valueOf(String.valueOf(businessActionContext.getActionContext("amount")));
		return transactionTemplate.execute(new TransactionCallback() {

			@Override
			public Boolean doInTransaction(TransactionStatus status) {
				try {
					AccountBean account = transferMoneyDAO.getAccountForUpdate(accountNo);
					if (account == null) {
						// 账户不存在,回滚什么都不做
						return true;
					}
					// 释放冻结金额
					if (account.getFreezedAmount() >= amount) {
						account.setFreezedAmount(account.getFreezedAmount() - amount);
						transferMoneyDAO.updateFreezedAmount(account);
					}
					logger.info(
							String.format("======>Undo prepareMinus account[%s] amount[%f], dtx transaction id: %s.",
									accountNo, amount, xid));
					return true;
				} catch (Throwable t) {
					logger.error("======>error occured in MinusMoneyActionImpl.rollback: " + t.getMessage(),
							t.getCause());
					status.setRollbackOnly();
					return false;
				}
			}
		});
	}

}

从以上代码我们可以看到它是一个“工行划款”的全过程。

一开始它会从prepareMinus方法走起,你在consumer端只需要调用这个prepareMinus然后后面的commit与rollback是seata根据业务方法执行的状态自动回调并决定后一步调用到底是调用commit还是调用rollback的,即在consumer端的业务方法内是不含有commit和rollback的。

此处的PrepareMinus要做的事就是:

  • 先检查帐户是否存,如果不存在直接抛出一个RuntimeException迫使全局事务走后面的rollback分支而不继续走数据库的commit也不走该MinusMoneyAction中的commit(第二步);
  • 如果要转帐的数额大于余额,那肯定也不行的,会抛错;
  • 检查好了,它这边开始做业务幂等了,即为了后面的业务rollback做准备,它会先把一个“冻结余额”+“转帐额”;

这就是prepare阶段,prepare阶段如果成功seata会自动走下一步commit,如果遇到有问题就可以运行rollback方法。

那么我们来看业务commit(即confirm)方法吧:

  • 经过了上述的prepare过程,一切无误,那么我们就要开始扣款了。为了做到业务幂等,在此要再做一次余额校验,因为spring中的bean都是“非线程安全”的,此时可能由于并发操作的原因,在过了commit方法后实际数据库内的余额因为其它生产上的一些业务方法导致了这个余额已经低于转帐额了,因此在这里要再做一次校验,如果校验不通过那么抛出RuntimeException。
  • 把要转帐帐户from_account扣去转帐额然后把中间状态 freezed_amount-转帐额以还原到原有状态 ,整个“工行阶段的业务 ”结束。

很多人在此处要问,为什么需要增加一个freezed_amount,直接扣不就完了。

是!你可以直接扣,可是我们前面说过幂等了,那么请问你在commit或者是在rollback时你会怎么回滚这个数据?

我们人操作的话就是原来转出10元,失败了,把10元退给原帐户!

因此我们这边拿了这个freezed_amount就是来做计算机可以认得的这个“中间暂存”变量。还记得我们在上篇中提到的业务幂等吗?我们需要保存一切中间状态以便于“业务回退/反交易”。

那么我们下面来看看这个“业务回退”是怎么样的,即rollback方法

  • 整个划款过程分为2个步骤,6个小步骤。它们是:minusMoneyAction, plusMoneyAction,每个步骤都有prepare, commit, rollback。
  • 此处的rollback为业务rollback,即在6个分解的小步骤中有任何一步抛RuntimeException那么seata会自动触发两个大步骤中的rollback。
  • rollback要做的,拿icbc是扣款来说就是一个“业务回退”,它先查询该帐户是否存在,如果不存在那也不要做了,帐户不存在不存在任何抛错只要return true就可以了什么都不用做。这边的return true是什么意思尼?这叫空回滚

  • 所谓空回滚就是
    事务协调器在调用TCC服务的一阶段Try操作时,可能会出现因为丢包而导致的网络超时,此时事务协调器会触发二阶段回滚,调用TCC服务的Cancel操作;TCC服务在未收到Try请求的情况下收到Cancel请求,这种场景被称为空回滚;TCC服务在实现时应当允许空回滚的执行;如果你觉得前面这段话有点拗口,那么我们再说了白一点,看下图就能理解了
    冬日魔幻之旅-seata+dubbo+nacos+springboot解决分布式事务的全网段唯一实践之作(下)_第9张图片

从上图看到,这个rollback以返回true来判定回滚成功,此时你要不给它true给它false或者是Exception的话它就会不断的尝试回滚,于是你在后台会看到一堆的try rollback but failed try again...,要try多少次呢?它是依赖于seata server端的conf/nacos-config.txt中的这么几个参数来设定的

client.tm.commit.retry.count=1
client.tm.rollback.retry.count=1

你现在理解为什么在rollback调用时如果检查到了帐户已经不存在,直接返回true而不需要再thru什么Exception或者是return false了吧?再加上你如果前面这2个retry.count参数没有设好,到时你就会限入“无限回滚”(因为默认这两个值是-1,代表无限尝试)的状态 ,最后把jvm给搞爆掉。

  • rollback中对于帐户检查完后如果没有问题那么接下来要做的就是把freezed_amount-要转帐额还原到原来的freezed_amount,并把余额还原回操作前的值即可

下面给出DAO的详细代码,DAO代码很简单,没什么需要多说的

TransferMoneyDAO.java

package org.sky.tcc.bank.icbc.dao;

import org.sky.tcc.bean.AccountBean;

public interface TransferMoneyDAO {
	public void addAccount(AccountBean account) throws Exception;

	public int updateAmount(AccountBean account) throws Exception;

	public AccountBean getAccount(String accountNo) throws Exception;

	public AccountBean getAccountForUpdate(String accountNo) throws Exception;

	public int updateFreezedAmount(AccountBean account) throws Exception;
}

TransferMoneyDAOImpl.java

package org.sky.tcc.bank.icbc.dao;

import java.sql.ResultSet;
import java.sql.SQLException;

import org.sky.tcc.bean.AccountBean;
import org.sky.tcc.dao.BaseDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Component;

@Component
public class TransferMoneyDAOImpl extends BaseDAO implements TransferMoneyDAO {
	@Autowired
	private JdbcTemplate fromJdbcTemplate;

	@Override
	public void addAccount(AccountBean account) throws Exception {
		String sql = "insert into bank_account(account_id,amount,freezed_amount) values(?,?,?)";
		fromJdbcTemplate.update(sql, account.getAccountId(), account.getAmount(), account.getFreezedAmount());
	}

	@Override
	public int updateAmount(AccountBean account) throws Exception {
		String sql = "update bank_account set amount=?, freezed_amount=? where account_id=?";
		int result = 0;
		result = fromJdbcTemplate.update(sql, account.getAmount(), account.getFreezedAmount(), account.getAccountId());
		return result;
	}

	@Override
	public AccountBean getAccount(String accountNo) throws Exception {
		String sql = "select account_id,amount,freezed_amount from bank_account where account_id=?";
		AccountBean account = null;
		// Object[] params = new Object[] { accountNo };
		try {
			account = fromJdbcTemplate.queryForObject(sql, new RowMapper() {
				@Override
				public AccountBean mapRow(ResultSet rs, int rowNum) throws SQLException {
					AccountBean account = new AccountBean();
					account.setAccountId(rs.getString("account_id"));
					account.setAmount(rs.getDouble("amount"));
					account.setFreezedAmount(rs.getDouble("freezed_amount"));
					return account;

				}
			}, accountNo);
		} catch (Exception e) {
			logger.error("getAccount error: " + e.getMessage(), e);
			account = null;
		}
		return account;
	}

	@Override
	public AccountBean getAccountForUpdate(String accountNo) throws Exception {
		String sql = "select account_id,amount,freezed_amount from bank_account where account_id=? for update";
		AccountBean account = null;
		// Object[] params = new Object[] { accountNo };
		try {
			account = fromJdbcTemplate.queryForObject(sql, new RowMapper() {
				@Override
				public AccountBean mapRow(ResultSet rs, int rowNum) throws SQLException {

					AccountBean account = new AccountBean();
					account.setAccountId(rs.getString("account_id"));
					account.setAmount(rs.getDouble("amount"));
					account.setFreezedAmount(rs.getDouble("freezed_amount"));
					return account;
				}
			}, accountNo);
		} catch (Exception e) {
			logger.error("getAccount error: " + e.getMessage(), e);
			return null;
		}
		return account;
	}

	@Override
	public int updateFreezedAmount(AccountBean account) throws Exception {
		String sql = "update bank_account set freezed_amount=? where account_id=?";
		int result = 0;
		result = fromJdbcTemplate.update(sql, account.getFreezedAmount(), account.getAccountId());
		return result;
	}

}

用于启用动的ICBCApplication,此处因为我们用了.xml模式配置dubbo,因此可就不能使用@EnableDubbo了啊

package org.sky.tcc.bank.icbc;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;

@EnableAutoConfiguration
@ComponentScan(basePackages = { "org.sky.tcc.bank" })
public class ICBCApplication {

	public static void main(String[] args) {
		SpringApplication.run(ICBCApplication.class, args);

	}

}

tcc-bank-cmb工程

冬日魔幻之旅-seata+dubbo+nacos+springboot解决分布式事务的全网段唯一实践之作(下)_第10张图片

这是一个“招行打款”的dubbo provider,它和前面的工行扣款类似,也是实现了TCC的提交方式,只不过它要做的是“增加余额操作”。

其它逻辑和tcc-bank-icbc一样,我们在此看一下它的三个TCC吧

PlusMoneyActionImpl.java

package org.sky.tcc.bank.cmb.dubbo;

import org.sky.service.BaseService;
import org.sky.tcc.bank.cmb.dao.TransferMoneyDAO;
import org.sky.tcc.bean.AccountBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;

import io.seata.core.context.RootContext;
import io.seata.rm.tcc.api.BusinessActionContext;

public class PlusMoneyActionImpl extends BaseService implements PlusMoneyAction {

	@Autowired
	private TransactionTemplate transactionTemplate;

	@Autowired
	private TransferMoneyDAO transferMoneyDAO;

	@Override
	public String sayHello() throws RuntimeException {
		return "hi I am cmb-dubbo";
	}

	@Override
	public boolean prepareAdd(BusinessActionContext businessActionContext, String accountNo, double amount) {
		logger.info("======>inti prepare add");
		final String xid = RootContext.getXID();
		return transactionTemplate.execute(new TransactionCallback() {

			@Override
			public Boolean doInTransaction(TransactionStatus status) {
				try {
					// 校验账户
					AccountBean account = transferMoneyDAO.getAccountForUpdate(accountNo);
					if (account == null) {
						logger.info(
								"======>prepareAdd: 账户[" + accountNo + "]不存在, txId:" + businessActionContext.getXid());
						return false;
					}
					// 待转入资金作为 不可用金额
					double freezedAmount = account.getFreezedAmount() + amount;
					account.setFreezedAmount(freezedAmount);
					transferMoneyDAO.updateFreezedAmount(account);
					logger.info(String.format(
							"PlusMoneyActionImpl.prepareAdd account[%s] amount[%f], dtx transaction id: %s.", accountNo,
							amount, xid));
					return true;
				} catch (Throwable t) {
					logger.error("======>error occured in PlusMoneyActionImpl.prepareAdd: " + t.getMessage(),
							t.getCause());
					status.setRollbackOnly();
					return false;
				}
			}
		});
	}

	@Override
	public boolean commit(BusinessActionContext businessActionContext) {
		logger.info("======>into PlusMoneyActionImpl.commit() method");
		// 分布式事务ID
		final String xid = RootContext.getXID();
		// 账户ID
		final String accountNo = String.valueOf(businessActionContext.getActionContext("accountNo"));
		// 转出金额
		final double amount = Double.valueOf(String.valueOf(businessActionContext.getActionContext("amount")));
		return transactionTemplate.execute(new TransactionCallback() {

			@Override
			public Boolean doInTransaction(TransactionStatus status) {
				try {
					AccountBean account = transferMoneyDAO.getAccountForUpdate(accountNo);
					// 加钱
					double newAmount = account.getAmount() + amount;
					account.setAmount(newAmount);
					// 冻结金额 清除
					account.setFreezedAmount(account.getFreezedAmount() - amount);
					transferMoneyDAO.updateAmount(account);

					logger.info(String.format("======>add account[%s] amount[%f], dtx transaction id: %s.", accountNo,
							amount, xid));
					return true;
				} catch (Throwable t) {
					logger.error("======>error occured in PlusMoneyActionImpl.commit: " + t.getMessage(), t.getCause());
					status.setRollbackOnly();
					return false;
				}
			}
		});
	}

	@Override
	public boolean rollback(BusinessActionContext businessActionContext) {
		logger.info("======>into PlusMoneyActionImpl.rollback() method");
		// 分布式事务ID
		final String xid = RootContext.getXID();
		// 账户ID
		final String accountNo = String.valueOf(businessActionContext.getActionContext("accountNo"));
		// 转出金额
		final double amount = Double.valueOf(String.valueOf(businessActionContext.getActionContext("amount")));
		return transactionTemplate.execute(new TransactionCallback() {

			@Override
			public Boolean doInTransaction(TransactionStatus status) {
				try {
					AccountBean account = transferMoneyDAO.getAccountForUpdate(accountNo);
					if (account == null) {
						// 账户不存在, 无需回滚动作
						return true;
					}
					// 冻结金额 清除
					if (account.getFreezedAmount() >= amount) {
						account.setFreezedAmount(account.getFreezedAmount() - amount);
						transferMoneyDAO.updateFreezedAmount(account);
					}
					logger.info(String.format("======>Undo account[%s] amount[%f], dtx transaction id: %s.", accountNo,
							amount, xid));
					return true;
				} catch (Throwable t) {
					logger.error("======>error occured in PlusMoneyActionImpl.rollback: " + t.getMessage(),
							t.getCause());
					status.setRollbackOnly();
					return false;
				}
			}
		});
	}

}
  • prepareAdd阶段阶段,检查帐户如果有异常抛出RuntimeEcxeption让seata触发回滚(业务 回滚+事务回滚)。如果无误那么把中间状态 freezed_amount+转入额。
  • commit阶段,帐户余额+转入金融,接着把freezed_amount-转入额度还原到原来的值。
  • rollback阶段,和minusMoneyAction逻辑一样,把冻结款-转帐额再把余额回退到操作前的值。

pom.xml


	4.0.0
	
		org.sky.demo
		nacos-parent
		0.0.1-SNAPSHOT
	
	org.sky.demo
	tcc-bank-cmb
	0.0.1
	tcc-bank-cmb
	Demo project Dubbo+Nacos+SeataTCC
	
		UTF-8
	
	
		
			org.mybatis
			mybatis
		
		
			org.mybatis
			mybatis-spring
		
		
			org.springframework.boot
			spring-boot-starter-jdbc
			
				
					org.springframework.boot
					spring-boot-starter-logging
				
			
		
		
			org.apache.dubbo
			dubbo
		
		
			org.apache.curator
			curator-framework
		
		
			org.apache.curator
			curator-recipes
		
		
			mysql
			mysql-connector-java
		
		
			com.alibaba
			druid
		
		
			org.springframework.boot
			spring-boot-starter-test
			test
		
		
			org.spockframework
			spock-core
			test
		
		
			org.spockframework
			spock-spring
		
		
			org.springframework.boot
			spring-boot-configuration-processor
			true
		

		
			org.springframework.boot
			spring-boot-starter-log4j2
		
		
			org.springframework.boot
			spring-boot-starter-web
			
				
					org.springframework.boot
					spring-boot-starter-logging
				
				
					org.springframework.boot
					spring-boot-starter-tomcat
				
			
		
		
			org.aspectj
			aspectjweaver
		
		
			com.lmax
			disruptor
		
		
			redis.clients
			jedis
		
		
			com.google.guava
			guava
		
		
			com.alibaba
			fastjson
		
		
		
			org.apache.dubbo
			dubbo-registry-nacos
		
		
			com.alibaba.nacos
			nacos-client
		
		
			org.sky.demo
			skycommon
			${skycommon.version}
		
		
			io.seata
			seata-all
		
		
			com.alibaba.boot
			nacos-config-spring-boot-starter
			
				
					nacos-client
					com.alibaba.nacos
				
			
		
		
			io.netty
			netty-all
		
		
			org.projectlombok
			lombok
		
	
	
		${project.artifactId}
		src/main/java
		src/test/java
		
			
				org.apache.maven.plugins
				maven-compiler-plugin
			
			
				org.springframework.boot
				spring-boot-maven-plugin
				
					-Dfile.encoding=UTF-8
				
				
					
						
							repackage
						
					
				
			
			
				org.apache.maven.plugins
				maven-war-plugin
				2.6
				
					false
				
			
		
		
			
				src/main/resources
				
					application*.properties
				
			
			
				src/main/webapp
				META-INF/resources
				
					**/**
				
			
			
				src/main/resources
				true
				
					application.properties
				
			
		
	

application.properties

spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.56.101:3306/bank_cmb?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=cmb
spring.datasource.password=111111
spring.datasource.initialize=false
spring.datasource.initialSize=5
spring.datasource.minIdle=5
spring.datasource.maxActive: 20
spring.datasource.maxWait: 30000
spring.datasource.validationQuery=SELECT 1 FROM DUAL
spring.datasource.testWhileIdle=true
spring.datasource.testOnBorrow=false
spring.datasource.testOnReturn=false
spring.datasource.poolPreparedStatements=true
spring.datasource.maxPoolPreparedStatementPerConnectionSize=128
logging.config=classpath:log4j2.xml

spring/dubbo-bean.xml







	
	
	
	

	

	

	
	

	

 spring/spirng-bean.xml





	
		
			PROPAGATION_REQUIRES_NEW
		
		
			
		
	

自动装配用TccBankConfig.java,这边要注意的是此处的GlobalTransaction里的名字可必须是tcc-bank-cmb啦,不要复制粘贴后忘改了

package org.sky.tcc.bank.cmb.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import com.alibaba.druid.pool.DruidDataSource;

import io.seata.spring.annotation.GlobalTransactionScanner;

@Configuration
@ImportResource(locations = { "spring/spring-bean.xml", "spring/dubbo-bean.xml" })
public class TccBankConfig {

	@Bean
	@ConfigurationProperties(prefix = "spring.datasource")
	public DruidDataSource druidDataSource() {
		return new DruidDataSource();

	}

	@Bean
	public DataSourceTransactionManager transactionManager(DruidDataSource druidDataSource) {
		return new DataSourceTransactionManager(druidDataSource);
	}

	@Bean
	public JdbcTemplate jdbcTemplate(DruidDataSource druidDataSource) {
		return new JdbcTemplate(druidDataSource);
	}

	@Bean
	public GlobalTransactionScanner globalTransactionScanner() {
		return new GlobalTransactionScanner("tcc-bank-cmb", "demo-tx-grp");
	}

}

TransferMoneyDAO.java

package org.sky.tcc.bank.cmb.dao;

import org.sky.tcc.bean.AccountBean;

public interface TransferMoneyDAO {
	public void addAccount(AccountBean account) throws Exception;

	public int updateAmount(AccountBean account) throws Exception;

	public AccountBean getAccount(String accountNo) throws Exception;

	public AccountBean getAccountForUpdate(String accountNo) throws Exception;

	public int updateFreezedAmount(AccountBean account) throws Exception;
}

TransferMoneyDAOImpl.java

package org.sky.tcc.bank.cmb.dao;

import java.sql.ResultSet;
import java.sql.SQLException;
import org.sky.tcc.bean.AccountBean;
import org.sky.tcc.dao.BaseDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Component;

@Component
public class TransferMoneyDAOImpl extends BaseDAO implements TransferMoneyDAO {
	@Autowired
	private JdbcTemplate toJdbcTemplate;

	@Override
	public void addAccount(AccountBean account) throws Exception {
		String sql = "insert into bank_account(account_id,amount,freezed_amount) values(?,?,?)";
		toJdbcTemplate.update(sql, account.getAccountId(), account.getAmount(), account.getFreezedAmount());
	}

	@Override
	public int updateAmount(AccountBean account) throws Exception {
		String sql = "update bank_account set amount=?, freezed_amount=? where account_id=?";
		int result = 0;
		result = toJdbcTemplate.update(sql, account.getAmount(), account.getFreezedAmount(), account.getAccountId());
		return result;
	}

	@Override
	public AccountBean getAccount(String accountNo) throws Exception {
		String sql = "select account_id,amount,freezed_amount from bank_account where account_id=?";
		AccountBean account = null;
		// Object[] params = new Object[] { accountNo };
		try {
			account = toJdbcTemplate.queryForObject(sql, new RowMapper() {
				@Override
				public AccountBean mapRow(ResultSet rs, int rowNum) throws SQLException {

					AccountBean account = new AccountBean();
					account.setAccountId(rs.getString("account_id"));
					account.setAmount(rs.getDouble("amount"));
					account.setFreezedAmount(rs.getDouble("freezed_amount"));
					return account;
				}
			}, accountNo);
		} catch (Exception e) {
			logger.error("getAccount error: " + e.getMessage(), e);
			account = null;
		}
		return account;
	}

	@Override
	public AccountBean getAccountForUpdate(String accountNo) throws Exception {
		String sql = "select account_id,amount,freezed_amount from bank_account where account_id=? for update";
		AccountBean account = null;
		// Object[] params = new Object[] { accountNo };
		try {
			account = toJdbcTemplate.queryForObject(sql, new RowMapper() {
				@Override
				public AccountBean mapRow(ResultSet rs, int rowNum) throws SQLException {
					AccountBean account = new AccountBean();
					account.setAccountId(rs.getString("account_id"));
					account.setAmount(rs.getDouble("amount"));
					account.setFreezedAmount(rs.getDouble("freezed_amount"));
					return account;

				}
			}, accountNo);
		} catch (Exception e) {
			logger.error("getAccount error: " + e.getMessage(), e);
			account = null;
		}
		return account;
	}

	@Override
	public int updateFreezedAmount(AccountBean account) throws Exception {
		String sql = "update bank_account set freezed_amount=? where account_id=?";
		int result = 0;
		result = toJdbcTemplate.update(sql, account.getFreezedAmount(), account.getAccountId());
		return result;
	}

}

用于启用动的CMBApplication,此处因为我们用了.xml模式配置dubbo,因此可就不能使用@EnableDubbo了哦再次提醒一次。

package org.sky.tcc.bank.cmb;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;

@EnableAutoConfiguration
@ComponentScan(basePackages = { "org.sky.tcc.bank" })
public class CMBApplication {

	public static void main(String[] args) {
		SpringApplication.run(CMBApplication.class, args);

	}

}

到此为止两个dubbo provider制作 完成,我们把它们分别运行起来。

启动之前我放出此次在生产环境调整过的nacos-config.txt文件,你只要在nacos服务启动的情况下重新在seata/conf下

./nacos-config.sh localhost

就可以了。 

seata/conf/nacos-config.txt

transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.thread-factory.boss-thread-prefix=NettyBoss
transport.thread-factory.worker-thread-prefix=NettyServerNIOWorker
transport.thread-factory.server-executor-thread-prefix=NettyServerBizHandler
transport.thread-factory.share-boss-worker=false
transport.thread-factory.client-selector-thread-prefix=NettyClientSelector
transport.thread-factory.client-selector-thread-size=1
transport.thread-factory.client-worker-thread-prefix=NettyClientWorkerThread
transport.thread-factory.boss-thread-size=1
transport.thread-factory.worker-thread-size=8
transport.shutdown.wait=3
service.vgroup_mapping.demo-tx-grp=default
service.default.grouplist=192.168.56.101:8091
service.enableDegrade=false
service.disable=false
service.max.commit.retry.timeout=10000
service.max.rollback.retry.timeout=3
client.async.commit.buffer.limit=10000
client.lock.retry.internal=3
client.lock.retry.times=3
client.lock.retry.policy.branch-rollback-on-conflict=true
client.table.meta.check.enable=true
client.report.retry.count=1
client.tm.commit.retry.count=1
client.tm.rollback.retry.count=1
store.mode=db
store.file.dir=file_store/data
store.file.max-branch-session-size=16384
store.file.max-global-session-size=512
store.file.file-write-buffer-cache-size=16384
store.file.flush-disk-mode=async
store.file.session.reload.read_size=100
store.db.datasource=druid
store.db.db-type=mysql
store.db.driver-class-name=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://192.168.56.101:3306/seata?useUnicode=true
store.db.user=seata
store.db.password=111111
store.db.min-conn=1
store.db.max-conn=3
store.db.global.table=global_table
store.db.branch.table=branch_table
store.db.query-limit=100
store.db.lock-table=lock_table
recovery.committing-retry-period=1000
recovery.asyn-committing-retry-period=1000
recovery.rollbacking-retry-period=1000
recovery.timeout-retry-period=1000
transaction.undo.data.validation=true
transaction.undo.log.serialization=jackson
transaction.undo.log.save.days=1
transaction.undo.log.delete.period=86400000
transaction.undo.log.table=undo_log
transport.serialization=seata
transport.compressor=none
metrics.enabled=false
metrics.registry-type=compact
metrics.exporter-list=prometheus
metrics.exporter-prometheus-port=9898
support.spring.datasource.autoproxy=false
  • tcc-bank-icbc运行在28880端口;
  • tcc-bank-cmb运行在29990端口;

冬日魔幻之旅-seata+dubbo+nacos+springboot解决分布式事务的全网段唯一实践之作(下)_第11张图片

冬日魔幻之旅-seata+dubbo+nacos+springboot解决分布式事务的全网段唯一实践之作(下)_第12张图片

tcc-money-king工程

 为了全真模拟生产,我们制作了一个spring boot的consumer,在这个工程里我们依然使用springboot+xml配置混合的方式,关键 在该工程的业务方法内,我们看下去。

冬日魔幻之旅-seata+dubbo+nacos+springboot解决分布式事务的全网段唯一实践之作(下)_第13张图片

pom.xml


	4.0.0
	
		org.sky.demo
		nacos-parent
		0.0.1-SNAPSHOT
	
	org.sky.demo
	tcc-money-king
	0.0.1
	war
	Demo project Dubbo+Nacos+SeataTCC Consumer
	
		
			org.apache.dubbo
			dubbo
		
		
			org.apache.curator
			curator-recipes
		

		
			org.springframework.boot
			spring-boot-starter-test
			test
		
		
			org.spockframework
			spock-core
			test
		
		
			org.spockframework
			spock-spring
		
		
			org.springframework.boot
			spring-boot-configuration-processor
			true
		

		
			org.springframework.boot
			spring-boot-starter-log4j2
		
		
			org.springframework.boot
			spring-boot-starter-web
			
				
					org.springframework.boot
					spring-boot-starter-logging
				
				
					org.springframework.boot
					spring-boot-starter-tomcat
				
			
		
		
			org.springframework.boot
			spring-boot-starter-tomcat
			compile
		
		
			org.aspectj
			aspectjweaver
		
		
			com.lmax
			disruptor
		
		
			redis.clients
			jedis
		
		
			com.google.guava
			guava
		
		
			com.alibaba
			fastjson
		
		
		
			org.apache.dubbo
			dubbo-registry-nacos
		
		
			com.alibaba.nacos
			nacos-client
		
		
			org.sky.demo
			skycommon
			${skycommon.version}
		
		
			io.seata
			seata-all
		
		
			com.alibaba.boot
			nacos-config-spring-boot-starter
			
				
					nacos-client
					com.alibaba.nacos
				
			
		
		
			org.apache.curator
			curator-framework
		
		
			org.apache.curator
			curator-recipes
		
		
			io.netty
			netty-all
		
		
			org.projectlombok
			lombok
		
		
			javax.servlet
			javax.servlet-api
			${javax.servlet.version}
			provided
		
	
	
		src/main/java
		src/test/java
		
			
				org.springframework.boot
				spring-boot-maven-plugin
			
		
		
			
				src/main/resources
			
			
				src/main/webapp
				META-INF/resources
				
					**/**
				
			
			
				src/main/resources
				true
				
					application.properties
					application-${profileActive}.properties
				
			
		
	

application.properties

server.port=8082
server.tomcat.maxConnections=300
server.tomcat.maxThreads=300
server.tomcat.uriEncoding=UTF-8
server.tomcat.maxThreads=300
server.tomcat.minSpareThreads=150
server.connectionTimeout=20000
server.tomcat.maxHttpPostSize=0
server.tomcat.acceptCount=300
logging.config=classpath:log4j2.xml

spring/dubbo-reference.xml





	
		
	
	
	
	

	
	
	
	

spring boot自动注解用SeataAutoConfig.java

package org.sky.tcc.moneyking.config;

import io.seata.spring.annotation.GlobalTransactionScanner;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;

@Configuration
@ImportResource(locations = { "spring/dubbo-reference.xml" })
public class SeataAutoConfig {
	@Bean
	public GlobalTransactionScanner globalTransactionScanner() {
		return new GlobalTransactionScanner("tcc-bank-sample", "demo-tx-grp");
	}
}

这边的GlobalTRansactionScanner里的第一个参数可就是事务边界了啊,注意这边的事务group必须和seata端的nacos-config.txt内配置的完全一致。

TccMoneyKingBizService.java

package org.sky.tcc.moneyking.service.biz;

import org.sky.exception.DemoRpcRunTimeException;

public interface TccMoneyKingBizService {
	public boolean transfer(String from, String to, double amount) throws DemoRpcRunTimeException;
}

核心业务方法TccMoneyKingBizServiceImpl.java

package org.sky.tcc.moneyking.service.biz;

import org.sky.exception.DemoRpcRunTimeException;
import org.sky.service.BaseService;
import org.sky.tcc.bank.cmb.dubbo.PlusMoneyAction;
import org.sky.tcc.bank.icbc.dubbo.MinusMoneyAction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import io.seata.spring.annotation.GlobalTransactional;

@Service
public class TccMoneyKingBizServiceImpl extends BaseService implements TccMoneyKingBizService {

	@Autowired
	private MinusMoneyAction minusMoneyAction;
	
	@Autowired
	private PlusMoneyAction plusMoneyAction;

	@Override
	@GlobalTransactional(timeoutMills = 300000, name = "tcc-bank-sample")
	public boolean transfer(String from, String to, double amount) throws DemoRpcRunTimeException {

		boolean answer = minusMoneyAction.prepareMinus(null, from, amount);
		if (!answer) {
			// 扣钱参与者,一阶段失败; 回滚本地事务和分布式事务
			throw new DemoRpcRunTimeException("账号:[" + from + "] 预扣款失败");
		}
		// 加钱参与者,一阶段执行
		answer = plusMoneyAction.prepareAdd(null, to, amount);

		if (!answer) {
			throw new DemoRpcRunTimeException("账号:[" + to + "] 预收款失败");
		}
		return true;
	}

}

这边可以看到是如何调用icbc的扣款和cmb的打款动作 的,这边根本不需要你再去写什么commit和rollback,只要这两个dubbo provider中的prepare方法执行正常,seata就会自动回调icbc和cmb中的commit方法;只要icbc或者 是cmb中有任何一步抛错,就会触发这两个provider中的业务回滚rollback方法。

MonekyKingController.java

package org.sky.tcc.moneyking.controller;

import java.util.HashMap;
import java.util.Map;

import org.sky.controller.BaseController;
import org.sky.tcc.bean.AccountBean;
import org.sky.tcc.moneyking.service.biz.TccMoneyKingBizService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

@RestController
@RequestMapping("moneyking")
public class MonekyKingController extends BaseController {

	@Autowired
	private TccMoneyKingBizService tccMoneyKingBizService;

	@PostMapping(value = "/transfermoney", produces = "application/json")
	public ResponseEntity transferMoney(@RequestBody String params) throws Exception {
		ResponseEntity response = null;
		String returnResultStr;
		HttpHeaders headers = new HttpHeaders();
		headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
		Map result = new HashMap<>();
		try {
			logger.info("input params=====" + params);
			JSONObject requestJsonObj = JSON.parseObject(params);
			Map acctMap = getAccountFromJson(requestJsonObj);
			AccountBean acctFrom = acctMap.get("account_from");
			AccountBean acctTo = acctMap.get("account_to");
			boolean answer = tccMoneyKingBizService.transfer(acctFrom.getAccountId(), acctTo.getAccountId(),
					acctFrom.getAmount());
//			tccMoneyKingBizService.icbcHello();
//			tccMoneyKingBizService.cmbHello();
			result.put("account_from", acctFrom.getAccountId());
			result.put("account_to", acctTo.getAccountId());
			result.put("transfer_money", acctFrom.getAmount());
			result.put("message", "transferred successfully");
			returnResultStr = JSON.toJSONString(result);
			logger.info("transfer money successfully======>\n" + returnResultStr);
			response = new ResponseEntity<>(returnResultStr, headers, HttpStatus.OK);
		} catch (Exception e) {
			logger.error("transfer money with error: " + e.getMessage(), e);
			result.put("message", "transfer money with error[ " + e.getMessage() + "]");
			returnResultStr = JSON.toJSONString(result);
			response = new ResponseEntity<>(returnResultStr, headers, HttpStatus.EXPECTATION_FAILED);
		}
		return response;
	}
}

用于启动的MoneyKingApplication.java

package org.sky.tcc.moneyking;

import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.transaction.annotation.EnableTransactionManagement;


@ServletComponentScan
@EnableAutoConfiguration
@ComponentScan(basePackages = { "org.sky" })

public class MoneyKingApplication {

	public static void main(String[] args) {
		SpringApplication.run(MoneyKingApplication.class, args);
	}

}

把MoneyKingApplication启动起来。

冬日魔幻之旅-seata+dubbo+nacos+springboot解决分布式事务的全网段唯一实践之作(下)_第14张图片

看,两个dubbo provider已经被seata纳入托管。

测试案例

我们初始化两个帐户,一个叫a一个叫b。然后通过 a给b每次打100块钱。

use bank_icbc;
delete from bank_account;
insert into bank_account
(account_id,amount,freezed_amount)values('a',50000,0);
use bank_cmb;
delete from bank_account;
insert into bank_account
(account_id,amount,freezed_amount)values('b',100,0);

正常划款 

{
	"account_from" : "a",
	"account_to" : "b",
	"transfer_money" : 100
}

冬日魔幻之旅-seata+dubbo+nacos+springboot解决分布式事务的全网段唯一实践之作(下)_第15张图片

请观察icbc和cmb的后台,从prepare为人为触发外,commit的一系列的动作都 是被自动触发的。冬日魔幻之旅-seata+dubbo+nacos+springboot解决分布式事务的全网段唯一实践之作(下)_第16张图片

冬日魔幻之旅-seata+dubbo+nacos+springboot解决分布式事务的全网段唯一实践之作(下)_第17张图片

冬日魔幻之旅-seata+dubbo+nacos+springboot解决分布式事务的全网段唯一实践之作(下)_第18张图片

再看数据库端

冬日魔幻之旅-seata+dubbo+nacos+springboot解决分布式事务的全网段唯一实践之作(下)_第19张图片

冬日魔幻之旅-seata+dubbo+nacos+springboot解决分布式事务的全网段唯一实践之作(下)_第20张图片

 

非正常划款

转帐额大于余额

{
	"account_from" : "a",
	"account_to" : "b",
	"transfer_money" : 1000000000
}

来看icbc和cmb端的回滚

冬日魔幻之旅-seata+dubbo+nacos+springboot解决分布式事务的全网段唯一实践之作(下)_第21张图片 看到没,rollback被自动触发。数据库端当然也没被插进数据(被回滚掉了)。

冬日魔幻之旅-seata+dubbo+nacos+springboot解决分布式事务的全网段唯一实践之作(下)_第22张图片

冬日魔幻之旅-seata+dubbo+nacos+springboot解决分布式事务的全网段唯一实践之作(下)_第23张图片

帐户不存在

{
	"account_from" : "a",
	"account_to" : "c",
	"transfer_money" : 100
}

冬日魔幻之旅-seata+dubbo+nacos+springboot解决分布式事务的全网段唯一实践之作(下)_第24张图片

 冬日魔幻之旅-seata+dubbo+nacos+springboot解决分布式事务的全网段唯一实践之作(下)_第25张图片

总结

我们可以通过上述的例子看到,seata把分布式事务的锁可以定义为最最小业务原子操作,这使得本来冗长的事务锁的开销可以尽量的小,尽快的释放原子操作从而加速了分布式事物处理的效率。

Seata通过数据一致性、尽可能少破坏业务代码、高性能这三者关系中进行了一个取舍,它付的代价就是使用netty通讯实现了异步消息回调+spring aop,这个对服务器的硬件要求很高。当服务器的硬件如果跟不上的话,你会发现部署一个seata简直是要了你的老命了,很多网上的网友也都说过,我部署了一个seata比原来竟然慢了8倍。这倒不是说这个框架不好,只是它的开销会比较大。当然,在现今硬件越来越廉价的情况下,要保证数据的最终一致完整性,总要有适当的付出的。

你可能感兴趣的:(架构师修练之道)