8

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

 3 years ago
source link: https://blog.csdn.net/lifetragedy/article/details/104084983
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

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

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

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

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

例子-跨行转款问题

watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2xpZmV0cmFnZWR5,size_16,color_FFFFFF,t_70

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

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

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

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

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

转帐要么成功要么失败;

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

20200125221120297.png

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

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

20200125221404736.png

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

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

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。
watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2xpZmV0cmFnZWR5,size_16,color_FFFFFF,t_70

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

20200125223707768.png

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

watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2xpZmV0cmFnZWR5,size_16,color_FFFFFF,t_70

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

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

20200125224435563.png
watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2xpZmV0cmFnZWR5,size_16,color_FFFFFF,t_70
watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2xpZmV0cmFnZWR5,size_16,color_FFFFFF,t_70

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

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

20200125225108887.png

pom.xml

application.properties

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

TccBankConfig.java

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

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

spring/spring-bean.xml

对于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工程内了。

我们可以通过实现类看到它其实是事先了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

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

一开始它会从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服务在实现时应当允许空回滚的执行;如果你觉得前面这段话有点拗口,那么我们再说了白一点,看下图就能理解了

    watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2xpZmV0cmFnZWR5,size_16,color_FFFFFF,t_70

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

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

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

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

TransferMoneyDAO.java

TransferMoneyDAOImpl.java

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

tcc-bank-cmb工程

watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2xpZmV0cmFnZWR5,size_16,color_FFFFFF,t_70

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

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

PlusMoneyActionImpl.java

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

pom.xml

application.properties

spring/dubbo-bean.xml

 spring/spirng-bean.xml

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

TransferMoneyDAO.java

TransferMoneyDAOImpl.java

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

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

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

./nacos-config.sh localhost

就可以了。 

seata/conf/nacos-config.txt

  • tcc-bank-icbc运行在28880端口;
  • tcc-bank-cmb运行在29990端口;
watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2xpZmV0cmFnZWR5,size_16,color_FFFFFF,t_70
watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2xpZmV0cmFnZWR5,size_16,color_FFFFFF,t_70

tcc-money-king工程

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

watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2xpZmV0cmFnZWR5,size_16,color_FFFFFF,t_70

pom.xml

application.properties

spring/dubbo-reference.xml

spring boot自动注解用SeataAutoConfig.java

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

TccMoneyKingBizService.java

核心业务方法TccMoneyKingBizServiceImpl.java

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

MonekyKingController.java

用于启动的MoneyKingApplication.java

把MoneyKingApplication启动起来。

20200126013126889.png
watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2xpZmV0cmFnZWR5,size_16,color_FFFFFF,t_70

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

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

20200126013747574.png

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

watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2xpZmV0cmFnZWR5,size_16,color_FFFFFF,t_70
watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2xpZmV0cmFnZWR5,size_16,color_FFFFFF,t_70
watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2xpZmV0cmFnZWR5,size_16,color_FFFFFF,t_70

再看数据库端

watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2xpZmV0cmFnZWR5,size_16,color_FFFFFF,t_70
20200126015545770.png

非正常划款

转帐额大于余额

20200126015617827.png

来看icbc和cmb端的回滚

watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2xpZmV0cmFnZWR5,size_16,color_FFFFFF,t_70
 看到没,rollback被自动触发。数据库端当然也没被插进数据(被回滚掉了)。
watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2xpZmV0cmFnZWR5,size_16,color_FFFFFF,t_70
20200126015839924.png

帐户不存在

watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2xpZmV0cmFnZWR5,size_16,color_FFFFFF,t_70

 

watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2xpZmV0cmFnZWR5,size_16,color_FFFFFF,t_70

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK