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

本文主要是介绍冬日魔幻之旅-seata+dubbo+nacos+springboot解决分布式事务的全网段唯一实践之作(下),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

开篇

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

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

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

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

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

例子-跨行转款问题

还记得我们在上曾经出现过这么一个例子用于详细描述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)。

两个不同的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。

在我们的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的全注解方法来搭建整个项目的框架,见下例。

这边除了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一下即可,如下例:

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

整个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

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.sky.demo</groupId><artifactId>nacos-parent</artifactId><version>0.0.1-SNAPSHOT</version></parent><groupId>org.sky.demo</groupId><artifactId>tcc-bank-icbc</artifactId><version>0.0.1</version><name>tcc-bank-icbc</name><description>Demo project Dubbo+Nacos+SeataTCC</description><properties><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding></properties><dependencies><dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId></dependency><dependency><groupId>org.mybatis</groupId><artifactId>mybatis-spring</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId><exclusions><exclusion><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-logging</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.apache.dubbo</groupId><artifactId>dubbo</artifactId></dependency><dependency><groupId>org.apache.curator</groupId><artifactId>curator-framework</artifactId></dependency><dependency><groupId>org.apache.curator</groupId><artifactId>curator-recipes</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.spockframework</groupId><artifactId>spock-core</artifactId><scope>test</scope></dependency><dependency><groupId>org.spockframework</groupId><artifactId>spock-spring</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-log4j2</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><exclusions><exclusion><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-logging</artifactId></exclusion><exclusion><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-tomcat</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId></dependency><dependency><groupId>com.lmax</groupId><artifactId>disruptor</artifactId></dependency><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId></dependency><dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId></dependency><!-- Dubbo Registry Nacos --><dependency><groupId>org.apache.dubbo</groupId><artifactId>dubbo-registry-nacos</artifactId></dependency><dependency><groupId>com.alibaba.nacos</groupId><artifactId>nacos-client</artifactId></dependency><dependency><groupId>org.sky.demo</groupId><artifactId>skycommon</artifactId><version>${skycommon.version}</version></dependency><dependency><groupId>io.seata</groupId><artifactId>seata-all</artifactId></dependency><dependency><groupId>com.alibaba.boot</groupId><artifactId>nacos-config-spring-boot-starter</artifactId><exclusions><exclusion><artifactId>nacos-client</artifactId><groupId>com.alibaba.nacos</groupId></exclusion></exclusions></dependency><dependency><groupId>io.netty</groupId><artifactId>netty-all</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency></dependencies><build><finalName>${project.artifactId}</finalName><sourceDirectory>src/main/java</sourceDirectory><testSourceDirectory>src/test/java</testSourceDirectory><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId></plugin><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><jvmArguments>-Dfile.encoding=UTF-8</jvmArguments></configuration><executions><execution><goals><goal>repackage</goal></goals></execution></executions></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-war-plugin</artifactId><version>2.6</version><configuration><failOnMissingWebXml>false</failOnMissingWebXml></configuration></plugin></plugins><resources><resource><directory>src/main/resources</directory><excludes><exclude>application*.properties</exclude></excludes></resource><resource><directory>src/main/webapp</directory><targetPath>META-INF/resources</targetPath><includes><include>**/**</include></includes></resource><resource><directory>src/main/resources</directory><filtering>true</filtering><includes><include>application.properties</include></includes></resource></resources></build>
</project>

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();}@Beanpublic DataSourceTransactionManager transactionManager(DruidDataSource druidDataSource) {return new DataSourceTransactionManager(druidDataSource);}@Beanpublic JdbcTemplate jdbcTemplate(DruidDataSource druidDataSource) {return new JdbcTemplate(druidDataSource);}@Beanpublic 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

<?xml version="1.0" encoding="UTF-8"?>
<!-- ~ Copyright 1999-2018 Alibaba Group Holding Ltd. ~ ~ Licensed under the Apache License, Version 2.0 (the "License"); ~ you may not use this file except in compliance with the License. ~ You may obtain a copy of the License at ~ ~ http://www.apache.org/licenses/LICENSE-2.0 ~ ~ Unless required by applicable law or agreed to in writing, software ~ distributed under the License is distributed on an "AS IS" BASIS, ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ~ See the License for the specific language governing permissions and ~ limitations under the License. -->
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://code.alibabatech.com/schema/dubbohttp://code.alibabatech.com/schema/dubbo/dubbo.xsd"default-autowire="byName"><bean id="transactionTemplate"class="org.springframework.transaction.support.TransactionTemplate"><property name="propagationBehaviorName"><value>PROPAGATION_REQUIRES_NEW</value></property><property name="transactionManager"><ref bean="transactionManager" /></property></bean>
</beans>

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

spring/dubbo-bean.xml

<?xml version="1.0" encoding="UTF-8"?>
<!-- ~ Copyright 1999-2018 Alibaba Group Holding Ltd. ~ ~ Licensed under the Apache License, Version 2.0 (the "License"); ~ you may not use this file except in compliance with the License. ~ You may obtain a copy of the License at ~ ~ http://www.apache.org/licenses/LICENSE-2.0 ~ ~ Unless required by applicable law or agreed to in writing, software ~ distributed under the License is distributed on an "AS IS" BASIS, ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ~ See the License for the specific language governing permissions and ~ limitations under the License. -->
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://code.alibabatech.com/schema/dubbohttp://code.alibabatech.com/schema/dubbo/dubbo.xsd"default-autowire="byName"><dubbo:application name="tcc-bank-icbc" /><!--使用 zookeeper 注册中心暴露服务,注意要先开启 zookeeper --><dubbo:registry address="nacos://192.168.56.101:8848" /><!--<transfer:registry address="multicast://224.5.6.7:1234?unicast=false" /> --><dubbo:protocol name="dubbo" port="28880" /><dubbo:provider timeout="30000" threads="10"threadpool="fixed" /><!-- 第一个TCC 参与者服务发布 --><dubbo:serviceinterface="org.sky.tcc.bank.icbc.dubbo.MinusMoneyAction"timeout="30000" ref="minusMoneyActionImpl" /><bean name="minusMoneyActionImpl"class="org.sky.tcc.bank.icbc.dubbo.MinusMoneyActionImpl" />
</beans>

我们可以看到在这个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*/@Autowiredprivate TransferMoneyDAO transferMoneyDAO;/*** 扣钱数据源事务模板*/@Autowiredprivate TransactionTemplate transactionTemplate;@Overridepublic String sayHello() throws RuntimeException {return "hi I am icbc-dubbo";}@Overridepublic boolean prepareMinus(BusinessActionContext businessActionContext, String accountNo, double amount) {logger.info("==========into prepareMinus");// 分布式事务IDfinal String xid = RootContext.getXID();return transactionTemplate.execute(new TransactionCallback<Boolean>() {@Overridepublic 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;}}});}@Overridepublic boolean commit(BusinessActionContext businessActionContext) {logger.info("======>into MinusMoneyActionImpl.commit() method");// 分布式事务IDfinal String xid = RootContext.getXID();// 账户IDfinal String accountNo = String.valueOf(businessActionContext.getActionContext("accountNo"));// 转出金额final double amount = Double.valueOf(String.valueOf(businessActionContext.getActionContext("amount")));return transactionTemplate.execute(new TransactionCallback<Boolean>() {@Overridepublic 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;}}});}@Overridepublic boolean rollback(BusinessActionContext businessActionContext) {logger.info("======>into MinusMoneyActionImpl.rollback() method");// 分布式事务IDfinal String xid = RootContext.getXID();// 账户IDfinal String accountNo = String.valueOf(businessActionContext.getActionContext("accountNo"));// 转出金额final double amount = Double.valueOf(String.valueOf(businessActionContext.getActionContext("amount")));return transactionTemplate.execute(new TransactionCallback<Boolean>() {@Overridepublic 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服务在实现时应当允许空回滚的执行;如果你觉得前面这段话有点拗口,那么我们再说了白一点,看下图就能理解了

从上图看到,这个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 {@Autowiredprivate JdbcTemplate fromJdbcTemplate;@Overridepublic 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());}@Overridepublic 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;}@Overridepublic 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<AccountBean>() {@Overridepublic 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;}@Overridepublic 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<AccountBean>() {@Overridepublic 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;}@Overridepublic 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工程

这是一个“招行打款”的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 {@Autowiredprivate TransactionTemplate transactionTemplate;@Autowiredprivate TransferMoneyDAO transferMoneyDAO;@Overridepublic String sayHello() throws RuntimeException {return "hi I am cmb-dubbo";}@Overridepublic boolean prepareAdd(BusinessActionContext businessActionContext, String accountNo, double amount) {logger.info("======>inti prepare add");final String xid = RootContext.getXID();return transactionTemplate.execute(new TransactionCallback<Boolean>() {@Overridepublic 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;}}});}@Overridepublic boolean commit(BusinessActionContext businessActionContext) {logger.info("======>into PlusMoneyActionImpl.commit() method");// 分布式事务IDfinal String xid = RootContext.getXID();// 账户IDfinal String accountNo = String.valueOf(businessActionContext.getActionContext("accountNo"));// 转出金额final double amount = Double.valueOf(String.valueOf(businessActionContext.getActionContext("amount")));return transactionTemplate.execute(new TransactionCallback<Boolean>() {@Overridepublic 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;}}});}@Overridepublic boolean rollback(BusinessActionContext businessActionContext) {logger.info("======>into PlusMoneyActionImpl.rollback() method");// 分布式事务IDfinal String xid = RootContext.getXID();// 账户IDfinal String accountNo = String.valueOf(businessActionContext.getActionContext("accountNo"));// 转出金额final double amount = Double.valueOf(String.valueOf(businessActionContext.getActionContext("amount")));return transactionTemplate.execute(new TransactionCallback<Boolean>() {@Overridepublic 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

<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.sky.demo</groupId><artifactId>nacos-parent</artifactId><version>0.0.1-SNAPSHOT</version></parent><groupId>org.sky.demo</groupId><artifactId>tcc-bank-cmb</artifactId><version>0.0.1</version><name>tcc-bank-cmb</name><description>Demo project Dubbo+Nacos+SeataTCC</description><properties><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding></properties><dependencies><dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId></dependency><dependency><groupId>org.mybatis</groupId><artifactId>mybatis-spring</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId><exclusions><exclusion><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-logging</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.apache.dubbo</groupId><artifactId>dubbo</artifactId></dependency><dependency><groupId>org.apache.curator</groupId><artifactId>curator-framework</artifactId></dependency><dependency><groupId>org.apache.curator</groupId><artifactId>curator-recipes</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.spockframework</groupId><artifactId>spock-core</artifactId><scope>test</scope></dependency><dependency><groupId>org.spockframework</groupId><artifactId>spock-spring</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-log4j2</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><exclusions><exclusion><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-logging</artifactId></exclusion><exclusion><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-tomcat</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId></dependency><dependency><groupId>com.lmax</groupId><artifactId>disruptor</artifactId></dependency><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId></dependency><dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId></dependency><!-- Dubbo Registry Nacos --><dependency><groupId>org.apache.dubbo</groupId><artifactId>dubbo-registry-nacos</artifactId></dependency><dependency><groupId>com.alibaba.nacos</groupId><artifactId>nacos-client</artifactId></dependency><dependency><groupId>org.sky.demo</groupId><artifactId>skycommon</artifactId><version>${skycommon.version}</version></dependency><dependency><groupId>io.seata</groupId><artifactId>seata-all</artifactId></dependency><dependency><groupId>com.alibaba.boot</groupId><artifactId>nacos-config-spring-boot-starter</artifactId><exclusions><exclusion><artifactId>nacos-client</artifactId><groupId>com.alibaba.nacos</groupId></exclusion></exclusions></dependency><dependency><groupId>io.netty</groupId><artifactId>netty-all</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency></dependencies><build><finalName>${project.artifactId}</finalName><sourceDirectory>src/main/java</sourceDirectory><testSourceDirectory>src/test/java</testSourceDirectory><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId></plugin><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><jvmArguments>-Dfile.encoding=UTF-8</jvmArguments></configuration><executions><execution><goals><goal>repackage</goal></goals></execution></executions></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-war-plugin</artifactId><version>2.6</version><configuration><failOnMissingWebXml>false</failOnMissingWebXml></configuration></plugin></plugins><resources><resource><directory>src/main/resources</directory><excludes><exclude>application*.properties</exclude></excludes></resource><resource><directory>src/main/webapp</directory><targetPath>META-INF/resources</targetPath><includes><include>**/**</include></includes></resource><resource><directory>src/main/resources</directory><filtering>true</filtering><includes><include>application.properties</include></includes></resource></resources></build>
</project>

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

<?xml version="1.0" encoding="UTF-8"?>
<!-- ~ Copyright 1999-2018 Alibaba Group Holding Ltd. ~ ~ Licensed under the Apache License, Version 2.0 (the "License"); ~ you may not use this file except in compliance with the License. ~ You may obtain a copy of the License at ~ ~ http://www.apache.org/licenses/LICENSE-2.0 ~ ~ Unless required by applicable law or agreed to in writing, software ~ distributed under the License is distributed on an "AS IS" BASIS, ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ~ See the License for the specific language governing permissions and ~ limitations under the License. -->
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://code.alibabatech.com/schema/dubbohttp://code.alibabatech.com/schema/dubbo/dubbo.xsd"default-autowire="byName"><dubbo:application name="tcc-bank-cmb" /><!--使用 zookeeper 注册中心暴露服务,注意要先开启 zookeeper --><dubbo:registry address="nacos://192.168.56.101:8848" /><!--<transfer:registry address="multicast://224.5.6.7:1234?unicast=false" /> --><dubbo:protocol name="dubbo" port="29990" /><dubbo:provider timeout="30000" threads="10"threadpool="fixed" /><!-- 第一个TCC 参与者服务发布 --><dubbo:serviceinterface="org.sky.tcc.bank.cmb.dubbo.PlusMoneyAction"timeout="30000" ref="plusMoneyActionImpl" /><bean name="plusMoneyActionImpl"class="org.sky.tcc.bank.cmb.dubbo.PlusMoneyActionImpl" />
</beans>

 spring/spirng-bean.xml

<?xml version="1.0" encoding="UTF-8"?>
<!-- ~ Copyright 1999-2018 Alibaba Group Holding Ltd. ~ ~ Licensed under the Apache License, Version 2.0 (the "License"); ~ you may not use this file except in compliance with the License. ~ You may obtain a copy of the License at ~ ~ http://www.apache.org/licenses/LICENSE-2.0 ~ ~ Unless required by applicable law or agreed to in writing, software ~ distributed under the License is distributed on an "AS IS" BASIS, ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ~ See the License for the specific language governing permissions and ~ limitations under the License. -->
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://code.alibabatech.com/schema/dubbohttp://code.alibabatech.com/schema/dubbo/dubbo.xsd"default-autowire="byName"><bean id="transactionTemplate"class="org.springframework.transaction.support.TransactionTemplate"><property name="propagationBehaviorName"><value>PROPAGATION_REQUIRES_NEW</value></property><property name="transactionManager"><ref bean="transactionManager" /></property></bean>
</beans>

自动装配用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();}@Beanpublic DataSourceTransactionManager transactionManager(DruidDataSource druidDataSource) {return new DataSourceTransactionManager(druidDataSource);}@Beanpublic JdbcTemplate jdbcTemplate(DruidDataSource druidDataSource) {return new JdbcTemplate(druidDataSource);}@Beanpublic 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 {@Autowiredprivate JdbcTemplate toJdbcTemplate;@Overridepublic 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());}@Overridepublic 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;}@Overridepublic 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<AccountBean>() {@Overridepublic 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;}@Overridepublic 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<AccountBean>() {@Overridepublic 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;}@Overridepublic 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端口;

tcc-money-king工程

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

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.sky.demo</groupId><artifactId>nacos-parent</artifactId><version>0.0.1-SNAPSHOT</version></parent><groupId>org.sky.demo</groupId><artifactId>tcc-money-king</artifactId><version>0.0.1</version><packaging>war</packaging><description>Demo project Dubbo+Nacos+SeataTCC Consumer</description><dependencies><dependency><groupId>org.apache.dubbo</groupId><artifactId>dubbo</artifactId></dependency><dependency><groupId>org.apache.curator</groupId><artifactId>curator-recipes</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.spockframework</groupId><artifactId>spock-core</artifactId><scope>test</scope></dependency><dependency><groupId>org.spockframework</groupId><artifactId>spock-spring</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-log4j2</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><exclusions><exclusion><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-logging</artifactId></exclusion><exclusion><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-tomcat</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-tomcat</artifactId><scope>compile</scope></dependency><dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId></dependency><dependency><groupId>com.lmax</groupId><artifactId>disruptor</artifactId></dependency><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId></dependency><dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId></dependency><!-- Dubbo Registry Nacos --><dependency><groupId>org.apache.dubbo</groupId><artifactId>dubbo-registry-nacos</artifactId></dependency><dependency><groupId>com.alibaba.nacos</groupId><artifactId>nacos-client</artifactId></dependency><dependency><groupId>org.sky.demo</groupId><artifactId>skycommon</artifactId><version>${skycommon.version}</version></dependency><dependency><groupId>io.seata</groupId><artifactId>seata-all</artifactId></dependency><dependency><groupId>com.alibaba.boot</groupId><artifactId>nacos-config-spring-boot-starter</artifactId><exclusions><exclusion><artifactId>nacos-client</artifactId><groupId>com.alibaba.nacos</groupId></exclusion></exclusions></dependency><dependency><groupId>org.apache.curator</groupId><artifactId>curator-framework</artifactId></dependency><dependency><groupId>org.apache.curator</groupId><artifactId>curator-recipes</artifactId></dependency><dependency><groupId>io.netty</groupId><artifactId>netty-all</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>javax.servlet</groupId><artifactId>javax.servlet-api</artifactId><version>${javax.servlet.version}</version><scope>provided</scope></dependency></dependencies><build><sourceDirectory>src/main/java</sourceDirectory><testSourceDirectory>src/test/java</testSourceDirectory><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins><resources><resource><directory>src/main/resources</directory></resource><resource><directory>src/main/webapp</directory><targetPath>META-INF/resources</targetPath><includes><include>**/**</include></includes></resource><resource><directory>src/main/resources</directory><filtering>true</filtering><includes><include>application.properties</include><include>application-${profileActive}.properties</include></includes></resource></resources></build>
</project>

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

<?xml version="1.0" encoding="UTF-8"?>
<!-- ~ Copyright 1999-2018 Alibaba Group Holding Ltd. ~ ~ Licensed under the Apache License, Version 2.0 (the "License"); ~ you may not use this file except in compliance with the License. ~ You may obtain a copy of the License at ~ ~ http://www.apache.org/licenses/LICENSE-2.0 ~ ~ Unless required by applicable law or agreed to in writing, software ~ distributed under the License is distributed on an "AS IS" BASIS, ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ~ See the License for the specific language governing permissions and ~ limitations under the License. -->
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://code.alibabatech.com/schema/dubbohttp://code.alibabatech.com/schema/dubbo/dubbo.xsd"default-autowire="byName"><dubbo:application name="tcc-bank-sample"><dubbo:parameter key="qos.enable" value="false" /></dubbo:application><!--使用 zookeeper 注册中心暴露服务,注意要先开启 zookeeper --><dubbo:registry address="nacos://192.168.56.101:8848" /><!--<transfer:registry address="multicast://224.5.6.7:1234?unicast=false" /> --><!-- 第一个TCC参与者 服务订阅 --><dubbo:reference id="minusMoneyAction"interface="org.sky.tcc.bank.icbc.dubbo.MinusMoneyAction"check="false" lazy="true" /><!-- 第二个TCC参与者 服务订阅 --><dubbo:reference id="plusMoneyAction"interface="org.sky.tcc.bank.cmb.dubbo.PlusMoneyAction"check="false" lazy="true" /></beans>

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 {@Beanpublic 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 {@Autowiredprivate MinusMoneyAction minusMoneyAction;@Autowiredprivate 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 {@Autowiredprivate TccMoneyKingBizService tccMoneyKingBizService;@PostMapping(value = "/transfermoney", produces = "application/json")public ResponseEntity<String> transferMoney(@RequestBody String params) throws Exception {ResponseEntity<String> response = null;String returnResultStr;HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_JSON_UTF8);Map<String, Object> result = new HashMap<>();try {logger.info("input params=====" + params);JSONObject requestJsonObj = JSON.parseObject(params);Map<String, AccountBean> 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启动起来。

看,两个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
}

请观察icbc和cmb的后台,从prepare为人为触发外,commit的一系列的动作都 是被自动触发的。

再看数据库端

 

非正常划款

转帐额大于余额

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

来看icbc和cmb端的回滚

 看到没,rollback被自动触发。数据库端当然也没被插进数据(被回滚掉了)。

帐户不存在

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

 

总结

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

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

这篇关于冬日魔幻之旅-seata+dubbo+nacos+springboot解决分布式事务的全网段唯一实践之作(下)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/200977

相关文章

在Ubuntu上部署SpringBoot应用的操作步骤

《在Ubuntu上部署SpringBoot应用的操作步骤》随着云计算和容器化技术的普及,Linux服务器已成为部署Web应用程序的主流平台之一,Java作为一种跨平台的编程语言,具有广泛的应用场景,本... 目录一、部署准备二、安装 Java 环境1. 安装 JDK2. 验证 Java 安装三、安装 mys

Springboot的ThreadPoolTaskScheduler线程池轻松搞定15分钟不操作自动取消订单

《Springboot的ThreadPoolTaskScheduler线程池轻松搞定15分钟不操作自动取消订单》:本文主要介绍Springboot的ThreadPoolTaskScheduler线... 目录ThreadPoolTaskScheduler线程池实现15分钟不操作自动取消订单概要1,创建订单后

oracle数据库索引失效的问题及解决

《oracle数据库索引失效的问题及解决》本文总结了在Oracle数据库中索引失效的一些常见场景,包括使用isnull、isnotnull、!=、、、函数处理、like前置%查询以及范围索引和等值索引... 目录oracle数据库索引失效问题场景环境索引失效情况及验证结论一结论二结论三结论四结论五总结ora

JAVA中整型数组、字符串数组、整型数和字符串 的创建与转换的方法

《JAVA中整型数组、字符串数组、整型数和字符串的创建与转换的方法》本文介绍了Java中字符串、字符数组和整型数组的创建方法,以及它们之间的转换方法,还详细讲解了字符串中的一些常用方法,如index... 目录一、字符串、字符数组和整型数组的创建1、字符串的创建方法1.1 通过引用字符数组来创建字符串1.2

Redis事务与数据持久化方式

《Redis事务与数据持久化方式》该文档主要介绍了Redis事务和持久化机制,事务通过将多个命令打包执行,而持久化则通过快照(RDB)和追加式文件(AOF)两种方式将内存数据保存到磁盘,以防止数据丢失... 目录一、Redis 事务1.1 事务本质1.2 数据库事务与redis事务1.2.1 数据库事务1.

element-ui下拉输入框+resetFields无法回显的问题解决

《element-ui下拉输入框+resetFields无法回显的问题解决》本文主要介绍了在使用ElementUI的下拉输入框时,点击重置按钮后输入框无法回显数据的问题,具有一定的参考价值,感兴趣的... 目录描述原因问题重现解决方案方法一方法二总结描述第一次进入页面,不做任何操作,点击重置按钮,再进行下

SpringCloud集成AlloyDB的示例代码

《SpringCloud集成AlloyDB的示例代码》AlloyDB是GoogleCloud提供的一种高度可扩展、强性能的关系型数据库服务,它兼容PostgreSQL,并提供了更快的查询性能... 目录1.AlloyDBjavascript是什么?AlloyDB 的工作原理2.搭建测试环境3.代码工程1.

Java调用Python代码的几种方法小结

《Java调用Python代码的几种方法小结》Python语言有丰富的系统管理、数据处理、统计类软件包,因此从java应用中调用Python代码的需求很常见、实用,本文介绍几种方法从java调用Pyt... 目录引言Java core使用ProcessBuilder使用Java脚本引擎总结引言python

SpringBoot操作spark处理hdfs文件的操作方法

《SpringBoot操作spark处理hdfs文件的操作方法》本文介绍了如何使用SpringBoot操作Spark处理HDFS文件,包括导入依赖、配置Spark信息、编写Controller和Ser... 目录SpringBoot操作spark处理hdfs文件1、导入依赖2、配置spark信息3、cont

springboot整合 xxl-job及使用步骤

《springboot整合xxl-job及使用步骤》XXL-JOB是一个分布式任务调度平台,用于解决分布式系统中的任务调度和管理问题,文章详细介绍了XXL-JOB的架构,包括调度中心、执行器和Web... 目录一、xxl-job是什么二、使用步骤1. 下载并运行管理端代码2. 访问管理页面,确认是否启动成功