【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账

Seata中间件实现一对一转账

  • 1. 转账界面
  • 2. 本地事务在分布式下的问题
    • 2.1. 本地事务
      • 2.1.1. 事务四大特性
      • 2.1.2. 本地事务的概念
      • 2.1.3. 本地事务的实现(使用注解@Transactional)
      • 2.1.4. 事务的隔离级别(数据库SQL规定的一些规范)
      • 2.1.5. 传播行为
      • 2.1.6. 事务的坑(本类方法互调时,本地事务失效问题)
    • 2.2. 分布式事务
      • 2.2.1. 为什么会有分布式事务?
      • 2.2.2. CAP定理
        • 2.2.2.1. 为啥无同时保证 CA 呢?
        • 2.2.2.2. 总结
      • 2.2.3. BASE理论
        • 2.2.3.1. 总结
      • 2.2.4. ACID、CAP和BASE的区别
  • 3. 分布式事务的解决方案(保证不同服务中的数据库的数据一致性)
    • 3.1. 2PC(XA Transactions)
    • 3.2. 柔性事务--TCC事务补偿型方案
    • 3.3. 最大努力通知
    • 3.4. 可靠消息+最终一致性
  • 4. Seata中间件的TCC模式实现一对一转账
    • 4.1. Seata原理
    • 4.2. AT 模式
    • 4.3. 使用Seata的AT模式
      • 4.3.1. 2.registry.conf、file.conf
    • 4.4. 为什么不使用AT模式?
    • 4.5. TCC模式
      • 4.5.1.TCC模式原理
      • 4.5.2. TCC补偿demo
    • 4.1. 背景
    • 4.2. 目的
    • 4.3. 模块流程图
    • 4.9. 扣款TCC事务的实现(TccActionOne)和收款TCC事务的实现(TccActionTwo)
      • 4.9.1. 扣款事务的接口
      • 4.9.2. 扣款事务的接口实现类
      • 4.9.3. 收款事务的接口
      • 4.9.4. 收款事务的接口实现类
      • 4.9.5. 交易模型中TCC解决收付方转账金额的实现
        • 4.9.5.1. controller层
        • 4.9.5.2. service层
      • 4.9.6. 为什么需要分布式事务ID?

1. 转账界面

点击左侧菜单栏的"转账"按钮后,便可以进入转账主界面。在界面中,需要用户填写收款账号和收款账户名以及转账金额等信息。
【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第1张图片
在用户填写完整的信息后,便可以点击"转账"按钮继续操作。
【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第2张图片

点击"转账"按钮后便会弹出输入支付密码的弹窗,用户需要在输入框中填写支付密码后,点击"确定"按钮进行转账操作,如果想取消本次转账可以点击取消按钮进行取消。
【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第3张图片
当用户填写的所有信息都正确,切余额足够时,系统便会弹出转账成功的提示框。
【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第4张图片
如果收款账户不存在,系统会弹出“账户不存在”的弹窗提示。
【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第5张图片
若输入的收款名错误,系统会弹出"转账失败"的弹窗提示。
【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第6张图片
若用户的余额不足,系统会弹出"余额不足"的弹窗提示。
【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第7张图片
若用户的支付密码输入错误,则会弹出"密码错误"的提示框。
【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第8张图片

2. 本地事务在分布式下的问题

  1. 远程服务的假失败,意思其实就是远程服务实际上成功,但是由于网络等原因返回失败,导致本地事务回退。 远程服务却已经操作了数据库;
  2. 第二个问题就是远程服务之后出现问题,导致远程服务请求发出之后是无法回退的。
    【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第9张图片

2.1. 本地事务

2.1.1. 事务四大特性

  1. 原子性:指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。

下订单、减库存、减积分是不可分割的业务逻辑操作,这三个操作要么同时成功,要么同时失败。

  1. 一致性:数据在事务的前后,业务整体一致(事务必须使数据库从一个一致性状态变换到另外一个一致性状态)。
    在这里插入图片描述

转账前和转账后的总金额不变。

  1. 隔离性:事务之间互相隔离。事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。
  2. 持久性:指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。

2.1.2. 本地事务的概念

【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第10张图片
下订单、扣库存、减账户余额这3个操作是同一个事务,要么同时成功要么同时失败。

本地事务指下订单、扣库存、减账户余额这3个操作连接的是同一个数据库,与数据库建立了同一条连接,在该连接发送了3条SQL(下订单、扣库存、减账户),只要有一条SQL执行失败,则3个SQL全部回滚。

2.1.3. 本地事务的实现(使用注解@Transactional)

【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第11张图片
使用注解@Transactional需要注意隔离级别和传播行为。

2.1.4. 事务的隔离级别(数据库SQL规定的一些规范)

参考:
事务的隔离级别有哪些?
https://www.xiaolincoding.com/mysql/transaction/mvcc.html#%E4%BA%8B%E5%8A%A1%E7%9A%84%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB%E6%9C%89%E5%93%AA%E4%BA%9B
  1. Read uncommitted(读未提交):一个事务可以读取另一个未提交事务的数据;
    实现:使用isolation来声明事务隔离级别。
    【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第12张图片

使用读未提交隔离级别时,并发事务时可能会导致脏读、不可重复读、幻读等现象。
脏读举例:数据库中字段price为1000,A修改数据库为1500,此时B查看price为1500,结果A回滚了事务,数据库中数据还是1000。
结果:B出现脏读(B持有的price为1500,实际上应为1000)。

  1. Read committed(读已提交):指一个事务提交之后,它做的变更才能被其他事务看到(一个事务要等另一个事务提交后才能读取数据)。(针对的是UPDATE操作);
    在这里插入图片描述
    实现:使用isolation来声明事务隔离级别。
    【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第13张图片

使用读已提交隔离级别时,并发事务时可能会导致不可重复读、幻读等现象。
举例:数据库中字段price为1000,A查看数据为1000,告诉了C,此时B将数据进行了修改,修改为1600并提交,当C去查看price为1600。
结果:出现了不可重复读现象。

  1. Repeatable read(可重复读):指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,MySQL InnoDB 引擎的默认隔离级别;
    实现:使用isolation来声明事务隔离级别;
    【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第14张图片

使用可重复读隔离级别时,并发事务时可能会导致幻读等现象。
举例:数据库中只有一条记录,当A查看时只有一条,A告诉C,此时B去数据库插入一条数据,C去查看时有两条。(可重复读针对的是同一条数据前后读取同一条数据,得到的结果是一致的;但并不能保证前后读取数据库表的数据记录不变)
结果:出现了幻读现象。

  1. Serializable (串行化):会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行;
    实现:使用isolation来声明事务隔离级别。

使用串行化隔离级别时,可以避免脏读、不可重复读与幻读。

2.1.5. 传播行为

【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第15张图片
想要发生传播就一定要有两个以上的事务,而这里指的是两个方法都要在事务中进行,当一个事务方法A调用另一个事务方法B时,另一个事务方法B该如何运行。
A方法中调用了B、C方法,传播行为指B、C这俩小事务要不要和A共有一个事务。

如,B的传播行为Propagation.REQUIRED,则说明B需要一个事务,此时如果A有事务,就使用A的事务(相当于B和A在同一条连接中执行),若A没有事务,就新建事务。

    @Transactional(timeout = 10)// A事务的所有设置会传播到和他共有一个事务的方法(即B事务的任何设置都是无效,都得遵循A的事务设置),而C的新事务不会受A的影响。
    public void A(){

        // 使用了代理,B、C的事务传播行为才会生效
        bService.B();
        cService.C();
    }
    //    此时如果A有事务,就使用A的事务,A没有事务,就新建事务
    @Transactional(propagation = Propagation.REQUIRED,timeout = 5)
    public void B(){

    }
    //   不和A公用一个事务,新建事务
    @Transactional(propagation = Propagation.REQUIRES_NEW,timeout = 20)
    public void C(){

    }    

2.1.6. 事务的坑(本类方法互调时,本地事务失效问题)

    @Transactional(timeout = 10)
    public void A(){
		// 使用了代理,B、C的事务传播行为才会生效
        // bService.B();
        // cService.C();
        // 直接调用B、C方法,相当于跳过了代理,b,c此时做任何设置都没作用,它们此时都是和A公用一个事务,此时不生效,要使用代理
        B();
        C();
    }
    //    此时如果A有事务,就使用A的事务,A没有事务,就新建事务
    @Transactional(propagation = Propagation.REQUIRED,timeout = 5)
    public void B(){

    }
    //   不和A公用一个事务,新建事务
    @Transactional(propagation = Propagation.REQUIRES_NEW,timeout = 20)
    public void C(){

    }    

同一个对象内事务方法互调默认失效,原因:绕过了代理对象。

在方法上面加上@Transaction,那么在本类中调用的时候实际上就是通过cglib继承代理对象来调用,增强,然后调用原来对象的方法。
如果嵌套其它加上注解的方法,那么这些方法是不起作用的,原因就是第一个被调用的方法是通过代理对象调用的,但是其他方法是直接调用,没有经过事务的增强调用。

本地事务失效问题:
同一个对象内事务方法互调默认失效。,
原因:绕过了代理对象,事务使用代理对象来控制的
解决:使用代理对象来调用事务方法
针对:本类方法互调使用事务

可以通过开启aspectj动态代理注解@EnableAspectJAutoProxy(exposeProxy = true),创建一个代理对象,然后通过代理对象来调用这些方法。
那么就可以解决本地事务失效问题,相当于就是把所有方法增强之后的一个代理对象来调用这些方法。本质上就是通过代理对象来调用。也就是方法已经经过增强了。

  1. 引入spring-boot-starter-aop;spring-boot-starter-aop会自动引入aspectj做动态代理;
    在这里插入图片描述
  2. 开启aspectj动态代理功能
    主启动类使用@EnableAspectJAutoProxy(exposeProxy = true)开启aspectj动态代理功能,以后所有的动态代理都是aspectj创建的(即使没有接口也可以创建动态代理);

exposeProxy = true:对外暴露代理对象
【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第16张图片

  1. 用代理对象本类互调(使用AopContext.currentProxy()获取代理对象)
    即:
 @Transactional
    public void a(){
        OrderServiceImpl o = (OrderServiceImpl) AopContext.currentProxy();
        o.b();
        o.c();
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void b(){
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public void c(){
    }

2.2. 分布式事务

2.2.1. 为什么会有分布式事务?

分布式事务产生的场景
分布式系统会把一个应用系统拆分为可独立部署的多个服务,因此需要服务与服务之间远程协作才能完成事务操作。
这种分布式系统环境下由不同的服务之间通过网络远程协作完成事务称之为分布式事务,例如用户注册送积分事务、创建订单减库存事务,银行转账事务等都是分布式事务。

分布式经常出现的异常:机器宕机,网络异常,消息丢失,消息乱序,数据错误,不可靠的TCP,存储数据丢失。

2.2.2. CAP定理

参考:

差点跪了!阿里3面真题:CAP和BASE理论了解么?可以结合实际案例说下不?
https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247495298&idx=1&sn=965be0f54ab44bda818656db1f21a39f&chksm=cea1a149f9d6285f1169413ab7663ca2a9c1a8440a5ae5816566eb66b20e4d86f5db1002f66c&token=657875872&lang=zh_CN#rd

面试指北
https://www.yuque.com/books/share/04ac99ea-7726-4adb-8e57-bf21e2cc7183/ng9vmg#UzNZp

【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第17张图片

  1. C就是一致性,所有节点访问同一份最新的数据副本,也就是每个服务的数据库的信息都有相同备份;
  2. A就是可用性,非故障的节点在合理的时间内返回合理的响应(不是错误或者超时的响应)。(数据库集群宕机一个,仍然能够使用;)
  3. P就是分区容错性,分布式系统出现网络分区的时候,仍然能够对外提供服务。

什么是网络分区?
分布式系统中,多个节点之前的网络本来是连通的,但是因为某些故障(比如部分节点网络出了问题)某些节点之间不连通了,整个网络就分成了几块区域,这就叫网络分区。
【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第18张图片
当发生网络分区的时候,如果我们要继续服务,那么强一致性和可用性只能 2 选 1。也就是说当网络分区之后 P 是前提,决定了 P 之后才有 C 和 A 的选择。也就是说分区容错性(Partition tolerance)我们是必须要实现的。
简而言之就是:CAP 理论中分区容错性 P 是一定要满足的,在此基础上,只能满足可用性 A 或者一致性 C。

因此,分布式系统理论上不可能选择 CA 架构,只能选择 CP 或者 AP 架构。

2.2.2.1. 为啥无同时保证 CA 呢?

举个例子:若系统出现"分区",系统中的某个节点在进行写操作。为了保证 C, 必须要禁止其他所有集群节点(不是不同分区的节点?)的读写操作,这就和 A 发生冲突了。如果为了保证 A,其他节点的读写操作正常的话,那就和 C 发生冲突了。

如下图,假设3个机器都要保存8号数据。此时,一个请求过来,让这3个备份节点都保存8号数据。

  1. 由于通信故障,节点1和节点3的网线断了(出现了分区错误),而保存数据这个操作只给节点1发送了保存数据请求保存8号数据,节点1让节点2同步了该请求,节点3因为网线中断,无法同步该请求。
  2. 此时假设还满足可用性,即3个节点机器都能正常使用,那么下一个客户端请求由于负载均衡机制,分配到节点3读取8号数据,但由于之前的分区错误,没同步到节点1的8号数据,那么此时,从节点1和节点2中读到的8号数据与节点3中读到的8号数据不一致,即不满足一致性。
  3. 而如果要满足一致性,则必须让节点3不能被访问(只要节点3没同步,系统就不响应8号数据保存成功。)。那么此时又不满足所有节点的可用性了。
    【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第19张图片

选择的关键在于当前的业务场景,没有定论,比如对于需要确保强一致性的场景如银行一般会选择保证CP。

2.2.2.2. 总结

【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第20张图片

2.2.3. BASE理论

BASE 是 Basically Available(基本可用) 、Soft-state(软状态) 和 Eventually Consistent(最终一致性) 三个短语的缩写。BASE 理论是对 CAP 中一致性 C 和可用性 A 权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于 CAP 定理逐步演化而来的,它大大降低了我们对系统的要求。

BASE理论的核心思想
即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。

也就是牺牲数据的强一致性来满足系统的高可用性,系统中一部分数据不可用或者不一致时,仍需要保持系统整体"主要可用"。

BASE 理论本质上是对 CAP 的延伸和补充,更具体地说,是对 CAP 中 AP 方案的一个补充。
【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第21张图片

  1. 基本可用
    基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性。但是,这绝不等价于系统不可用。
    什么叫允许损失部分可用性呢?
  • 响应时间上的损失: 正常情况下,处理用户请求需要 0.5s 返回结果,但是由于系统出现故障,处理用户请求的时间变为 3 s。
  • 系统功能上的损失:正常情况下,用户可以使用系统的全部功能,但是由于系统访问量突然剧增,系统的部分非核心功能无法使用。
  1. 软状态
  • 软状态指允许系统中的数据存在中间状态(CAP 理论中的数据不一致),并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时

如,之前所述的下订单(节点1)、减库存(节点2)、加积分业务(节点3),当减库存事务执行成功,但由于网络问题响应超时,使得下订单事务回滚,但远程服务器中的减库存事务并不会回滚(本地事务无法控制远程事务),那么会出现下订单的服务器中的商品数据与减库存服务中的商品数据不一致问题。
但是BASE理论允许各节点继续保持可用,等过段时间后,系统检查发现刚才减出来的库存没人用,那么会在库存表中将减掉的库存加回来。
最终结果还是会保持节点1和节点2的数据一致性。

  1. 最终一致性
  • 最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
2.2.3.1. 总结
  • 【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第22张图片

2.2.4. ACID、CAP和BASE的区别

ACID 是数据库事务完整性的理论,CAP 是分布式系统设计理论,BASE 是 CAP 理论中 AP 方案的延伸。

3. 分布式事务的解决方案(保证不同服务中的数据库的数据一致性)

参考:
分布式事务解决方案实战
https://blog.csdn.net/huxiang19851114/article/details/114607489?spm=1001.2014.3001.5506

3.1. 2PC(XA Transactions)

【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第23张图片
数据库支持的2PC【二阶段提交】,又被称为XA Transactions。Mysql从5.5开始支持,SQL Server 2005开始支持,Oracle 7开始支持。

其中XA是一个两阶段提交协议:

  1. 准备(prepare)阶段。即所有的参与者准备执行事务并锁住需要的资源。参与者ready时,向transaction manager报告已准备就绪。
  2. 提交阶段(commit)。当transaction manager确认所有参与者都ready后,向所有参与者发送commit命令。

其中如果有任何一个数据库否决此次提交,那么所有数据库都会被要求回滚他们在此事务中的那部分信息。

一句话:意思就是一个大的事务管理器连接多个服务,每次要执行业务时都去问服务准备好了吗,如果准备好那么就开始执行。如果没有那么就不执行。问题就是性能不行,每次都要询问。

举例:
队长:要打BOSS了,各位你们就位了吗?(Prepare)
队员A:我就位了(Ready)
队员B:我就位了(Ready)
队员C:我就位了(Ready)
队员D:我就位了(Ready)
队长:所有人都就位了,兄弟们,丢技能吧!(Commit---------------------------------------------------------------
队长:要打BOSS了,各位你们就位了吗?(Prepare)
队员A:我就位了(Ready)
队员B:我就位了(Ready)
队员C:我就位了(Ready)
队员D:我没蓝....No Ready)
队长:各位兄弟,还有个傻X没准备好,本次团战计划取消,大家继续打野(Cancel

优缺点

  1. XA协议比较简单,商业数据库使用,使用分布式事务的成本低。
  2. XA性能不理想,特别是交易下单链路,往往并发量很高,XA无法满足高并发场景。
  3. XA在商业数据库支持比较理想,在mysql数据库支持不太理想,MYSQL的XA实现没有记录prepare阶段日志,主备切换导致主库和备库数据不一致。

也有3PC,引入超时机制(无论协调者还是参与者,在向对方发送请求后,若长时间未收到回应则做出相应处理)

3.2. 柔性事务–TCC事务补偿型方案

意思就是把业务按照3个接口写入,try,confirm和cancel。先写好业务交给各自的服务数据库try,然后就去confirm是否执行成功,如果没有那么就通过canel来回滚补偿。

3.3. 最大努力通知

两个服务订阅一个消息队列。大模块调用两个服务但是失败了,就把失败信息交给消息队列,两个服务接收到之后就去回滚。而且为了防止服务不知道,它会多次发送消息给消息队列,直到两个服务都知道了。

3.4. 可靠消息+最终一致性

可靠消息+最终一致性与最大努力通知相似。

4. Seata中间件的TCC模式实现一对一转账

【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第24张图片

seata官网教程:http://seata.io/zh-cn/docs/overview/what-is-seata.html

Day433.Seata使用 -谷粒商城
https://blog.csdn.net/qq_43284469/article/details/121025269?ops_request_misc=&request_id=&biz_id=102&utm_term=%E8%B0%B7%E7%B2%92%E5%95%86%E5%9F%8E%20seata&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-0-121025269.142^v50^control_1,201^v3^control_1&spm=1018.2226.3001.4187

谷粒商城(RabbitMq简介、订单服务、分布式事务)思路详解
https://blog.csdn.net/m0_46388866/article/details/120523932

4.1. Seata原理

  1. 首先是TM(事务管理器,全局事务范围)来给TC发送开启一个事务;
  2. 然后TM调用第一个服务(Storage),然后这个服务的RM(资源管理器)向TC注册分支事务,报告自己的事务状态;
  3. 然后TM继续调用下一个服务(Order),下一个服务也是这么做;
  4. 如果下一个服务出现问题(Account),那么TC就会协调其它服务,回滚状态(回滚Storage、Order、Account(Storage、Order已经提交,则会反向补偿,恢复之前的状态)),回滚状态的方法就是通过回滚表。
    【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第25张图片
    【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第26张图片
    【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第27张图片

4.2. AT 模式

at方式的seata分布式事务处理其实就是提交事务,如果发生问题,那么就可以通过undolog来完成回滚。但是这种问题就是需要一个订单一个订单的处理无法处理高并发情况

https://blog.csdn.net/qq_43284469/article/details/121025269?ops_request_misc=&request_id=&biz_id=102&utm_term=%E8%B0%B7%E7%B2%92%E5%95%86%E5%9F%8E%20seata&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-0-121025269.142^v50^control_1,201^v3^control_1&spm=1018.2226.3001.4187

4.3. 使用Seata的AT模式

Day433.Seata使用 -谷粒商城
https://blog.csdn.net/qq_43284469/article/details/121025269?ops_request_misc=&request_id=&biz_id=102&utm_term=%E8%B0%B7%E7%B2%92%E5%95%86%E5%9F%8E%20seata&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-0-121025269.142^v50^control_1,201^v3^control_1&spm=1018.2226.3001.4187

谷粒商城(RabbitMq简介、订单服务、分布式事务)思路详解
https://blog.csdn.net/m0_46388866/article/details/120523932

【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第28张图片

4.3.1. 2.registry.conf、file.conf

【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第29张图片
【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第30张图片

4.4. 为什么不使用AT模式?

https://blog.csdn.net/huxiang19851114/article/details/114607489?spm=1001.2014.3001.5506
https://blog.csdn.net/weixin_44244088/article/details/125417959
https://blog.csdn.net/m0_45406092/article/details/121263208

AT模式最大的问题是运行期锁导致性能低!

举例:两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。

情况1
【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第31张图片

  1. tx1先开始,开启本地事务,拿到本地锁(数据库的锁,保证数据库表中数据的ACID),更新操作m = 1000 - 100 = 900本地事务提交前,需要先拿到该记录的全局锁(可以理解是分布式锁,保证全局事务的提交或回滚) ,然后本地提交并释放本地锁
  2. tx2后开始,开启本地事务,拿到本地锁(tx1释放出来的本地锁),更新操作m = 900 - 100 = 800。本地事务提交前,需要尝试拿该记录的全局锁 ,tx1全局提交前,该记录的全局锁被 tx1持有,tx2需要重试等待全局锁。

情况2
【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第32张图片

  1. 如果tx1的二阶段全局回滚,则tx1需要重新获取该数据的本地锁(此时该本地锁正被tx2持有),进行反向补偿的更新操作,实现分支的回滚;
  2. 此时,如果 tx2 仍在等待该数据的全局锁,同时持有本地锁,则 tx1 的分支回滚会失败(因为该数据的本地锁在tx2那里,tx1没有获得本地锁,就不能进行数据update,恢复之前的数据);
  3. tx1分支的回滚会一直重试,直到 tx2的全局锁等锁超时(等tx1释放出来的锁),放弃等待全局锁,并回滚本地事务释放本地锁;
  4. tx1 的分支获得tx2释放出来的该数据的本地锁,最终回滚成功。

因为整个过程全局锁在tx1结束前一直是被tx1持有的,所以不会发生脏写的问题。(但是可能会有脏读)

总结:这两种情况时,情况1需要等待,情况2陷入死锁并最终都放弃并回滚(啥也没干成),所以不管哪种情况发生,自然会导致性能严重下降。

4.5. TCC模式

https://www.yuque.com/books/share/04ac99ea-7726-4adb-8e57-bf21e2cc7183/ng9vmg#Q3gvA

前面讲过了Seata实现分布式解决方案AT模式,使用非常简单,对业务代码也没有侵入,但是超高并发情况下也发现了其中两个最大的问题:

  1. 全局锁等待,性能不理想
  2. 全局锁和本地锁互斥导致死锁,啥也没干成!同样性能不理想

这也就是AT模式提交最大的痛点!所以我们针对高并发场景推出TCC模式,他的特点是:高性能,但代码侵入!

4.5.1.TCC模式原理

TCC模式也属于二阶段提交,它 将事务提交分为Try - Confirm - Cancel3个操作。其和两阶段提交有点类似,Try为第一阶段,Confirm - Cancel为第二阶段,是一种应用层面侵入业务的两阶段提交。
【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第33张图片
【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第34张图片
其核心在于将业务分为两个操作步骤完成。不依赖 RM (业务模块)对分布式事务的支持,而是通过对业务逻辑的分解来实现分布式事务。

4.5.2. TCC补偿demo

分布式事务(七)Seata TCC模式-Spring Cloud微服务添加 TCC 分布式事务
https://wanght.blog.csdn.net/article/details/107702760

分布式事务(六)Seata TCC模式-TCC模式介绍
https://blog.csdn.net/weixin_38305440/article/details/107692098

4.1. 背景

在分布式银行系统中,由于数据不是存储在同一个数据库进行操作,转账过程中涉及跨数据库的事务,故一对一转账系统中需要解决分布式事务的问题。

4.2. 目的

在跨数据库情况下在复杂的网络环境中保证转账业务的安全。

4.3. 模块流程图

【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第35张图片
【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第36张图片

4.9. 扣款TCC事务的实现(TccActionOne)和收款TCC事务的实现(TccActionTwo)

https://blog.csdn.net/huxiang19851114/article/details/114607489?spm=1001.2014.3001.5506
https://blog.csdn.net/it__ls/article/details/125622729
https://wanght.blog.csdn.net/article/details/107702760
https://www.imooc.com/article/320178

牛客论坛项目的编程式事务:
https://blog.csdn.net/qq_60225495/article/details/122793859?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522166393900316800182176115%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=166393900316800182176115&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~rank_v31_ecpm-1-122793859-null-null.nonecase&utm_term=%E7%BC%96%E7%A8%8B%E5%BC%8F%E4%BA%8B%E5%8A%A1&spm=1018.2226.3001.4450

转账过程中分为两个部分,先发起扣款,后发起收款。
【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第37张图片
【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第38张图片

4.9.1. 扣款事务的接口

Seata 实现 TCC 操作需要定义一个接口,我们在接口中添加以下方法:

  1. Try - prepare()
  2. Confirm - commit()
  3. Cancel - rollback()
package com.icbc.distributed.transfer.action;

import com.icbc.distributed.transfer.entity.AccBalanceTableEntity;
import com.icbc.distributed.transfer.entity.TransferInfoEntity;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;
/**
 * The interface Tcc action one.
 * TCC事务参与者1:扣款
 */
@LocalTCC
public interface TccActionOne {
    /**
     * 第一阶段的方法————准备
     * @TwoPhaseBusinessAction:二阶段TCC补偿提交注解
     * name:TCC参与者名称,你高兴怎么取名都可以;
     * commitMethod:二阶段提交方法,与下面接口名对应(底层就是invoke反射);
     * rollbackMethod:二阶段回滚方法,注意与下面接口名对应;
     * @param actionContext 用于在上下文中获取全局事务ID(用于在两个阶段之间传递数据。)
     * @param uuid 业务唯一编码
     * @param amount 扣除金额
     * @param accBalanceTableEntityFrom 账户余额表
     * @param transferInfoEntity 这是前端表单传过来的
     *
     * 通过@TwoPhaseBusinessAction注解指定第二阶段的两个方法名(commit、rollback)
     * BusinessActionContext 上下文对象,用来在两个阶段之间传递数据。
     * @BusinessActionContextParameter注解的参数数据会被存入BusinessActionContext
     * 原文链接:https://blog.csdn.net/weixin_38305440/article/details/107702760
     */
    @TwoPhaseBusinessAction(name = "TccActionOne" , commitMethod = "commit", rollbackMethod = "rollback")
    public boolean prepare(BusinessActionContext actionContext,
                           @BusinessActionContextParameter(paramName = "transferInfoEntity") TransferInfoEntity transferInfoEntity,
                           @BusinessActionContextParameter(paramName = "uuid") String uuid,
                           @BusinessActionContextParameter(paramName = "amount") Double amount,
                           @BusinessActionContextParameter(paramName = "accBalanceTableEntityFrom") AccBalanceTableEntity accBalanceTableEntityFrom);

    /**
     * Commit boolean.
     * 第二阶段 - 提交
     * @param actionContext the action context
     * @return the boolean
     */
    public boolean commit(BusinessActionContext actionContext);

    /**
     * Rollback boolean.
     * 第二阶段 - 回滚
     * @param actionContext the action context
     * @return the boolean
     */
    public boolean rollback(BusinessActionContext actionContext);
}

【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第39张图片

4.9.2. 扣款事务的接口实现类

package com.icbc.distributed.transfer.action.impl;

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.icbc.distributed.transfer.entity.AccBalanceTableEntity;
import com.icbc.distributed.transfer.entity.TransferInfoEntity;
import com.icbc.distributed.transfer.action.ResultHolder;
import com.icbc.distributed.transfer.action.TccActionOne;
import com.icbc.distributed.transfer.tccservice.SeataTccTransferService;
import com.icbc.distributed.transfer.service.AccAgrtTableService;
import com.icbc.distributed.transfer.service.AccBalanceTableService;
import com.icbc.distributed.transfer.service.AccDetailsTableService;
import io.seata.core.context.RootContext;
import io.seata.rm.tcc.api.BusinessActionContext;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;

import java.math.BigDecimal;
import java.util.HashMap;

/**
 * The type Tcc action one.
 * TCC事务参与者1:扣款
 */
@Service("tccActionOne")
public class TccActionOneImpl implements TccActionOne {

    @Autowired
    SeataTccTransferService seataTccTransferService;

    @Autowired
    private TransactionTemplate fromDsTransactionTemplate;

    @Autowired
    private AccBalanceTableService accBalanceTableService;

    @Autowired
    private AccAgrtTableService accAgrtTableService;

    @Autowired
    private AccDetailsTableService accDetailsTableService;
    /*
     * 一阶段:冻结付方资金
     */
//    @Transactional
    @Override
    public boolean prepare(BusinessActionContext actionContext, TransferInfoEntity transferInfoEntity, String uuid, Double amount, AccBalanceTableEntity accBalanceTableEntityFrom) {
        //分布式事务ID
        String xid = actionContext.getXid();
        System.out.println("TccActionOne prepare, xid:" + xid);
        System.out.println("一阶段");
        // 编程式事务(使用编程式事务,就不需要在prepare、commit、rollback方法前加@Transactional注解了)
        return fromDsTransactionTemplate.execute(new TransactionCallback<Boolean>(){

            @Override
            public Boolean doInTransaction(TransactionStatus status) {
                try {

                    System.out.println("deductbalance ... xid: " + RootContext.getXID());
                    RootContext.bind(RootContext.getXID());// 当前线程绑定的分布式事务ID为xid

                    System.out.println("付方一阶段执行");
                    // 根据用户账户获取账户余额信息
                    QueryWrapper querywrapper = new QueryWrapper<AccBalanceTableEntity>();
                    querywrapper.eq("acc_id",accBalanceTableEntityFrom.getAccId());
                    AccBalanceTableEntity accBalanceTableEntity = accBalanceTableService.getOne(querywrapper);

                    // 不用TCC的话,更新账户余额、更新账户冻结金额的操作应如下代码
                    // Double balance =  accBalanceTableEntityFrom.getCurBalance()-amount;
                    // accBalanceTableEntity.setCurBalance(balance);//更新余额
                    // accBalanceTableEntity.setFreezeBalance(accBalanceTableEntity.getFreezeBalance()+amount);

                    // 1.冻结转账金额(获取原始冻结金额,在此基础上增加新的转账金额)
                    Double frzeeBalance;
                    // 1.1.获取原始冻结金额
                    if(accBalanceTableEntity.getFreezeBalance()==null){
                         // 如果账户没有原始冻结金额,则初始化原始冻结金额frzeeBalance(设置为0)
                         frzeeBalance = Double.valueOf(0);
                    }else {
                        // 如果账户有原始冻结金额,则获取原始冻结金额,赋给frzeeBalance。
                         frzeeBalance = accBalanceTableEntity.getFreezeBalance();
                    }
                    // 1.2.新的冻结金额(保留2位小数)
                    // 冻结资金中增加amount元(amount:转账金额)。账户当前余额中减去amount元
                    Double newFrzeeBalance = Double.valueOf(String.format("%17.2f", frzeeBalance+amount ));
                    // 减去转账金额后,新的账户余额
                    Double newCurBalance = Double.valueOf(String.format("%17.2f", accBalanceTableEntity.getCurBalance()-amount));

                    // 2.更新数据库用户余额表(更新表中的冻结金额、用户余额)
                    //   更新账户交易明细详情表(用户余额更新了,所以账户交易明细详情表AccDetailsTableEntity中的余额也要相应更新)
                    accBalanceTableEntity.setFreezeBalance(newFrzeeBalance);
                    accBalanceTableEntity.setCurBalance(newCurBalance);
                    System.out.printf("现有余额%f,转账%f",accBalanceTableEntity.getCurBalance(),amount);
//                    accBalanceTableEntityFrom.setLastTxnDate(transferInfoEntity.getExecDate());//更新最后交易日
                    accBalanceTableEntity.setRegionId(null);
//                    System.out.println(accBalanceTableEntity);
                    // 更新数据库用户余额表、账户交易明细详情表
                    accBalanceTableService.saveOrUpdate(accBalanceTableEntity);//插表
                    seataTccTransferService.addDetail(transferInfoEntity,uuid,amount,accBalanceTableEntity);
                    if("1900140000000490".equals(accBalanceTableEntity.getAccId())){
                        throw new RuntimeException("付方一阶段失败.");
                    }
//                    throw new RuntimeException("TccActionTwo failed.");
                    return true;
                } catch (Throwable t) {
                    t.printStackTrace();
                    return false;
                }
            }
        });
    }

    /*
    * 二阶段:执行扣款
    */
//    @Transactional
    @Override
    public boolean commit(BusinessActionContext actionContext) {
        //分布式事务ID
        String xid = actionContext.getXid();
        System.out.println("TccActionOne commit, xid:" + xid);
        System.out.println(actionContext.toString());
        //用户余额表accBalanceTableEntityFrom、转账金额amount等数据没有通过方法传参传进来,这里使用BusinessActionContext来获取这些数据。
        //用户余额表对象
        Object object = actionContext.getActionContext("accBalanceTableEntityFrom");
        AccBalanceTableEntity accBalanceTableEntityFrom = JSON.parseObject(object.toString(), AccBalanceTableEntity.class);
        System.out.println(accBalanceTableEntityFrom.toString());
        //转账金额
        BigDecimal bigDecimal = (BigDecimal)actionContext.getActionContext("amount");
        Double amount = bigDecimal.doubleValue();
        ResultHolder.setActionOneResult(xid, "T");

        // 编程式事务
        return fromDsTransactionTemplate.execute(new TransactionCallback<Boolean>() {
//            @Transactional
            @Override
            public Boolean doInTransaction(TransactionStatus status) {
                try{

                    System.out.println("deductbalance ... xid: " + RootContext.getXID());
                    // 在每个微服务中调用方法 RootContext.getXID() 检查XID?(当前线程绑定的分布式事务ID为xid?)
                    RootContext.bind(RootContext.getXID());
                    System.out.println("付方一阶段提交");
                    // 根据账户获取账户余额信息
                    QueryWrapper querywrapper = new QueryWrapper<AccBalanceTableEntity>();
                    querywrapper.eq("acc_id",accBalanceTableEntityFrom.getAccId());
                    AccBalanceTableEntity accBalanceTableEntity = accBalanceTableService.getOne(querywrapper);

                    Double frzeeBalance;
                    // 获取原始冻结金额
                    if(accBalanceTableEntity.getFreezeBalance()==null){
                        // 如果账户没有原始冻结金额,则初始化原始冻结金额frzeeBalance(设置为0)
                        frzeeBalance = Double.valueOf(0);
                    }else {
                        // 如果账户有原始冻结金额,则获取原始冻结金额,赋给frzeeBalance。
                        frzeeBalance = accBalanceTableEntity.getFreezeBalance();
                    }

                    // 不用TCC的话,在账户余额扣除转账金额的操作应如下代码
                    // accBalanceTableEntity.setCurBalance(accBalanceTableEntity.getCurBalance()-amount);

                    // 1.付方用户的冻结资金额减去转账金额(释放付方账户的冻结金额)
                    Double newFrzeeBalance = Double.valueOf(String.format("%17.2f", frzeeBalance-amount ));

                    // 2.更新数据库用户余额表(更新表中的冻结金额、最后交易日)
                    accBalanceTableEntity.setFreezeBalance(newFrzeeBalance);
                    accBalanceTableEntity.setLastTxnDate(accBalanceTableEntityFrom.getLastTxnDate());//更新最后交易日
                    accBalanceTableEntity.setRegionId(null);

                    accBalanceTableService.saveOrUpdate(accBalanceTableEntity);//插表(用户余额表)
                    System.out.println("付方一阶段提交成功");
                    if("3700120000000086".equals(accBalanceTableEntity.getAccId())){
                        throw new RuntimeException("付方二阶段提交失败");
                    }
//                    throw new RuntimeException("TccActionTwo failed.");
                    return true;
                }catch (Throwable t){
                    t.printStackTrace();
                    status.setRollbackOnly();
                    return false;
                }
            }
        });
    }
    /*
     * 二阶段:回滚操作
     */
//    @Transactional
    @Override
    public boolean rollback(BusinessActionContext actionContext) {
        //分布式事务ID
        String xid = actionContext.getXid();
        System.out.println("TccActionOne rollback, xid:" + xid);
        System.out.println("付方一阶段开始回滚");
        System.out.println(actionContext.toString());
//        TransferInfoEntity transferInfoEntity = (TransferInfoEntity) actionContext.getActionContext("transferInfoEntity");
        //用户余额表accBalanceTableEntityFrom、转账金额amount等数据没有通过方法传参传进来,这里使用BusinessActionContext来获取这些数据。
        //业务唯一编码
        String uuid = (String) actionContext.getActionContext("uuid");
        //转账金额
        BigDecimal bigDecimal = (BigDecimal)actionContext.getActionContext("amount");
        Double amount = bigDecimal.doubleValue();
        //用户余额表对象
        Object object = actionContext.getActionContext("accBalanceTableEntityFrom");
        AccBalanceTableEntity accBalanceTableEntityFrom = JSON.parseObject(object.toString(), AccBalanceTableEntity.class);
        System.out.println(accBalanceTableEntityFrom.toString());
        ResultHolder.setActionOneResult(xid, "R");


        return fromDsTransactionTemplate.execute(new TransactionCallback<Boolean>() {

            @Override
            public Boolean doInTransaction(TransactionStatus status) {
                try{

                    System.out.println("deductbalance ... xid: " + RootContext.getXID());
                    RootContext.bind(RootContext.getXID());
//                    HashMap formmap = new HashMap<>();
//                    formmap.put("acc_id",accBalanceTableEntityFrom.getAccId());
//                    accDetailsTableService.listByMap(formmap);
                    // 根据账户获取账户余额信息
                    QueryWrapper querywrapper = new QueryWrapper<AccBalanceTableEntity>();
                    querywrapper.eq("acc_id",accBalanceTableEntityFrom.getAccId());
                    AccBalanceTableEntity accBalanceTableEntity = accBalanceTableService.getOne(querywrapper);

                    //不用TCC的话,在账户余额回复已扣除的转账金额的操作应如下代码
                    // accBalanceTableEntity.setCurBalance(accBalanceTableEntity.getFreezeBalance()+accBalanceTableEntity.getCurBalance());


                    Double frzeeBalance;
                    if(accBalanceTableEntity.getFreezeBalance()==null){
                        frzeeBalance = Double.valueOf(0);
                    }else {
                        frzeeBalance = accBalanceTableEntity.getFreezeBalance();
                    }

                    //1.释放冻结金额(付方用户的冻结资金额减去转账金额)
                    Double newFrzeeBalance = Double.valueOf(String.format("%17.2f", frzeeBalance-amount ));
                    //2.回滚用户余额,恢复用户转账前的用户余额(付方用户的用户余额加上之前扣除的转账金额)
                    Double newCurBalance = Double.valueOf(String.format("%17.2f", accBalanceTableEntity.getCurBalance()+amount ));

                    //3.更新数据库用户余额表(更新表中的冻结金额、用户余额最后交易日)
                    accBalanceTableEntity.setFreezeBalance(newFrzeeBalance);
                    accBalanceTableEntity.setCurBalance(newCurBalance);
                    accBalanceTableEntity.setLastTxnDate(accBalanceTableEntityFrom.getLastTxnDate());//更新最后交易日
                    accBalanceTableEntity.setRegionId(null);
                    System.out.println(accBalanceTableEntity);

                    accBalanceTableService.saveOrUpdate(accBalanceTableEntity);//插表

                    HashMap<String,Object> txnmap = new HashMap<>();
                    txnmap.put("txn_id",uuid);//业务唯一编码
                    // 根据业务唯一编码txnmap去删除因为转账而账户交易明细详情表
                    accDetailsTableService.removeByMap(txnmap);
                    System.out.println("一阶段回滚成功");

//                    if(accBalanceTableEntity.getAccId().equals(4000000200000043L)){
//                        throw new RuntimeException("付方一阶段提交失败");
//                    }
                    return true;
                }catch (Throwable t){
                    t.printStackTrace();
                    status.setRollbackOnly();
                    return false;
                }
            }
        });
    }

}

【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第40张图片

4.9.3. 收款事务的接口

package com.icbc.distributed.transfer.action;

import com.icbc.distributed.transfer.entity.AccBalanceTableEntity;
import com.icbc.distributed.transfer.entity.TransferInfoEntity;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;

/**
 * The interface Tcc action two.
 * TCC事务参与者2:收款
 */
@LocalTCC
public interface TccActionTwo {

    /**
     * Prepare boolean.
     *
     * @param actionContext the action context
     * @param
     * @return the boolean
     */
    @TwoPhaseBusinessAction(name = "TccActionTwo" , commitMethod = "commit", rollbackMethod = "rollback")
    public boolean prepare(BusinessActionContext actionContext,
                           @BusinessActionContextParameter(paramName = "transferInfoEntity") TransferInfoEntity transferInfoEntity,
                           @BusinessActionContextParameter(paramName = "uuid") String uuid,
                           @BusinessActionContextParameter(paramName = "amount") Double amount,
                           @BusinessActionContextParameter(paramName = "accBalanceTableEntityTo") AccBalanceTableEntity accBalanceTableEntityTo);

    /**
     * Commit boolean.
     *
     * @param actionContext the action context
     * @return the boolean
     */
    public boolean commit(BusinessActionContext actionContext);

    /**
     * Rollback boolean.
     *
     * @param actionContext the action context
     * @return the boolean
     */
    public boolean rollback(BusinessActionContext actionContext);

}

【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第41张图片

4.9.4. 收款事务的接口实现类

【分布式金融交易模型】Seata中间件的TCC模式实现一对一转账_第42张图片

package com.icbc.distributed.transfer.action.impl;

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.icbc.distributed.transfer.entity.AccBalanceTableEntity;
import com.icbc.distributed.transfer.entity.TransferInfoEntity;
import com.icbc.distributed.transfer.action.ResultHolder;
import com.icbc.distributed.transfer.action.TccActionTwo;
import com.icbc.distributed.transfer.tccservice.SeataTccTransferService;
import com.icbc.distributed.transfer.service.AccAgrtTableService;
import com.icbc.distributed.transfer.service.AccBalanceTableService;
import com.icbc.distributed.transfer.service.AccDetailsTableService;
import io.seata.core.context.RootContext;
import io.seata.rm.tcc.api.BusinessActionContext;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;

import java.math.BigDecimal;
import java.util.HashMap;

/**
 * The type Tcc action two.
 * TCC事务参与者2:收款
 */
@Service("tccActionTwo")
public class TccActionTwoImpl implements TccActionTwo {

    @Autowired
    SeataTccTransferService seataTccTransferService;
    @Autowired
    private TransactionTemplate toDsTransactionTemplate;


    @Autowired
    private AccBalanceTableService accBalanceTableService;

    @Autowired
    private AccAgrtTableService accAgrtTableService;

    @Autowired
    private AccDetailsTableService accDetailsTableService;


    @Override
    public boolean prepare(BusinessActionContext actionContext, TransferInfoEntity transferInfoEntity, String uuid, Double amount, AccBalanceTableEntity accBalanceTableEntityTo) {
        String xid = actionContext.getXid();
        System.out.println("TccActionTwo prepare, xid:" + xid);


        return toDsTransactionTemplate.execute(new TransactionCallback<Boolean>() {
//            @Transactional
            @Override
            public Boolean doInTransaction(TransactionStatus status) {
                try{

                    System.out.println("addbalance ... xid: " + RootContext.getXID());
                    System.out.println("收方开始执行阶段");
                    RootContext.bind(RootContext.getXID());

                    //业务
//                    accBalanceTableEntityTo.setCurBalance(accBalanceTableEntityTo.getCurBalance()+amount);//更新余额
//                    accBalanceTableEntityTo.setLastTxnDate(transferInfoEntity.getExecDate());//更新最后交易日
                    QueryWrapper querywrapper = new QueryWrapper<AccBalanceTableEntity>();
                    querywrapper.eq("acc_id",accBalanceTableEntityTo.getAccId());
                    AccBalanceTableEntity accBalanceTableEntity = accBalanceTableService.getOne(querywrapper);

                    Double frzeeBalance;
                    if(accBalanceTableEntity.getFreezeBalance()==null){
                        frzeeBalance = Double.valueOf(0);
                    }else {
                        frzeeBalance = accBalanceTableEntity.getFreezeBalance();
                    }

                    //待转入资金作为不可用金额(冻结金额)
                    Double newFrzeeBalance = Double.valueOf(String.format("%17.2f", frzeeBalance+amount ));

                    accBalanceTableEntity.setFreezeBalance(newFrzeeBalance);
                    accBalanceTableEntity.setRegionId(null);
                    // 更新数据库
                    accBalanceTableService.saveOrUpdate(accBalanceTableEntity);//插表
                    seataTccTransferService.addDetail(transferInfoEntity,uuid,amount,accBalanceTableEntityTo);
                    if("2000100000000517".equals(accBalanceTableEntity.getAccId())){
                        throw new RuntimeException("二阶段执行失败");
                    }
//                    System.out.println(String.format("Undo prepareAdd account[%s] amount[%f], dtx transaction id: %s.", accountNo, amount, xid));
                    return true;
                }catch (Throwable t){
                    t.printStackTrace();
                    return false;
                }
            }
        });
    }

    @Override
    public boolean commit(BusinessActionContext actionContext) {
        String xid = actionContext.getXid();
        System.out.println("TccActionTwo commit, xid:" + xid);
        System.out.println(actionContext.toString());
        BigDecimal bigDecimal = (BigDecimal)actionContext.getActionContext("amount");
        Double amount = bigDecimal.doubleValue();
        Object object = actionContext.getActionContext("accBalanceTableEntityTo");
        AccBalanceTableEntity accBalanceTableEntityTo = JSON.parseObject(object.toString(), AccBalanceTableEntity.class);
        ResultHolder.setActionTwoResult(xid, "T");

        return toDsTransactionTemplate.execute(new TransactionCallback<Boolean>() {
//            @Transactional
            @Override
            public Boolean doInTransaction(TransactionStatus status) {
                try{

                    RootContext.bind(actionContext.getXid());//RootContext.getXID()
                    System.out.println("deductbalance ... xid: " + RootContext.getXID());
                    System.out.println("收方二阶段提交");

                    QueryWrapper querywrapper = new QueryWrapper<AccBalanceTableEntity>();
                    querywrapper.eq("acc_id",accBalanceTableEntityTo.getAccId());
                    AccBalanceTableEntity accBalanceTableEntity = accBalanceTableService.getOne(querywrapper);

                    Double frzeeBalance;
                    if(accBalanceTableEntity.getFreezeBalance()==null){
                        frzeeBalance = Double.valueOf(0);
                    }else {
                        frzeeBalance = accBalanceTableEntity.getFreezeBalance();
                    }
                    //释放收方账户中的冻结金额
                    Double newFrzeeBalance = Double.valueOf(String.format("%17.2f", frzeeBalance - amount ));
                    //收方账户加钱
                    Double newCurBalance = Double.valueOf(String.format("%17.2f", accBalanceTableEntity.getCurBalance() + amount ));

                    // 更新用户余额表
                    accBalanceTableEntity.setCurBalance(newCurBalance);//更新余额
                    accBalanceTableEntity.setFreezeBalance(newFrzeeBalance);//更新余额
                    accBalanceTableEntity.setLastTxnDate(accBalanceTableEntityTo.getLastTxnDate());//更新最后交易日
                    accBalanceTableEntity.setRegionId(null);
                    accBalanceTableService.saveOrUpdate(accBalanceTableEntity);//插表
//                    if("52001400000005300".equals(accBalanceTableEntity.getAccId())){
//                        throw new RuntimeException("二阶段提交失败");
//                    }
//                    throw new RuntimeException("TccActionTwo failed.");
                    return true;
                }catch (Throwable t){
                    t.printStackTrace();
                    status.setRollbackOnly();
                    return false;
                }
            }
        });
    }

    @Override
    public boolean rollback(BusinessActionContext actionContext) {
        String xid = actionContext.getXid();
        System.out.println("TccActionTwo rollback, xid:" + xid);
        ResultHolder.setActionTwoResult(xid, "R");

        System.out.println("收方二阶段开始回滚");
        System.out.println(actionContext.toString());
//        TransferInfoEntity transferInfoEntity = (TransferInfoEntity) actionContext.getActionContext("transferInfoEntity");
        String uuid = (String) actionContext.getActionContext("uuid");
        System.out.println(uuid);
        BigDecimal bigDecimal = (BigDecimal)actionContext.getActionContext("amount");
        Double amount = bigDecimal.doubleValue();
        Object object = actionContext.getActionContext("accBalanceTableEntityTo");
        AccBalanceTableEntity accBalanceTableEntityTo = JSON.parseObject(object.toString(), AccBalanceTableEntity.class);

        System.out.println(accBalanceTableEntityTo.toString());

        return  toDsTransactionTemplate.execute(new TransactionCallback<Boolean>() {

            @Override
            public Boolean doInTransaction(TransactionStatus status) {
                try{
                    System.out.println("deductbalance ... xid: " + RootContext.getXID());
                    RootContext.bind(RootContext.getXID());

                    QueryWrapper querywrapper = new QueryWrapper<AccBalanceTableEntity>();
                    querywrapper.eq("acc_id",accBalanceTableEntityTo.getAccId());
                    AccBalanceTableEntity accBalanceTableEntity = accBalanceTableService.getOne(querywrapper);
                    Double frzeeBalance;
                    if(accBalanceTableEntity.getFreezeBalance()==null){
                        frzeeBalance = Double.valueOf(0);
                    }else {
                        frzeeBalance = accBalanceTableEntity.getFreezeBalance();
                    }
                    //冻结金额清除
                    Double newFrzeeBalance = Double.valueOf(String.format("%17.2f", frzeeBalance-amount ));
                    // 更新数据库用户余额表
                    accBalanceTableEntity.setFreezeBalance(newFrzeeBalance);
                    accBalanceTableEntity.setLastTxnDate(accBalanceTableEntityTo.getLastTxnDate());//更新最后交易日
                    accBalanceTableEntity.setRegionId(null);
                    System.out.println(accBalanceTableEntity);
                    accBalanceTableService.saveOrUpdate(accBalanceTableEntity);//插表

                    HashMap<String,Object> txnmap = new HashMap<>();
                    txnmap.put("txn_id",uuid);
                    // 删除账户交易明细详情表
                    accDetailsTableService.removeByMap(txnmap);
                    System.out.println("二阶段回滚成功");
                    return true;
                }catch (Throwable t){
                    t.printStackTrace();
                    status.setRollbackOnly();
                    return false;
                }
            }
        });
    }
}

4.9.5. 交易模型中TCC解决收付方转账金额的实现

4.9.5.1. controller层
package com.icbc.distributed.transfer.controller;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.icbc.common.utils.ParseJwt;
import com.icbc.common.utils.R;
import com.icbc.distributed.transfer.entity.AccBalanceTableEntity;
import com.icbc.distributed.transfer.entity.AccInfoTableEntity;
import com.icbc.distributed.transfer.entity.TransferInfoEntity;
import com.icbc.distributed.transfer.tccservice.TccTransactionService;
import com.icbc.distributed.transfer.service.*;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.*;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


@RefreshScope
@RestController
@RequestMapping("/transfer")
public class TransferController {

    @Autowired
    TccTransactionService transferService;


    @Autowired
    private AccBalanceTableService accBalanceTableService;


    @Autowired
    private AccDetailsTableService accDetailsTableService;

    @Autowired
    private BizRegTableService bizRegTableService;


    @Autowired
    private AccInfoTableService accInfoTableService;

//    @RequestBody TransferInfoEntity transferInfoEntity
    // 1.收付方账户信息校验
    @RequestMapping(value = "/check", method = RequestMethod.POST)
    @ResponseBody
    public R checkAcc(@RequestBody TransferInfoEntity transferInfoEntity) throws Exception {

        System.out.println("账号检查");
        System.out.println(transferInfoEntity);

        try {

        R ret = transferService.checkAcc(transferInfoEntity);
            return ret;
        }catch (Throwable t){

            t.printStackTrace();
            return R.error("账号异常");
        }


    }
    // 2.支付密码校验
    @RequestMapping(value = "/transfer", method = RequestMethod.POST)
    @ResponseBody
    public R CheckPayPassword(@RequestBody TransferInfoEntity transferInfoEntity,@RequestHeader("token") String token){

        try{
            // 2.1.token校验(校验通过后,确定是登录用户所在的浏览器在操作,才会进行后续的转账业务)
            ParseJwt.parseJWT(token);
        }catch (Exception e){
            return R.error(500,"wrong token");
        }

        String accId = transferInfoEntity.getAccIdFrom();
        // 将前端表单传过来的密码进行加密(数据库中保存的就是加密后的密码,只有同为加密的密码,才能进行比较)
        String paypassword= DigestUtils.md5Hex(transferInfoEntity.getPassword() + transferInfoEntity.getAccIdFrom());

        // 根据账户ID查询账户信息
        AccInfoTableEntity accInfoTableEntity = accInfoTableService.getOne(new QueryWrapper<AccInfoTableEntity>().eq("acc_id",accId));
        if(accInfoTableEntity == null){
            return R.error("账号不存在");
        }
        System.out.println(paypassword+"\n");
        System.out.println(accInfoTableEntity.getPayPassword());
        // 2.2.判断前端传过来的密码和数据库中保存的密码是否一致,一致的话才开始TCC转账事务
        if(paypassword.equals(accInfoTableEntity.getPayPassword())){
            //3.开始转账
            try {
                R ret = transferService.doTransactionCommit(transferInfoEntity);
                return ret;
            }catch (Throwable t){
                t.printStackTrace();
                return R.error("转账异常");
            }


        }else {
            return R.error("密码错误");
        }

    }


    // 4.转账结束后更新账户余额
    @PostMapping("/accId/{accId}")
    public Collection<AccBalanceTableEntity> getListMap(@PathVariable("accId") Long accId) {
        Map<String, Object> map = new HashMap<>();
        //kay是字段名 value是字段值
        map.put("acc_id", accId);

        List<AccBalanceTableEntity> accBalanceEntityList = accBalanceTableService.listByMap(map);
        AccBalanceTableEntity accBalanceTableEntity = accBalanceEntityList.get(0);
        accBalanceTableEntity.setCurBalance(100 + accBalanceTableEntity.getCurBalance());
        accBalanceTableService.saveOrUpdate(accBalanceTableEntity);
        return accBalanceEntityList;
    }

}
4.9.5.2. service层

其中,收付方账户信息校验(checkAcc)具体为:

    public R checkAcc(TransferInfoEntity transferInfoEntity){

        Map<String, Object> map = new HashMap<>();
        //kay是字段名 value是字段值
        map.put("acc_id", transferInfoEntity.getAccIdFrom());
        //1数据初始化和检查(获取前端转账表单填写的转账金额)
        Double amount = transferInfoEntity.getAmount();

        // 1.1.核对付方的账户ID和名字,判断付方账号是否存在
        // 将数据库中的付方账号ID和前端表单输入的付方ID进行核对(拿前端输入的付款账号去数据库表中查询该账号是否存在于数据库表中)
        List<AccInfoTableEntity> accInfoTableEntityFromList = accInfoTableService.listByMap(map);
        if(accInfoTableEntityFromList.size()<1){

            return R.error("账号错误, 账号不存在");
        }

        // 判断账号是否异常
        // 1.2.将数据库中的付方账号名字和前端表单输入的付方户名进行核对
        AccInfoTableEntity accInfoTableEntityFrom = accInfoTableEntityFromList.get(0);
        if(!accInfoTableEntityFrom.getAccTitle().equals(transferInfoEntity.getAccTitleFrom())){
            return  R.error("付方账号与用户名对不上");
        }

        if(accInfoTableEntityFrom.getAccStatus()==-1){
            return  R.error("账号错误, 账号存在异常");
        }

        // 1.3.核对收方的账号ID和名字
        // 将数据库中的收方账号ID和前端表单输入的收方ID进行核对
        map.put("acc_id", transferInfoEntity.getAccIdTo());
        List<AccInfoTableEntity> accInfoTableEntityToList = accInfoTableService.listByMap(map);

        if(accInfoTableEntityToList.size()<1){
            return  R.error("信息错误, 付方账号不存在");
        }


        //判断收方账号是否存在异常
        //1.4.将数据库中的收方账号名字和前端表单输入的收方户名进行核对
        AccInfoTableEntity accInfoTableEntityTo = accInfoTableEntityToList.get(0);
        if(!accInfoTableEntityTo.getAccTitle().equals(transferInfoEntity.getAccTitleTo())){
            return  R.error("收方账号与用户名对不上");
        }

        if(accInfoTableEntityTo.getAccStatus()==-1){
            return  R.error("账号错误,付方账号存在异常");
        }

        //1.5.查询付方账户余额信息是否被正常记录
        map.put("acc_id", transferInfoEntity.getAccIdFrom());
        List<AccBalanceTableEntity> accBalanceEntityListFrom = accBalanceTableService.listByMap(map);

        if(accBalanceEntityListFrom.size()==0){
            return  R.error("账户余额信息缺失");
        }else if(accBalanceEntityListFrom.size()>1){
            return  R.error("余额信息重复");
        }
        //1.6.查询收方账户余额信息是否被正常记录
        map.put("acc_id", transferInfoEntity.getAccIdTo());
        List<AccBalanceTableEntity> accBalanceEntityListTo = accBalanceTableService.listByMap(map);

        if(accBalanceEntityListTo.size()==0){
            return  R.error("收方账户余额信息缺失");
        }else if(accBalanceEntityListTo.size()>1){
            return  R.error("收方余额重复");
        }
        //1.7.查询付方余额是否充足
        AccBalanceTableEntity accBalanceTableEntityFrom = accBalanceEntityListFrom.get(0);
        if (accBalanceTableEntityFrom.getCurBalance() < amount || accBalanceTableEntityFrom.getCurBalance()<0) {
            return  R.error("余额不足");
        }

        return  R.ok("请输入支付密码");
    }

一对一转账(doTransactionCommit)具体为:

    /**
     * 发起转账事务
     *
     * @return string string
     *
     */
    @GlobalTransactional
    public R doTransactionCommit(TransferInfoEntity transferInfoEntity){


        Map<String, Object> map = new HashMap<>();
        //kay是字段名 value是字段值
        map.put("acc_id", transferInfoEntity.getAccIdFrom());
        //3.1.数据初始化和检查(获取转账金额)
        Double amount = transferInfoEntity.getAmount();

        //3.2.业务防重控制
        String uuid = "";
        while (true){
            uuid = UUID.randomUUID().toString().substring(0,30);
            Map<String, Object> bizmap = new HashMap<>();
            bizmap.put("txn_id",uuid); //txn_id:业务唯一编号
            List<BizRegTableEntity> bizRegTableEntityList = bizRegTableService.listByMap(bizmap);
            System.out.printf("业务编号的个数为:%d\n",bizRegTableEntityList.size());
            System.out.println(bizRegTableEntityList.toString());
            if(bizRegTableEntityList.size()>0){
                System.out.println( "业务编号重复");
            }else if(bizRegTableEntityList.size() ==0){
                break;
            }
        }

        //3.3.新增业务登记簿(将前端表单输入的信息新增到数据库的业务登记表)
        BizRegTableEntity bizRegTableEntity = new BizRegTableEntity();
        bizRegTableEntity.setSnId(idWorker.nextId());//记录序号
        bizRegTableEntity.setTxnDate(transferInfoEntity.getExecDate());//交易日期
        bizRegTableEntity.setTxnId(uuid);//业务唯一编码
        bizRegTableEntity.setChannelType(1);//渠道种类
        bizRegTableEntity.setExecOrganno(transferInfoEntity.getExecOrganno());//操作机构
        bizRegTableEntity.setExecTellerno(transferInfoEntity.getExecTellerno());//操作柜员
        bizRegTableEntity.setTxnCode(111);//交易代码
        bizRegTableEntity.setTxnType(111);//交易类型
        bizRegTableEntity.setCashTransferFlag(1);//现金转账标志
        bizRegTableEntity.setDebitAccId(String.valueOf(transferInfoEntity.getAccIdFrom()));//借方账号(发起账号)
        bizRegTableEntity.setDebitAccName(transferInfoEntity.getAccTitleFrom());//借方名称
        bizRegTableEntity.setDebitCurrType(1);//借方币种 transferInfoEntity.getCurrType()
        bizRegTableEntity.setDebitAmount(amount);//借方发生额(转账金额)
        bizRegTableEntity.setCreditAccId(String.valueOf(transferInfoEntity.getAccIdTo()));//贷方账号
        bizRegTableEntity.setCreditAccName(transferInfoEntity.getAccTitleTo());//贷方币种
        bizRegTableEntity.setCreditCurrType(1);//贷方币种  transferInfoEntity.getCurrType()
        bizRegTableEntity.setCreditAmount(transferInfoEntity.getAmount());//贷方发生额
        bizRegTableEntity.setStatus(1);//状态1-处理中2-交易
        bizRegTableEntity.setLastModifyDate(transferInfoEntity.getExecDate());//最后更新日期
        bizRegTableEntity.setRegionId(1);//地区编号
        System.out.println(bizRegTableEntity.toString());
        bizRegTableService.save(bizRegTableEntity);

        //3.4.查询账户信息表
        //3.4.1.判断付方账号ID是否存在
        List<AccInfoTableEntity> accInfoTableEntityFromList = accInfoTableService.listByMap(map);
        if(accInfoTableEntityFromList.size()<1){
            return R.error("账号错误, 账号不存在");
        }

        //3.4.2.判断付方账号名字是否异常
        AccInfoTableEntity accInfoTableEntityFrom = accInfoTableEntityFromList.get(0);
        if(!accInfoTableEntityFrom.getAccTitle().equals(transferInfoEntity.getAccTitleFrom())){
            return R.error("付方账号与用户名对不上");
        }

        if(accInfoTableEntityFrom.getAccStatus()==-1){
            return  R.error("账号错误, 账号存在异常");
        }

        //3.4.3.判断收方账号ID是否存在
        map.put("acc_id", transferInfoEntity.getAccIdTo());
        List<AccInfoTableEntity> accInfoTableEntityToList = accInfoTableService.listByMap(map);

        if(accInfoTableEntityToList.size()<1){
            return  R.error("信息错误, 付方账号不存在");
        }


        //3.4.4.判断收方账号名字是否存在异常
        AccInfoTableEntity accInfoTableEntityTo = accInfoTableEntityToList.get(0);
        if(!accInfoTableEntityTo.getAccTitle().equals(transferInfoEntity.getAccTitleTo())){
            return  R.error("收方账号与用户名对不上");
        }

        if(accInfoTableEntityTo.getAccStatus()==-1){
            return  R.error("账号错误,付方账号存在异常");
        }

        //3.5.查询账户余额表
        //3.5.1.判断付方账户余额信息是否正常
        map.put("acc_id", transferInfoEntity.getAccIdFrom());
        List<AccBalanceTableEntity> accBalanceEntityListFrom = accBalanceTableService.listByMap(map);

        if(accBalanceEntityListFrom.size()==0){
            return  R.error("账户余额信息缺失");
        }else if(accBalanceEntityListFrom.size()>1){
            return  R.error("余额信息重复");
        }

        //3.5.2.判断付方账户余额信息是否正常
        map.put("acc_id", transferInfoEntity.getAccIdTo());
        List<AccBalanceTableEntity> accBalanceEntityListTo = accBalanceTableService.listByMap(map);

        if(accBalanceEntityListTo.size()==0){
            return  R.error("收方账户余额信息缺失");
        }else if(accBalanceEntityListTo.size()>1){
            return  R.error("收方余额重复");
        }
        //3.6.判断付方余额是否充足
        AccBalanceTableEntity accBalanceTableEntityFrom = accBalanceEntityListFrom.get(0);
        if (accBalanceTableEntityFrom.getCurBalance() < amount || accBalanceTableEntityFrom.getCurBalance()<0) {
            return  R.error("余额不足");
        }


        boolean result = false;
        //3.7. 扣款事务(tccActionOne.prepare方法具体请看4.9节)
        //第一个TCC 事务参与者
        //accBalanceTableEntityFrom:付方账户余额
        //amount:前端输入的转账金额
        result = tccActionOne.prepare(null, transferInfoEntity,uuid,amount,accBalanceTableEntityFrom);
        if(!result){
            throw new RuntimeException("付方扣款失败.");
        }

        //3.8.收款事务(tccActionTwo.prepare方法具体请看4.9节)
        //第二个事务参与者
        //收方增加余额
        //增加收方明细
        System.out.println("开始收款账");
        AccBalanceTableEntity accBalanceTableEntityTo = accBalanceEntityListTo.get(0);

        result = tccActionTwo.prepare(null, transferInfoEntity,uuid,amount,accBalanceTableEntityTo);
        if(!result){
            throw new RuntimeException("收方收款失败.");
        }


        return  R.ok("转账正在进行中");
    }
}

完整代码

package com.icbc.distributed.transfer.tccservice;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.icbc.common.utils.R;
import com.icbc.distributed.common.IdWorker;
import com.icbc.distributed.transfer.entity.AccBalanceTableEntity;
import com.icbc.distributed.transfer.entity.AccInfoTableEntity;
import com.icbc.distributed.transfer.entity.BizRegTableEntity;
import com.icbc.distributed.transfer.entity.TransferInfoEntity;
import com.icbc.distributed.transfer.action.TccActionOne;
import com.icbc.distributed.transfer.action.TccActionTwo;
import com.icbc.distributed.transfer.service.*;



import io.seata.spring.annotation.GlobalTransactional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/**
 * The type Tcc transaction service.
 *
 * @author xuzexi
 */

@Service("tccTransaction")
public class TccTransactionService {

    @Autowired
    private TccActionOne tccActionOne;


    @Autowired
    private TccActionTwo tccActionTwo;

    @Autowired
    private AccBalanceTableService accBalanceTableService;

    @Autowired
    private AccAgrtTableService accAgrtTableService;

    @Autowired
    private AccDetailsTableService accDetailsTableService;

    @Autowired
    private BizRegTableService bizRegTableService;


    @Autowired
    private AccInfoTableService accInfoTableService;

    @Autowired
    IdWorker idWorker;// 分布式自增长ID


//    @GlobalTransactional
//    public String tansfer(TransferInfoEntity transferInfoEntity){
//        String ret = "转账失败";
//        try {
//            ret = doTransactionCommit(transferInfoEntity);
//        }catch (Throwable t){
//            t.printStackTrace();
//            return  "转账失败";
//        }
//        return ret;
//    }


    public R checkAcc(TransferInfoEntity transferInfoEntity){

        Map<String, Object> map = new HashMap<>();
        //kay是字段名 value是字段值
        map.put("acc_id", transferInfoEntity.getAccIdFrom());
        //1数据初始化和检查(获取前端转账表单填写的转账金额)
        Double amount = transferInfoEntity.getAmount();

        // 1.1.核对付方的账户ID和名字,判断付方账号是否存在
        // 将数据库中的付方账号ID和前端表单输入的付方ID进行核对(拿前端输入的付款账号去数据库表中查询该账号是否存在于数据库表中)
        List<AccInfoTableEntity> accInfoTableEntityFromList = accInfoTableService.listByMap(map);
        if(accInfoTableEntityFromList.size()<1){

            return R.error("账号错误, 账号不存在");
        }

        // 判断账号是否异常
        // 1.2.将数据库中的付方账号名字和前端表单输入的付方户名进行核对
        AccInfoTableEntity accInfoTableEntityFrom = accInfoTableEntityFromList.get(0);
        if(!accInfoTableEntityFrom.getAccTitle().equals(transferInfoEntity.getAccTitleFrom())){
            return  R.error("付方账号与用户名对不上");
        }

        if(accInfoTableEntityFrom.getAccStatus()==-1){
            return  R.error("账号错误, 账号存在异常");
        }

        // 1.3.核对收方的账号ID和名字
        // 将数据库中的收方账号ID和前端表单输入的收方ID进行核对
        map.put("acc_id", transferInfoEntity.getAccIdTo());
        List<AccInfoTableEntity> accInfoTableEntityToList = accInfoTableService.listByMap(map);

        if(accInfoTableEntityToList.size()<1){
            return  R.error("信息错误, 付方账号不存在");
        }


        //判断收方账号是否存在异常
        //1.4.将数据库中的收方账号名字和前端表单输入的收方户名进行核对
        AccInfoTableEntity accInfoTableEntityTo = accInfoTableEntityToList.get(0);
        if(!accInfoTableEntityTo.getAccTitle().equals(transferInfoEntity.getAccTitleTo())){
            return  R.error("收方账号与用户名对不上");
        }

        if(accInfoTableEntityTo.getAccStatus()==-1){
            return  R.error("账号错误,付方账号存在异常");
        }

        //1.5.查询付方账户余额信息是否被正常记录
        map.put("acc_id", transferInfoEntity.getAccIdFrom());
        List<AccBalanceTableEntity> accBalanceEntityListFrom = accBalanceTableService.listByMap(map);

        if(accBalanceEntityListFrom.size()==0){
            return  R.error("账户余额信息缺失");
        }else if(accBalanceEntityListFrom.size()>1){
            return  R.error("余额信息重复");
        }
        //1.6.查询收方账户余额信息是否被正常记录
        map.put("acc_id", transferInfoEntity.getAccIdTo());
        List<AccBalanceTableEntity> accBalanceEntityListTo = accBalanceTableService.listByMap(map);

        if(accBalanceEntityListTo.size()==0){
            return  R.error("收方账户余额信息缺失");
        }else if(accBalanceEntityListTo.size()>1){
            return  R.error("收方余额重复");
        }
        //1.7.查询付方余额是否充足
        AccBalanceTableEntity accBalanceTableEntityFrom = accBalanceEntityListFrom.get(0);
        if (accBalanceTableEntityFrom.getCurBalance() < amount || accBalanceTableEntityFrom.getCurBalance()<0) {
            return  R.error("余额不足");
        }

        return  R.ok("请输入支付密码");
    }
    /**
     * 发起转账事务
     *
     * @return string string
     *
     */
    @GlobalTransactional
    public R doTransactionCommit(TransferInfoEntity transferInfoEntity){


        Map<String, Object> map = new HashMap<>();
        //kay是字段名 value是字段值
        map.put("acc_id", transferInfoEntity.getAccIdFrom());
        //3.1.数据初始化和检查(获取转账金额)
        Double amount = transferInfoEntity.getAmount();

        //3.2.业务防重控制
        String uuid = "";
        while (true){
            uuid = UUID.randomUUID().toString().substring(0,30);
            Map<String, Object> bizmap = new HashMap<>();
            bizmap.put("txn_id",uuid); //txn_id:业务唯一编号
            List<BizRegTableEntity> bizRegTableEntityList = bizRegTableService.listByMap(bizmap);
            System.out.printf("业务编号的个数为:%d\n",bizRegTableEntityList.size());
            System.out.println(bizRegTableEntityList.toString());
            if(bizRegTableEntityList.size()>0){
                System.out.println( "业务编号重复");
            }else if(bizRegTableEntityList.size() ==0){
                break;
            }
        }

        //3.3.新增业务登记簿(将前端表单输入的信息新增到数据库的业务登记表)
        BizRegTableEntity bizRegTableEntity = new BizRegTableEntity();
        bizRegTableEntity.setSnId(idWorker.nextId());//记录序号
        bizRegTableEntity.setTxnDate(transferInfoEntity.getExecDate());//交易日期
        bizRegTableEntity.setTxnId(uuid);//业务唯一编码
        bizRegTableEntity.setChannelType(1);//渠道种类
        bizRegTableEntity.setExecOrganno(transferInfoEntity.getExecOrganno());//操作机构
        bizRegTableEntity.setExecTellerno(transferInfoEntity.getExecTellerno());//操作柜员
        bizRegTableEntity.setTxnCode(111);//交易代码
        bizRegTableEntity.setTxnType(111);//交易类型
        bizRegTableEntity.setCashTransferFlag(1);//现金转账标志
        bizRegTableEntity.setDebitAccId(String.valueOf(transferInfoEntity.getAccIdFrom()));//借方账号(发起账号)
        bizRegTableEntity.setDebitAccName(transferInfoEntity.getAccTitleFrom());//借方名称
        bizRegTableEntity.setDebitCurrType(1);//借方币种 transferInfoEntity.getCurrType()
        bizRegTableEntity.setDebitAmount(amount);//借方发生额(转账金额)
        bizRegTableEntity.setCreditAccId(String.valueOf(transferInfoEntity.getAccIdTo()));//贷方账号
        bizRegTableEntity.setCreditAccName(transferInfoEntity.getAccTitleTo());//贷方币种
        bizRegTableEntity.setCreditCurrType(1);//贷方币种  transferInfoEntity.getCurrType()
        bizRegTableEntity.setCreditAmount(transferInfoEntity.getAmount());//贷方发生额
        bizRegTableEntity.setStatus(1);//状态1-处理中2-交易
        bizRegTableEntity.setLastModifyDate(transferInfoEntity.getExecDate());//最后更新日期
        bizRegTableEntity.setRegionId(1);//地区编号
        System.out.println(bizRegTableEntity.toString());
        bizRegTableService.save(bizRegTableEntity);

        //3.4.查询账户信息表
        //3.4.1.判断付方账号ID是否存在
        List<AccInfoTableEntity> accInfoTableEntityFromList = accInfoTableService.listByMap(map);
        if(accInfoTableEntityFromList.size()<1){
            return R.error("账号错误, 账号不存在");
        }

        //3.4.2.判断付方账号名字是否异常
        AccInfoTableEntity accInfoTableEntityFrom = accInfoTableEntityFromList.get(0);
        if(!accInfoTableEntityFrom.getAccTitle().equals(transferInfoEntity.getAccTitleFrom())){
            return R.error("付方账号与用户名对不上");
        }

        if(accInfoTableEntityFrom.getAccStatus()==-1){
            return  R.error("账号错误, 账号存在异常");
        }

        //3.4.3.判断收方账号ID是否存在
        map.put("acc_id", transferInfoEntity.getAccIdTo());
        List<AccInfoTableEntity> accInfoTableEntityToList = accInfoTableService.listByMap(map);

        if(accInfoTableEntityToList.size()<1){
            return  R.error("信息错误, 付方账号不存在");
        }


        //3.4.4.判断收方账号名字是否存在异常
        AccInfoTableEntity accInfoTableEntityTo = accInfoTableEntityToList.get(0);
        if(!accInfoTableEntityTo.getAccTitle().equals(transferInfoEntity.getAccTitleTo())){
            return  R.error("收方账号与用户名对不上");
        }

        if(accInfoTableEntityTo.getAccStatus()==-1){
            return  R.error("账号错误,付方账号存在异常");
        }

        //3.5.查询账户余额表
        //3.5.1.判断付方账户余额信息是否正常
        map.put("acc_id", transferInfoEntity.getAccIdFrom());
        List<AccBalanceTableEntity> accBalanceEntityListFrom = accBalanceTableService.listByMap(map);

        if(accBalanceEntityListFrom.size()==0){
            return  R.error("账户余额信息缺失");
        }else if(accBalanceEntityListFrom.size()>1){
            return  R.error("余额信息重复");
        }

        //3.5.2.判断付方账户余额信息是否正常
        map.put("acc_id", transferInfoEntity.getAccIdTo());
        List<AccBalanceTableEntity> accBalanceEntityListTo = accBalanceTableService.listByMap(map);

        if(accBalanceEntityListTo.size()==0){
            return  R.error("收方账户余额信息缺失");
        }else if(accBalanceEntityListTo.size()>1){
            return  R.error("收方余额重复");
        }
        //3.6.判断付方余额是否充足
        AccBalanceTableEntity accBalanceTableEntityFrom = accBalanceEntityListFrom.get(0);
        if (accBalanceTableEntityFrom.getCurBalance() < amount || accBalanceTableEntityFrom.getCurBalance()<0) {
            return  R.error("余额不足");
        }

        boolean result = false;
        //3.7. 扣款事务
        //第一个TCC 事务参与者
        //accBalanceTableEntityFrom:付方账户余额
        //amount:前端输入的转账金额
        result = tccActionOne.prepare(null, transferInfoEntity,uuid,amount,accBalanceTableEntityFrom);
        if(!result){
            throw new RuntimeException("付方扣款失败.");
        }

        //3.8.收款事务
        //第二个事务参与者
        //收方增加余额
        //增加收方明细
        System.out.println("开始收款账");
        AccBalanceTableEntity accBalanceTableEntityTo = accBalanceEntityListTo.get(0);

        result = tccActionTwo.prepare(null, transferInfoEntity,uuid,amount,accBalanceTableEntityTo);
        if(!result){
            throw new RuntimeException("收方收款失败.");
        }
        return  R.ok("转账正在进行中");
 }
}

4.9.6. 为什么需要分布式事务ID?

分布式事务XID是如何将所有微服务串联起来的?
https://juejin.cn/post/6894516787481608205

你可能感兴趣的:(分布式金融交易模型,分布式,中间件)