分库分表-分布式事务理论和方案

一.分布式事务理论

1.1.基础概念

1.1.1.什么是事务什么是事务?

举个生活中的例子:你去小卖铺买东西,“一手交钱,一手交货”就是一个事务的例子,交钱和交货必须全部成功,事务才算成功,任一个活动失败,事务将撤销所有已成功的活动。
明白上述例子,再来看事务的定义:事务可以看做是一次大的活动,它由不同的小活动组成,这些活动要么全部成功,要么全部失败。

1.1.2.本地事务

在计算机系统中,更多的是通过关系型数据库来控制事务,这是利用数据库本身的事务特性来实现的,因此叫数据库事务,由于应用主要靠关系数据库来控制事务,而数据库通常和应用在同一个服务器,所以基于关系型数据库的事务又被称为本地事务。
回顾一下数据库事务的四大特性 ACID:
A(Atomic):原子性,构成事务的所有操作,要么都执行完成,要么全部不执行,不可能出现部分成功部分失败的情况。
C(Consistency):一致性,在事务执行前后,数据库的一致性约束没有被破坏。比如:张三向李四转100元,转账前和转账后的数据是正确状态这叫一致性,如果出现张三转出100元,李四账户没有增加100元这就出现了数据错误,就没有达到一致性。
I(Isolation):隔离性,数据库中的事务一般都是并发的,隔离性是指并发的两个事务的执行互不干扰,一个事务不能看到其他事务运行过程的中间状态。通过配置事务隔离级别可以避脏读、重复读等问题。
D(Durability):持久性,事务完成之后,该事务对数据的更改会被持久化到数据库,且不会被回滚。数据库事务在实现时会将一次事务涉及的所有操作全部纳入到一个不可分割的执行单元,该执行单元中的所有操作要么都成功,要么都失败,只要其中任一操作执行失败,都将导致整个事务的回滚。

1.1.3.分布式事务

随着互联网的快速发展,软件系统由原来的单体应用转变为分布式应用,下图描述了单体应用向微服务的演变:
在这里插入图片描述

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

我们知道本地事务依赖数据库本身提供的事务特性来实现,因此以下逻辑可以控制本地事务:

begin transaction; 
//1.本地数据库操作:张三减少金额    
//2.本地数据库操作:李四增加金额    
commit transation;

但是在分布式环境下,会变成下边这样:

begin transaction; 
//1.本地数据库操作:张三减少金额     
//2.远程调用:让李四增加金额     
commit transation;

可以设想,当远程调用让李四增加金额成功了,由于网络问题远程调用并没有返回,此时本地事务提交失败就回滚了张三减少金额的操作,此时张三和李四的数据就不一致了。因此在分布式架构的基础上,传统数据库事务就无法使用了,张三和李四的账户不在一个数据库中甚至不在一个应用系统里,实现转账事务需要通过远程调用,由于网络问题就会导致分布式事务问题。

1.1.4 分布式事务产生的场景

1、典型的场景就是微服务架构 微服务之间通过远程调用完成事务操作。
比如:订单微服务和库存微服务,下单的同时订单微服务请求库存微服务减库存。 简言之:跨JVM进程产生分布式事务。

在这里插入图片描述

2、单体系统访问多个数据库实例
当单体系统需要访问多个数据库(实例)时就会产生分布式事务。
比如:用户信息和订单信息分别在两个MySQL实例存储,用户管理系统删除用户信息,需要分别删除用户信息及用户的订单信息,由于数据分布在不同的数据实例,需要通过不同的数据库链接去操作数据,此时产生分布式事务。
简言之:跨数据库实例产生分布式事务。

在这里插入图片描述

3、多服务访问同一个数据库实例
比如:订单微服务和库存微服务即使访问同一个数据库也会产生分布式事务,原因就是跨JVM进程,两个微服务持有了不同的数据库链接进行数据库操作,此时产生分布式事务。

在这里插入图片描述

1.2分布式事务基础理论

1.2.1.CAP理论

1.2.1.1.理解CAP

CAP是 Consistency、Availability、Partition tolerance三个词语的缩写,分别表示一致性、可用性、分区容忍性。
如下图,是商品信息管理的执行流程:

在这里插入图片描述

整体执行流程如下:
1、商品服务请求主数据库写入商品信息(添加商品、修改商品、删除商品)
2、主数据库向商品服务响应写入成功。
3、商品服务请求从数据库读取商品信息。
C - Consistency:
一致性是指写操作后的读操作可以读取到最新的数据状态,当数据分布在多个节点上,从任意结点读取到的数据都是最新的状态。

上图中,商品信息的读写要满足一致性就是要实现如下目标:
1、商品服务写入主数据库成功,则向从数据库查询新数据也成功。
2、商品服务写入主数据库失败,则向从数据库查询新数据也失败。

如何实现一致性?
1、写入主数据库后要将数据同步到从数据库。
2、写入主数据库后,在向从数据库同步期间要将从数据库锁定,待同步完成后再释放锁,以免在新数据写入成功后,向从数据库查询到旧的数据。

分布式系统一致性的特点:
1、由于存在数据同步的过程,写操作的响应会有一定的延迟。
2、为了保证数据一致性会对资源暂时锁定,待数据同步完成释放锁定资源。
3、如果请求数据同步失败的结点则会返回错误信息,一定不会返回旧数据。

A - Availability
可用性是指任何事务操作都可以得到响应结果,且不会出现响应超时或响应错误。

上图中,商品信息读取满足可用性就是要实现如下目标:
1、从数据库接收到数据查询的请求则立即能够响应数据查询结果。
2、从数据库不允许出现响应超时或响应错误。

如何实现可用性?
1、写入主数据库后要将数据同步到从数据库。
2、由于要保证从数据库的可用性,不可将从数据库中的资源进行锁定。
3、即时数据还没有同步过来,从数据库也要返回要查询的数据,哪怕是旧数据,如果连旧数据也没有则可以按照约定返回一个默认信息,但不能返回错误或响应超时。

分布式系统可用性的特点:
1、 所有请求都有响应,且不会出现响应超时或响应错误。

P - Partition tolerance
通常分布式系统的各各结点部署在不同的子网,这就是网络分区,不可避免的会出现由于网络问题而导致结点之间通信失败,此时仍可对外提供服务,这叫分区容忍性。

上图中,商品信息读写满足分区容忍性就是要实现如下目标:
1、主数据库向从数据库同步数据失败不影响读写操作。
2、其一个结点挂掉不影响另一个结点对外提供服务。

如何实现分区容忍性?
1、尽量使用异步取代同步操作,例如使用异步方式将数据从主数据库同步到从数据,这样结点之间能有效的实现松耦合。
2、添加从数据库结点,其中一个从结点挂掉其它从结点提供服务。

分布式分区容忍性的特点:1、分区容忍性分是布式系统具备的基本能力。

CAP组合方式

1、上边商品管理的例子是否同时具备 CAP呢?
在所有分布式事务场景中不会同时具备CAP三个特性,因为在具备了P的前提下C和A是不能共存的。比如:下图满足了P即表示实现分区容忍:

在这里插入图片描述

本图分区容忍的含义是:
1)主数据库通过网络向从数据同步数据,可以认为主从数据库部署在不同的分区,通过网络进行交互。
2)当主数据库和从数据库之间的网络出现问题不影响主数据库和从数据库对外提供服务。
3)其一个结点挂掉不影响另一个结点对外提供服务。

如果要实现C则必须保证数据一致性,在数据同步的时候为防止向从数据库查询不一致的数据则需要将从数据库数据锁定,待同步完成后解锁,如果同步失败从数据库要返回错误信息或超时信息。如果要实现A则必须保证数据可用性,不管任何时候都可以向从数据查询数据,则不会响应超时或返回错误信息。通过分析发现在满足P的前提下C和A存在矛盾性。

1.2.1.2、CAP有哪些组合方式呢?

所以在生产中对分布式事务处理时要根据需求来确定满足CAP的哪两个方面。
1)AP:
放弃一致性,追求分区容忍性和可用性。这是很多分布式系统设计时的选择。
例如:
上边的商品管理,完全可以实现AP,前提是只要用户可以接受所查询的到数据在一定时间内不是最新的即可。通常实现AP都会保证最终一致性,后面讲的BASE理论就是根据AP来扩展的,一些业务场景 比如:订单退款,今日退款成功,明日账户到账,只要用户可以接受在一定时间内到账即可。
2)CP:
放弃可用性,追求一致性和分区容错性,我们的zookeeper其实就是追求的强一致,又比如跨行转账,一次转账请求要等待双方银行系统都完成整个事务才算完成。

3)CA:
放弃分区容忍性,即不进行分区,不考虑由于网络不通或结点挂掉的问题,则可以实现一致性和可用性。那么系统将不是一个标准的分布式系统,我们最常用的关系型数据就满足了CA。
上边的商品管理,如果要实现CA则架构如下:
在这里插入图片描述

主数据库和从数据库中间不再进行数据同步,数据库可以响应每次的查询请求,通过事务隔离级别实现每个查询请求都可以返回最新的数据。

1.2.1.3 总结

通过上面我们已经学习了CAP理论的相关知识,
CAP是一个已经被证实的理论:一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容忍性(Partition tolerance)这三项中的两项。
它可以作为我们进行架构设计、技术选型的考量标准。对于多数大型互联网应用的场景,结点众多、部署分散,而且现在的集群规模越来越大,所以节点故障、网络故障是常态,而且要保证服务可用性达到N个9(99.99…%),并要达到良好的响应性能来提高用户体验,因此一般都会做出如下选择:保证P和A,舍弃C强一致,保证最终一致性。

1.2.2.BASE理论

1、理解强一致性和最终一致性

CAP理论告诉我们一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容忍性(Partition tolerance)这三项中的两项,其中AP在实际应用中较多,AP即舍弃一致性,保证可用性和分区容忍性,但是在实际生产中很多场景都要实现一致性,比如前边我们举的例子主数据库向从数据库同步数据,即使不要一致性,但是最终也要将数据同步成功来保证数据一致,这种一致性和CAP中的一致性不同,CAP中的一致性要求在任何时间查询每个结点数据都必须一致,它强调的是强一致性,但是最终一致性是允许可以在一段时间内每个结点的数据不一致,但是经过一段时间每个结点的数据必须一致,它强调的是最终数据的一致性。

2、Base理论介绍

BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性)三个短语的缩写。
BASE理论是对CAP中AP的一个扩展,通过牺牲强一致性来获得可用性,当出现故障允许部分不可用但要保证核心功能可用,允许数据在一段时间内是不一致的,但最终达到一致状态。满足BASE理论的事务,我们称之为“柔性事务”。

基本可用:分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。
如,电商网站交易付款出现问题了,商品依然可以正常浏览。

软状态:由于不要求强一致性,所以BASE允许系统中存在中间状态(也叫软状态),这个状态不影响系统可用性,如订单的"支付中"、“数据同步中”等状态,待数据最终一致后状态改为“成功”状态。

最终一致:最终一致是指经过一段时间后,所有节点数据都将会达到一致。如订单的"支付中"状态,最终会变为“支付成功”或者"支付失败",使订单状态与实际交易结果达成一致,但需要一定时间的延迟、等待。

二、分布式事务解决方案

分布式事务,目前有以下几种解决方案:

  1. 2PC
  2. 3PC
  3. TCC
  4. 可靠性消息最终一致性

2.1、2PC方案

2.1.1.什么是2PC

2PC即两阶段提交协议,是将整个事务流程分为两个阶段,准备阶段(Prepare phase)、提交阶段(commitphase),2是指两个阶段,P是指准备阶段,C是指提交阶段。

举例:张三和李四好久不见,老友约起聚餐,饭店老板要求先买单,才能出票。这时张三和李四分别抱怨近况不如意,囊中羞涩,都不愿意请客,这时只能AA。只有张三和李四都付款,老板才能出票安排就餐。但由于张三和李四都是铁公鸡,形成了尴尬的一幕:
准备阶段:老板要求张三付款,张三付款。老板要求李四付款,李四付款。
提交阶段:老板出票,两人拿票纷纷落座就餐。

例子中形成了一个事务,若张三或李四其中一人拒绝付款,或钱不够,店老板都不会给出票,并且会把已收款退回。整个事务过程由事务管理器和参与者组成,店老板就是事务管理器,张三、李四就是事务参与者,事务管理器负责决策整个分布式事务的提交和回滚,事务参与者负责自己本地事务的提交和回滚。

在计算机中部分关系数据库如Oracle、MySQL支持两阶段提交协议,如下图:

  1. 准备阶段(Prepare phase):
    事务管理器给每个参与者发送Prepare消息,每个数据库参与者在本地执行事务,并写本地的Undo/Redo日志,此时事务没有提交。(Undo日志是记录修改前的数据,用于数据库回滚,Redo日志是记录修改后的数据,用于提交事务后写入数据文件)

  2. 提交阶段(commit phase):
    如果事务管理器收到了参与者的执行失败或者超时消息时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据事务管理器的指令执行提交或者回滚操作,并释放事务处理过程中使用的锁资源。注意:必须在最后阶段释放锁资源。
    下图展示了2PC的两个阶段,分成功和失败两个情况说明:

在这里插入图片描述

在这里插入图片描述

2.1.2.XA

2PC的传统方案是在数据库层面实现的,如Oracle、MySQL都支持2PC协议,为了统一标准减少行业内不必要的对接成本,需要制定标准化的处理模型及接口标准,国际开放标准组织Open Group定义了分布式事务处理模型DTP(Distributed Transaction Processing Reference Model)。

为了让大家更明确XA方案的内容程,下面新用户注册送积分为例来说明:
在这里插入图片描述

执行流程如下:
1、应用程序(AP)持有用户库和积分库两个数据源。
2、应用程序(AP)通过TM通知用户库RM新增用户,同时通知积分库RM为该用户新增积分,RM此时并未提交事务,此时用户和积分资源锁定。
3、TM收到执行回复,只要有一方失败则分别向其他RM发起回滚事务,回滚完毕,资源锁释放。
4、TM收到执行回复,全部成功,此时向所有RM发起提交事务,提交完毕,资源锁释放。

DTP模型定义如下角色:
AP(Application Program):即应用程序,可以理解为使用DTP分布式事务的程序。
RM(Resource Manager):即资源管理器,可以理解为事务的参与者,一般情况下是指一个数据库实例,通过资源管理器对该数据库进行控制,资源管理器控制着分支事务。
TM(Transaction Manager):事务管理器,负责协调和管理事务,事务管理器控制着全局事务,管理事务生命周期,并协调各个RM。全局事务是指分布式事务处理环境中,需要操作多个数据库共同完成一个工作,这个工作即是一个全局事务。

DTP模型定义TM和RM之间通讯的接口规范叫XA,简单理解为数据库提供的2PC接口协议,基于数据库的XA协议来实现2PC又称为XA方案。

以上三个角色之间的交互方式如下:
1)TM向AP提供 应用程序编程接口,AP通过TM提交及回滚事务。
2)TM交易中间件通过XA接口来通知RM数据库事务的开始、结束以及提交、回滚等。

总结:
整个2PC的事务流程涉及到三个角色AP、RM、TM。AP指的是使用2PC分布式事务的应用程序;RM指的是资源管理器,它控制着分支事务;TM指的是事务管理器,它控制着整个全局事务。
1)在准备阶段RM执行实际的业务操作,但不提交事务,资源锁定;
2)在提交阶段TM会接受RM在准备阶段的执行回复,只要有任一个RM执行失败,TM会通知所有RM执行回滚操作,否则,TM将会通知所有RM提交该事务。提交阶段结束资源锁释放。

以下几点是XA-两阶段提交协议中会遇到的一些问题:

  • 性能问题。从流程上我们可以看得出,其最大缺点就在于它的执行过程中间,节点都处于阻塞状态。各个操作数据库的节点此时都占用着数据库资源,只有当所有节点准备完毕,事务协调者才会通知进行全局提交,参与者进行本地事务提交后才会释放资源。这样的过程会比较漫长,对性能影响比较大。
  • 协调者单点故障问题。事务协调者是整个XA模型的核心,一旦事务协调者节点挂掉,会导致参与者收不到提交或回滚的通知,从而导致参与者节点始终处于事务无法完成的中间状态。
  • **丢失消息导致的数据不一致问题。**在第二个阶段,如果发生局部网络问题,一部分事务参与者收到了提交消息,另一部分事务参与者没收到提交消息,那么就会导致节点间数据的不一致问题。

2.1.3.Seata方案

Seata是由阿里中间件团队发起的开源项目 Fescar,后更名为Seata,它是一个是开源的分布式事务框架。
传统2PC的问题在Seata中得到了解决,它通过对本地关系数据库的分支事务的协调来驱动完成全局事务,是工作在应用层的中间件。

主要优点是性能较好,且不长时间占用连接资源,它以高效并且对业务0侵入的方式解决微服务场景下面临的分布式事务问题,
它目前提供AT模式(即2PC)及TCC模式的分布式事务解决方案。

Seata的设计思想如下:

Seata的设计目标其一是对业务无侵入,因此从业务无侵入的2PC方案着手,在传统2PC的基础上演进,并解决2PC方案面临的问题。
Seata把一个分布式事务理解成一个包含了若干分支事务的全局事务。
全局事务的职责是协调其下管辖的分支事务达成一致,要么一起成功提交,要么一起失败回滚。此外,通常分支事务本身就是一个关系数据库的本地事务,
下图是全局事务与分支事务的关系图:

下图是全局事务与分支事务的关系图:

在这里插入图片描述

与 传统2PC 的模型类似,Seata定义了3个组件来协议分布式事务的处理过程:
在这里插入图片描述

  • Transaction Coordinator (TC):事务协调器,它是独立的中间件,需要独立部署运行,它维护全局事务的运行状态,接收TM指令发起全局事务的提交与回滚,负责与RM通信协调各各分支事务的提交或回滚。
  • Transaction Manager ™: 事务管理器,TM需要嵌入应用程序中工作,它负责开启一个全局事务,并最终向TC发起全局提交或全局回滚的指令。
  • Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器TC的指令,驱动分支(本地)事务的提交和回滚。

还拿新用户注册送积分举例Seata的分布式事务过程:
在这里插入图片描述

具体的执行流程如下:

  1. 用户服务的 TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID。
  2. 用户服务的 RM 向 TC 注册分支事务,该分支事务在用户服务执行新增用户逻辑,并将其纳入 XID 对应全局事务的管辖。
  3. 用户服务执行分支事务,向用户表插入一条记录。
  4. 逻辑执行到远程调用积分服务时(XID 在微服务调用链路的上下文中传播)。积分服务的RM 向 TC 注册分支事务,该分支事务执行增加积分的逻辑,并将其纳入 XID 对应全局事务的管辖。
  5. 积分服务执行分支事务,向积分记录表插入一条记录,执行完毕后,返回用户服务。
  6. 用户服务分支事务执行完毕。
  7. TM 向 TC 发起针对 XID 的全局提交或回滚决议。
  8. TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
Seata实现2PC与传统2PC的差别:

架构层次方面:
传统2PC方案的 RM 实际上是在数据库层,RM 本质上就是数据库自身,通过 XA 协议实现,
而Seata的 RM 是以jar包的形式作为中间件层部署在应用程序这一侧的。

两阶段提交方面,传统2PC无论第二阶段的决议是commit还是rollback,事务性资源的锁都要保持到Phase2完成才释放。而Seata的做法是在Phase1 就将本地事务提交,这样就可以省去Phase2持锁的时间,整体提高效率。

Seata的隔离级别

Seata由于一阶段RM自动提交本地事务的原因,默认隔离级别为Read Uncommitted。如果希望隔离级别为Read Committed,那么可以使用SELECT...FOR UPDATE语句。Seata引擎重写了SELECT...FOR UPDATE语句执行逻辑,SELECT...FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT...FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是已提交的才返回。

出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。

Seata支持的模式

AT:依赖于RM拥有本地数据库事务的能力,对于客户业务无侵入性.AT模式中业务逻辑不需要关注事务机制,分支与全局事务的交互过程自动进行。

MT:MT模式本质上是一种TCC方案,业务逻辑需要被拆分为 Prepare/Commit/Rollback 3 部分,形成一个 MT 分支,加入全局事务。

2.2、3PC方案

三阶段提交又称3PC,其在两阶段提交的基础上增加了CanCommit阶段,并引入了超时机制。一旦事务参与者迟迟没有收到协调者的Commit请求,就会自动进行本地commit,这样相对有效地解决了协调者单点故障的问题。

阶段
CanCommit这个阶段类似于2PC中的第二个阶段中的Ready阶段,是一种事务询问操作,事务的协调者向所有参与者询问“你们是否可以完成本次事务?”,如果参与者节点认为自身可以完成事务就返回“YES”,否则“NO”。而在实际的场景中参与者节点会对自身逻辑进行事务尝试,其实说白了就是检查下自身状态的健康性,看有没有能力进行事务操作。
PrepareCommit阶段一中,如果所有的参与者都返回Yes的话,那么就会进入PreCommit阶段进行事务预提交。此时分布式事务协调者会向所有的参与者节点发送PreCommit请求,参与者收到后开始执行事务操作,并将Undo和Redo信息记录到事务日志中。参与者执行完事务操作后(此时属于未提交事务的状态),就会向协调者反馈“Ack”表示我已经准备好提交了,并等待协调者的下一步指令。
doCommit在阶段二中如果所有的参与者节点都可以进行PreCommit提交,那么协调者就会从**“预提交状态”-》“提交状态”。然后向所有的参与者节点发送"doCommit"请求,参与者节点在收到提交请求后就会各自执行事务提交操作,并向协调者节点反馈“Ack”**消息,协调者收到所有参与者的Ack消息后完成事务。 相反,如果有一个参与者节点未完成PreCommit的反馈或者反馈超时,那么协调者都会向所有的参与者节点发送abort请求,从而中断事务。

相比较2PC而言,3PC对于协调者(Coordinator)和参与者(Partcipant)都设置了超时时间,而2PC只有协调者才拥有超时机制。这解决了一个什么问题呢?这个优化点,主要是避免了参与者在长时间无法与协调者节点通讯(协调者挂掉了)的情况下,无法释放资源的问题,因为参与者自身拥有超时机制会在超时后,自动进行本地commit从而进行释放资源。而这种机制也侧面降低了整个事务的阻塞时间和范围。

另外,通过CanCommit、PreCommit、DoCommit三个阶段的设计,相较于2PC而言,多设置了一个缓冲阶段保证了在最后提交阶段之前各参与节点的状态是一致的。

以上就是3PC相对于2PC的一个提高(相对缓解了2PC中的前两个问题),但是3PC依然没有完全解决数据不一致的问题。

2.3.TCC方案

2.3.1.什么是TCC事务

TCC是Try、Confirm、Cancel三个词语的缩写,
TCC要求每个分支事务实现三个操作:

  • 预处理Try、

  • 确认Confirm、

  • 撤销Cancel。

  • Try操作做业务检查及资源预留,

  • Confirm做业务确认操作

  • Cancel实现一个与Try相反的操作即回滚操作

TM首先发起所有的分支事务的try操作,任何一个分支事务的try操作执行失败,TM将会发起所有分支事务的Cancel操作,若try操作全部成功,TM将会发起所有分支事务的Confirm操作,其Confirm/Cancel操作若执行失败,TM会进行重试。

在这里插入图片描述

分支事务失败的情况:

在这里插入图片描述

TCC分为三个阶段:

  1. Try 阶段是做业务检查(一致性)及资源预留(隔离),此阶段仅是一个初步操作,它和后续的Confirm 一起才能
    真正构成一个完整的业务逻辑。
  2. Confirm 阶段是做确认提交,Try阶段所有分支事务执行成功后开始执行 Confirm。通常情况下,采用TCC则
    认为 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。若Confirm阶段真的出错了,需引
    入重试机制或人工处理。
  3. Cancel 阶段是在业务执行错误需要回滚的状态下执行分支事务的业务取消,预留资源释放。通常情况下,采
    用TCC则认为Cancel阶段也是一定成功的。若Cancel阶段真的出错了,需引入重试机制或人工处理。
  4. TM事务管理器
    TM事务管理器可以实现为独立的服务,也可以让全局事务发起方充当TM的角色,TM独立出来是为了成为公
    用组件,是为了考虑系统结构和软件复用。

TM在发起全局事务时生成全局事务记录,全局事务ID贯穿整个分布式事务调用链条,用来记录事务上下文,
追踪和记录状态,由于Confirm 和cancel失败需进行重试,因此需要实现为幂等,幂等性是指同一个操作无论请求
多少次,其结果都相同。

2.4.可靠消息最终一致性

2.4.1.什么是可靠消息最终一致性

事务可靠消息最终一致性方案是指当事务发起方执行完成本地事务后并发出一条消息,事务参与方(消息消费者)一定能够接收消息并处理事务成功,此方案强调的是只要消息发给事务参与方最终事务要达到一致。

此方案是利用消息中间件完成,如下图:
事务发起方(消息生产方)将消息发给消息中间件,事务参与方从消息中间件接收消息,事务发起方和消息中间件之间,事务参与方(消息消费方)和消息中间件之间都是通过网络通信,由于网络通信的不确定性会导致分布式事务问题。

在这里插入图片描述

可靠消息最终一致性方案要解决的问题:
1.本地事务与消息发送的原子性问题

本地事务与消息发送的原子性问题即:事务发起方在本地事务执行成功后消息必须发出去,否则就丢弃消息。即实现本地事务和消息发送的原子性,要么都成功,要么都失败。
本地事务与消息发送的原子性问题是实现可靠消息最终一致性方案的关键问题。
先来尝试下这种操作,先发送消息,再操作数据库:

begin transaction;   
//1.发送MQ   
//2.数据库操作 
commit transation;

这种情况下无法保证数据库操作与发送消息的一致性,因为可能发送消息成功,数据库操作失败。你立马想到第二种方案,先进行数据库操作,再发送消息:

begin transaction;  
//1.数据库操作  
//2.发送MQ 
commit transation;

这种情况下貌似没有问题,如果发送MQ消息失败,就会抛出异常,导致数据库事务回滚。但如果是超时异常,数据库回滚,但MQ其实已经正常发送了,同样会导致不一致。

2、事务参与方接收消息的可靠性

事务参与方必须能够从消息队列接收到消息,如果接收消息失败可以重复接收消息。

3、消息重复消费的问题

由于网络2的存在,若某一个消费节点超时但是消费成功,此时消息中间件会重复投递此消息,就导致了消息的重复消费。要解决消息重复消费的问题就要实现事务参与方的方法幂等性。

2.4.2.解决方案

上节讨论了可靠消息最终一致性事务方案需要解决的问题,本节讨论具体的解决方案。

本地消息表方案

本地消息表这个方案最初是eBay提出的,此方案的核心是通过本地事务保证数据业务操作和消息的一致性,然后通过定时任务将消息发送至消息中间件,待确认消息发送给消费方成功再将消息删除。
下面以注册送积分为例来说明:

下例共有两个微服务交互,用户服务和积分服务,用户服务负责添加用户,积分服务负责增加积分。
在这里插入图片描述

交互流程如下:
1、用户注册
用户服务在本地事务新增用户和增加 ”积分消息日志“。(用户表和消息表通过本地事务保证一致)
下边是伪代码

begin transaction;  
//1.新增用户   
//2.存储积分消息日志 
commit transation;

这种情况下,本地数据库操作与存储积分消息日志处于同一个事务中,本地数据库操作与记录消息日志操作具备原子性。
2、定时任务扫描日志
如何保证将消息发送给消息队列呢?经过第一步消息已经写到消息日志表中,可以启动独立的线程,定时对消息日志表中的消息进行扫描并发送至消息中间件,在消息中间件反馈发送成功后删除该消息日志,否则等待定时任务下一周期重试。

3、消费消息
如何保证消费者一定能消费到消息呢?
这里可以使用MQ的ack(即消息确认)机制,消费者监听MQ,如果消费者接收到消息并且业务处理完成后向MQ发送ack(即消息确认),此时说明消费者正常消费消息完成,MQ将不再向消费者推送消息,否则消费者会不断重试向消费者来发送消息。积分服务接收到”增加积分“消息,开始增加积分,积分增加成功后向消息中间件回应ack,否则消息中间件将重复投递此消息。由于消息会重复投递,积分服务的”增加积分“功能需要实现幂等性。

RocketMQ事务消息方案

RocketMQ 是一个来自阿里巴巴的分布式消息中间件,于 2012 年开源,并在 2017 年正式成为 Apache 顶级项目。据了解,包括阿里云上的消息产品以及收购的子公司在内,阿里集团的消息产品全线都运行在 RocketMQ 之上,并且最近几年的双十一大促中,RocketMQ 都有抢眼表现。Apache RocketMQ 4.3之后的版本正式支持事务消息,为分布式事务实现提供了便利性支持。RocketMQ 事务消息设计则主要是为了解决 Producer 端的消息发送与本地事务执行的原子性问题,RocketMQ 的设计中 broker 与 producer 端的双向通信能力,使得 broker 天生可以作为一个事务协调者存在;而 RocketMQ本身提供的存储机制为事务消息提供了持久化能力;RocketMQ 的高可用机制以及可靠消息设计则为事务消息在系统发生异常时依然能够保证达成事务的最终一致性。

在RocketMQ 4.3后实现了完整的事务消息,实际上其实是对本地消息表的一个封装,将本地消息表移动到了MQ内部,解决 Producer 端的消息发送与本地事务执行的原子性问题。
在这里插入图片描述

执行流程如下:
为方便理解我们还以注册送积分的例子来描述 整个流程。
Producer 即MQ发送方,本例中是用户服务,负责新增用户。MQ订阅方即消息消费方,本例中是积分服务,负责新增积分。

1、Producer 发送事务消息
Producer (MQ发送方)发送事务消息至MQ Server,MQ Server将消息状态标记为Prepared(预备状态),注意此时这条消息消费者(MQ订阅方)是无法消费到的。
本例中,Producer 发送 ”增加积分消息“ 到MQ Server。

2、MQ Server回应消息发送成功
MQ Server接收到Producer 发送给的消息则回应发送成功表示MQ已接收到消息。

3、Producer 执行本地事务
Producer 端执行业务代码逻辑,通过本地数据库事务控制。
本例中,Producer 执行添加用户操作。

4、消息投递
若Producer本地事务执行成功则自动向MQServer发送commit消息,MQ Server接收到commit消息后将”增加积分消息“ 状态标记为可消费,此时MQ订阅方(积分服务)即正常消费消息;
若Producer 本地事务执行失败则自动向MQServer发送rollback消息,MQ Server接收到rollback消息后 将删除”增加积分消息“ 。MQ订阅方(积分服务)消费消息,消费成功则向MQ回应ack,否则将重复接收消息。这里ack默认自动回应,即程序执行正常则自动回应ack。

5、事务回查
如果执行Producer端本地事务过程中,执行端挂掉,或者超时,MQ Server将会不停的询问同组的其他 Producer来获取事务执行状态,这个过程叫事务回查。
MQ Server会根据事务回查结果来决定是否投递消息。
以上主干流程已由RocketMQ实现,对用户侧来说,用户需要分别实现本地事务执行以及本地事务回查方法,因此只需关注本地事务的执行状态即可。
RoacketMQ提供RocketMQLocalTransactionListener接口:


public interface RocketMQLocalTransactionListener {
   /**
   ‐ 发送prepare消息成功此方法被回调,该方法用于执行本地事务
   ‐ @param msg 回传的消息,利用transactionId即可获取到该消息的唯一Id
   ‐ @param arg 调用send方法时传递的参数,当send时候若有额外的参数可以传递到send方法中,这里能获取到
   ‐ @return 返回事务状态,COMMIT:提交  ROLLBACK:回滚  UNKNOW:回调
     */
       RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg);
   /**
   ‐ @param msg 通过获取transactionId来判断这条消息的本地事务执行状态
   ‐ @return 返回事务状态,COMMIT:提交  ROLLBACK:回滚  UNKNOW:回调
     */
       RocketMQLocalTransactionState checkLocalTransaction(Message msg);
   }

发送事务消息:
以下是RocketMQ提供用于发送事务消息的API:

TransactionMQProducer producer = new TransactionMQProducer("ProducerGroup");
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
//设置TransactionListener实现
producer.setTransactionListener(transactionListener);
//发送事务消息
SendResult sendResult = producer.sendMessageInTransaction(msg, null);

2.4.3.RocketMQ实现可靠消息最终一致性事务

业务说明

本实例通过RocketMQ中间件实现可靠消息最终一致性分布式事务,模拟两个账户的转账交易过程。两个账户在分别在不同的银行(张三在bank1、李四在bank2),bank1、bank2是两个微服务。交易过程是,张三给李四转账指定金额。
上述交易步骤,张三扣减金额与给bank2发转账消息,两个操作必须是一个整体性的事务。
在这里插入图片描述

启动RocketMQ
(1)下载RocketMQ服务器下载地址:http://mirrors.tuna.tsinghua.edu.cn/apache/rocketmq/4.5.0/rocketmq-all-4.5.0-bin-release.zip
(2)解压并启动启动nameserver:和broke
在这里插入图片描述

bank1实现如下功能:
1、张三扣减金额,提交本地事务。
2、向MQ发送转账消息。

2)Dao

@Mapper
@Component
public interface AccountInfoDao {
    @Update("update account_info set account_balance=account_balance+#{amount} where account_no=#{accountNo}")
    int updateAccountBalance(@Param("accountNo") String accountNo, @Param("amount") Double amount);


    @Select("select * from account_info where where account_no=#{accountNo}")
    AccountInfo findByIdAccountNo(@Param("accountNo") String accountNo);



    @Select("select count(1) from de_duplication where tx_no = #{txNo}")
    int isExistTx(String txNo);


    @Insert("insert into de_duplication values(#{txNo},now());")
    int addTx(String txNo);

}

3)AccountInfoService

@Service
@Slf4j
public class AccountInfoServiceImpl implements AccountInfoService {

    @Autowired
    AccountInfoDao accountInfoDao;

    @Autowired(required = false)
    RocketMQTemplate rocketMQTemplate;


    //向mq发送转账消息
    @Override
    public void sendUpdateAccountBalance(AccountChangeEvent accountChangeEvent) {

        //将accountChangeEvent转成json
        JSONObject jsonObject =new JSONObject();
        jsonObject.put("accountChange",accountChangeEvent);
        String jsonString = jsonObject.toJSONString();
        log.info(jsonString);
        //生成message类型
        Message<String> message = MessageBuilder.withPayload(jsonString).build();
        //发送一条事务消息
        /**
         * String txProducerGroup 生产组
         * String destination topic,
         * Message<?> message, 消息内容
         * Object arg 参数
         */
        rocketMQTemplate.sendMessageInTransaction("producer_group_txmsg_bank1","topic_txmsg",message,null);

    }

    //更新账户,扣减金额
    @Override
    @Transactional
    public void doUpdateAccountBalance(AccountChangeEvent accountChangeEvent) {
        //幂等判断
        if(accountInfoDao.isExistTx(accountChangeEvent.getTxNo())>0){
            return ;
        }
        //扣减金额
        accountInfoDao.updateAccountBalance(accountChangeEvent.getAccountNo(),accountChangeEvent.getAmount() * -1);
        //添加事务日志
        accountInfoDao.addTx(accountChangeEvent.getTxNo());
        if(accountChangeEvent.getAmount() == 3){
            throw new RuntimeException("人为制造异常");
        }
    }
}

4)RocketMQLocalTransactionListener
编写RocketMQLocalTransactionListener接口实现类,实现执行本地事务和事务回查两个方法。

/**

  • @author Administrator

  • @version 1.0
    **/
    @Component
    @Slf4j
    @RocketMQTransactionListener(txProducerGroup = “producer_group_txmsg_bank1”)
    public class ProducerTxmsgListener implements RocketMQLocalTransactionListener {

    @Autowired
    AccountInfoService accountInfoService;

    @Autowired
    AccountInfoDao accountInfoDao;

    //事务消息发送后的回调方法,当消息发送给mq成功,此方法被回调
    @Override
    @Transactional
    public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) {

     try {
         //解析message,转成AccountChangeEvent
         String messageString = new String((byte[]) message.getPayload());
         JSONObject jsonObject = JSONObject.parseObject(messageString);
         String accountChangeString = jsonObject.getString("accountChange");
         //将accountChange(json)转成AccountChangeEvent
         AccountChangeEvent accountChangeEvent = JSONObject.parseObject(accountChangeString, AccountChangeEvent.class);
         //执行本地事务,扣减金额
         accountInfoService.doUpdateAccountBalance(accountChangeEvent);
         //当返回RocketMQLocalTransactionState.COMMIT,自动向mq发送commit消息,mq将消息的状态改为可消费
         return RocketMQLocalTransactionState.COMMIT;
     } catch (Exception e) {
         e.printStackTrace();
         return RocketMQLocalTransactionState.ROLLBACK;
     }
    

    }

    //事务状态回查,查询是否扣减金额
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
    //解析message,转成AccountChangeEvent
    String messageString = new String((byte[]) message.getPayload());
    JSONObject jsonObject = JSONObject.parseObject(messageString);
    String accountChangeString = jsonObject.getString(“accountChange”);
    //将accountChange(json)转成AccountChangeEvent
    AccountChangeEvent accountChangeEvent = JSONObject.parseObject(accountChangeString, AccountChangeEvent.class);
    //事务id
    String txNo = accountChangeEvent.getTxNo();
    log.info(“事务状态回查”);
    int existTx = accountInfoDao.isExistTx(txNo);
    if(existTx>0){
    return RocketMQLocalTransactionState.COMMIT;
    }else{
    return RocketMQLocalTransactionState.UNKNOWN;
    }
    }
    }

5)Controller

@RestController
@Slf4j
public class AccountInfoController {
    @Autowired
    private AccountInfoService accountInfoService;

    @GetMapping(value = "/transfer")
    public String transfer(@RequestParam("accountNo")String accountNo, @RequestParam("amount") Double amount){
        //创建一个事务id,作为消息内容发到mq
        String tx_no = UUID.randomUUID().toString();
        AccountChangeEvent accountChangeEvent = new AccountChangeEvent(accountNo,amount,tx_no);
        //发送消息
        accountInfoService.sendUpdateAccountBalance(accountChangeEvent);
        return "转账成功";
    }
}

配置信息

server.port=8080
swagger.enable = true

spring.datasource.driver-class-name = com.mysql.jdbc.Driver
spring.datasource.url = jdbc:mysql://localhost:3306/bank1?useUnicode=true
spring.datasource.username = root
spring.datasource.password = 123456

rocketmq.producer.group = producer_bank1
rocketmq.name-server = 127.0.0.1:9876

logging.level.root = info
logging.level.org.springframework.web = info
logging.level.com.topcheer.mq  = debug

bank2需要实现如下功能:
1、监听MQ,接收消息。
2、接收到消息增加账户金额。
1) Service
注意为避免消息重复发送,这里需要实现幂等。

@Service
@Slf4j
public class AccountInfoServiceImpl implements AccountInfoService {

    @Autowired
    AccountInfoDao accountInfoDao;

    //更新账户,增加金额
    @Override
    @Transactional
    public void addAccountInfoBalance(AccountChangeEvent accountChangeEvent) {
        log.info("bank2更新本地账号,账号:{},金额:{}",accountChangeEvent.getAccountNo(),accountChangeEvent.getAmount());
        if(accountInfoDao.isExistTx(accountChangeEvent.getTxNo())>0){

            return ;
        }
        //增加金额
        accountInfoDao.updateAccountBalance(accountChangeEvent.getAccountNo(),accountChangeEvent.getAmount());
        //添加事务记录,用于幂等
        accountInfoDao.addTx(accountChangeEvent.getTxNo());
        if(accountChangeEvent.getAmount() == 4){
            throw new RuntimeException("人为制造异常");
        }
    }
}

监听类

@Component
@Slf4j
@RocketMQMessageListener(consumerGroup = "consumer_group_txmsg_bank2",topic = "topic_txmsg")
public class TxmsgConsumer implements RocketMQListener<String> {

    @Autowired
    AccountInfoService accountInfoService;

    //接收消息
    @Override
    public void onMessage(String message) {
        log.info("开始消费消息:{}",message);
        //解析消息
        JSONObject jsonObject = JSONObject.parseObject(message);
        String accountChangeString = jsonObject.getString("accountChange");
        //转成AccountChangeEvent
        AccountChangeEvent accountChangeEvent = JSONObject.parseObject(accountChangeString, AccountChangeEvent.class);
        //设置账号为李四的
        accountChangeEvent.setAccountNo("2");
        //更新本地账户,增加金额
        accountInfoService.addAccountInfoBalance(accountChangeEvent);

    }
}

测试:
一:能转账成功
在这里插入图片描述

2020-03-11 16:47:39.276  INFO 23240 --- [nio-8080-exec-1] c.t.m.s.impl.AccountInfoServiceImpl      : {"accountChange":{"accountNo":"1","amount":10.0,"txNo":"a611b185-d3b7-45b2-ace3-b50c3b2d2d54"}}
2020-03-11 16:47:40.513 DEBUG 23240 --- [nio-8080-exec-1] c.t.mq.dao.AccountInfoDao.isExistTx      : ==>  Preparing: select count(1) from de_duplication where tx_no = ? 
2020-03-11 16:47:40.526 DEBUG 23240 --- [nio-8080-exec-1] c.t.mq.dao.AccountInfoDao.isExistTx      : ==> Parameters: a611b185-d3b7-45b2-ace3-b50c3b2d2d54(String)
2020-03-11 16:47:40.574 DEBUG 23240 --- [nio-8080-exec-1] c.t.mq.dao.AccountInfoDao.isExistTx      : <==      Total: 1
2020-03-11 16:47:40.576 DEBUG 23240 --- [nio-8080-exec-1] c.t.m.d.A.updateAccountBalance           : ==>  Preparing: update account_info set account_balance=account_balance+? where account_no=? 
2020-03-11 16:47:40.577 DEBUG 23240 --- [nio-8080-exec-1] c.t.m.d.A.updateAccountBalance           : ==> Parameters: -10.0(Double), 1(String)
2020-03-11 16:47:40.579 DEBUG 23240 --- [nio-8080-exec-1] c.t.m.d.A.updateAccountBalance           : <==    Updates: 1
2020-03-11 16:47:40.580 DEBUG 23240 --- [nio-8080-exec-1] c.topcheer.mq.dao.AccountInfoDao.addTx   : ==>  Preparing: insert into de_duplication values(?,now()); 
2020-03-11 16:47:40.580 DEBUG 23240 --- [nio-8080-exec-1] c.topcheer.mq.dao.AccountInfoDao.addTx   : ==> Parameters: a611b185-d3b7-45b2-ace3-b50c3b2d2d54(String)
2020-03-11 16:47:40.582 DEBUG 23240 --- [nio-8080-exec-1] c.topcheer.mq.dao.AccountInfoDao.addTx   : <==    Updates: 1

2020-03-11 16:47:48.636  INFO 22956 --- [MessageThread_1] com.topcheer.mq.message.TxmsgConsumer    : 开始消费消息:{"accountChange":{"accountNo":"1","amount":10.0,"txNo":"a611b185-d3b7-45b2-ace3-b50c3b2d2d54"}}
2020-03-11 16:47:48.648  INFO 22956 --- [MessageThread_1] c.t.m.s.impl.AccountInfoServiceImpl      : bank2更新本地账号,账号:2,金额:10.0
2020-03-11 16:47:48.671 DEBUG 22956 --- [MessageThread_1] c.t.mq.dao.AccountInfoDao.isExistTx      : ==>  Preparing: select count(1) from de_duplication where tx_no = ? 
2020-03-11 16:47:48.686 DEBUG 22956 --- [MessageThread_1] c.t.mq.dao.AccountInfoDao.isExistTx      : ==> Parameters: a611b185-d3b7-45b2-ace3-b50c3b2d2d54(String)
2020-03-11 16:47:48.701 DEBUG 22956 --- [MessageThread_1] c.t.mq.dao.AccountInfoDao.isExistTx      : <==      Total: 1
2020-03-11 16:47:48.704 DEBUG 22956 --- [MessageThread_1] c.t.m.d.A.updateAccountBalance           : ==>  Preparing: update account_info set account_balance=account_balance+? where account_no=? 
2020-03-11 16:47:48.704 DEBUG 22956 --- [MessageThread_1] c.t.m.d.A.updateAccountBalance           : ==> Parameters: 10.0(Double), 2(String)
2020-03-11 16:47:48.707 DEBUG 22956 --- [MessageThread_1] c.t.m.d.A.updateAccountBalance           : <==    Updates: 1
2020-03-11 16:47:48.707 DEBUG 22956 --- [MessageThread_1] c.topcheer.mq.dao.AccountInfoDao.addTx   : ==>  Preparing: insert into de_duplication values(?,now()); 
2020-03-11 16:47:48.708 DEBUG 22956 --- [MessageThread_1] c.topcheer.mq.dao.AccountInfoDao.addTx   : ==> Parameters: a611b185-d3b7-45b2-ace3-b50c3b2d2d54(String)
2020-03-11 16:47:48.710 DEBUG 22956 --- [MessageThread_1] c.topcheer.mq.dao.AccountInfoDao.addTx   : <==    Updates: 1

二:张三转账失败的时候

三:李四转账失败的时候

2020-03-11 16:50:56.320  INFO 23240 --- [nio-8080-exec-8] c.t.m.s.impl.AccountInfoServiceImpl      : {"accountChange":{"accountNo":"1","amount":4.0,"txNo":"3088dda5-c5a0-4a40-8f9e-715ab50563ff"}}
2020-03-11 16:50:56.325 DEBUG 23240 --- [nio-8080-exec-8] c.t.mq.dao.AccountInfoDao.isExistTx      : ==>  Preparing: select count(1) from de_duplication where tx_no = ? 
2020-03-11 16:50:56.325 DEBUG 23240 --- [nio-8080-exec-8] c.t.mq.dao.AccountInfoDao.isExistTx      : ==> Parameters: 3088dda5-c5a0-4a40-8f9e-715ab50563ff(String)
2020-03-11 16:50:56.326 DEBUG 23240 --- [nio-8080-exec-8] c.t.mq.dao.AccountInfoDao.isExistTx      : <==      Total: 1
2020-03-11 16:50:56.326 DEBUG 23240 --- [nio-8080-exec-8] c.t.m.d.A.updateAccountBalance           : ==>  Preparing: update account_info set account_balance=account_balance+? where account_no=? 
2020-03-11 16:50:56.327 DEBUG 23240 --- [nio-8080-exec-8] c.t.m.d.A.updateAccountBalance           : ==> Parameters: -4.0(Double), 1(String)
2020-03-11 16:50:56.329 DEBUG 23240 --- [nio-8080-exec-8] c.t.m.d.A.updateAccountBalance           : <==    Updates: 1
2020-03-11 16:50:56.329 DEBUG 23240 --- [nio-8080-exec-8] c.topcheer.mq.dao.AccountInfoDao.addTx   : ==>  Preparing: insert into de_duplication values(?,now()); 
2020-03-11 16:50:56.330 DEBUG 23240 --- [nio-8080-exec-8] c.topcheer.mq.dao.AccountInfoDao.addTx   : ==> Parameters: 3088dda5-c5a0-4a40-8f9e-715ab50563ff(String)
2020-03-11 16:50:56.333 DEBUG 23240 --- [nio-8080-exec-8] c.topcheer.mq.dao.AccountInfoDao.addTx   : <==    Updates: 1
2020-03-11 16:51:14.092 DEBUG 23240 --- [pool-1-thread-1] c.t.mq.dao.AccountInfoDao.isExistTx      : ==>  Preparing: select count(1) from de_duplication where tx_no = ? 
2020-03-11 16:51:14.092 DEBUG 23240 --- [pool-1-thread-1] c.t.mq.dao.AccountInfoDao.isExistTx      : ==> Parameters: ef4bc8df-5afd-4dde-b2ee-a04c931d4211(String)
2020-03-11 16:51:14.093 DEBUG 23240 --- [pool-1-thread-1] c.t.mq.dao.AccountInfoDao.isExistTx      : <==      Total: 1
2020-03-11 16:52:14.095 DEBUG 23240 --- [pool-1-thread-1] c.t.mq.dao.AccountInfoDao.isExistTx      : ==>  Preparing: select count(1) from de_duplication where tx_no = ? 
2020-03-11 16:52:14.096 DEBUG 23240 --- [pool-1-thread-1] c.t.mq.dao.AccountInfoDao.isExistTx      : ==> Parameters: ef4bc8df-5afd-4dde-b2ee-a04c931d4211(String)
2020-03-11 16:52:14.099 DEBUG 23240 --- [pool-1-thread-1] c.t.mq.dao.AccountInfoDao.isExistTx      : <==      Total: 1
2020-03-11 16:52:56.326  INFO 23240 --- [lientSelector_1] RocketmqRemoting                         : closeChannel: close the connection to remote address[10.9.9.139:10909] result: true
2020-03-11 16:53:14.093 DEBUG 23240 --- [pool-1-thread-1] c.t.mq.dao.AccountInfoDao.isExistTx      : ==>  Preparing: select count(1) from de_duplication where tx_no = ? 
2020-03-11 16:53:14.094 DEBUG 23240 --- [pool-1-thread-1] c.t.mq.dao.AccountInfoDao.isExistTx      : ==> Parameters: ef4bc8df-5afd-4dde-b2ee-a04c931d4211(String)
2020-03-11 16:53:14.095 DEBUG 23240 --- [pool-1-thread-1] c.t.mq.dao.AccountInfoDao.isExistTx      : <==      Total: 1
2020-03-11 16:54:14.095 DEBUG 23240 --- [pool-1-thread-1] c.t.mq.dao.AccountInfoDao.isExistTx      : ==>  Preparing: select count(1) from de_duplication where tx_no = ? 
2020-03-11 16:54:14.096 DEBUG 23240 --- [pool-1-thread-1] c.t.mq.dao.AccountInfoDao.isExistTx      : ==> Parameters: ef4bc8df-5afd-4dde-b2ee-a04c931d4211(String)
2020-03-11 16:54:14.098 DEBUG 23240 --- [pool-1-thread-1] c.t.mq.dao.AccountInfoDao.isExistTx      : <==      Total: 1
2020-03-11 16:55:14.103 DEBUG 23240 --- [pool-1-thread-1] c.t.mq.dao.AccountInfoDao.isExistTx      : ==>  Preparing: select count(1) from de_duplication where tx_no = ? 
2020-03-11 16:55:14.104 DEBUG 23240 --- [pool-1-thread-1] c.t.mq.dao.AccountInfoDao.isExistTx      : ==> Parameters: ef4bc8df-5afd-4dde-b2ee-a04c931d4211(String)
2020-03-11 16:55:14.106 DEBUG 23240 --- [pool-1-thread-1] c.t.mq.dao.AccountInfoDao.isExistTx      : <==      Total: 1
2020-03-11 16:56:14.105 DEBUG 23240 --- [pool-1-thread-1] c.t.mq.dao.AccountInfoDao.isExistTx      : ==>  Preparing: select count(1) from de_duplication where tx_no = ? 
2020-03-11 16:56:14.107 DEBUG 23240 --- [pool-1-thread-1] c.t.mq.dao.AccountInfoDao.isExistTx      : ==> Parameters: ef4bc8df-5afd-4dde-b2ee-a04c931d4211(String)
2020-03-11 16:56:14.109 DEBUG 23240 --- [pool-1-thread-1] c.t.mq.dao.AccountInfoDao.isExistTx      : <==      Total: 1

会发现,张三的钱已经扣了,但是李四的钱一直没有,但是mq会一直循环的去消费这个消息

小结
可靠消息最终一致性就是保证消息从生产方经过消息中间件传递到消费方的一致性,本案例使用了RocketMQ作为消息中间件,
RocketMQ主要解决了两个功能:
1、本地事务与消息发送的原子性问题。
2、事务参与方接收消息的可靠性。

可靠消息最终一致性事务适合执行周期长且实时性要求不高的场景。引入消息机制后,同步的事务操作变为基于消息执行的异步操作, 避免了分布式事务中的同步阻塞操作的影响,并实现了两个服务的解耦。

2.5.最大努力通知

2.5.1.流程

最大努力通知也是一种解决分布式事务的方案,
下边是一个是充值的例子:
在这里插入图片描述

交互流程:
1、账户系统调用充值系统接口
2、充值系统完成支付处理向账户系统发起充值结果通知
若通知失败,则充值系统按策略进行重复通知
3、账户系统接收到充值结果通知修改充值状态。
4、账户系统未接收到通知会主动调用充值系统的接口查询充值结果。

通过上边的例子我们总结最大努力通知方案的目标:
目标:发起通知方通过一定的机制最大努力将业务处理结果通知到接收方。
具体包括:
1、有一定的消息重复通知机制。
因为接收通知方可能没有接收到通知,此时要有一定的机制对消息重复通知。
2、消息校对机制。
如果尽最大努力也没有通知到接收方,或者接收方消费消息后要再次消费,此时可由接收方主动向通知方查询消息
信息来满足需求。

最大努力通知与可靠消息一致性有什么不同?

1、解决方案思想不同
可靠消息一致性,发起通知方需要保证将消息发出去,并且将消息发到接收通知方,消息的可靠性关键由发起通知
方来保证。
最大努力通知,发起通知方尽最大的努力将业务处理结果通知为接收通知方,但是可能消息接收不到,此时需要接
收通知方主动调用发起通知方的接口查询业务处理结果,通知的可靠性关键在接收通知方。
2、两者的业务应用场景不同
可靠消息一致性关注的是交易过程的事务一致,以异步的方式完成交易。
最大努力通知关注的是交易后的通知事务,即将交易结果可靠的通知出去。
3、技术解决方向不同
可靠消息一致性要解决消息从发出到接收的一致性,即消息发出并且被接收到。
最大努力通知无法保证消息从发出到接收的一致性,只提供消息接收的可靠性机制。可靠机制是,最大努力的将消
息通知给接收方,当消息无法被接收方接收时,由接收方主动查询消息(业务处理结果)。

解决方案

通过对最大努力通知的理解,采用MQ的ack机制就可以实现最大努力通知。

方案1:

在这里插入图片描述

本方案是利用MQ的ack机制由MQ向接收通知方发送通知,
流程如下:
1、发起通知方将通知发给MQ。
使用普通消息机制将通知发给MQ。
注意:如果消息没有发出去可由接收通知方主动请求发起通知方查询业务执行结果。(后边会讲)

2、接收通知方监听 MQ。
3、接收通知方接收消息,业务处理完成回应ack。
4、接收通知方若没有回应ack则MQ会重复通知。
MQ会按照间隔1min、5min、10min、30min、1h、2h、5h、10h的方式,逐步拉大通知间隔 (如果MQ采用rocketMq,在broker中可进行配置),直到达到通知要求的时间窗口上限。
5、接收通知方可通过消息校对接口来校对消息的一致性。

方案2:

本方案也是利用MQ的ack机制,与方案1不同的是应用程序向接收通知方发送通知,如下图:

在这里插入图片描述

交互流程如下:
1、发起通知方将通知发给MQ。
使用可靠消息一致方案中的事务消息保证本地事务与消息的原子性,最终将通知先发给MQ。
2、通知程序监听 MQ,接收MQ的消息。
方案1中接收通知方直接监听MQ,方案2中由通知程序监听MQ。
通知程序若没有回应ack则MQ会重复通知。
3、通知程序通过互联网接口协议(如http、webservice)调用接收通知方案接口,完成通知。
通知程序调用接收通知方案接口成功就表示通知成功,即消费MQ消息成功,MQ将不再向通知程序投递通知消息。

4、接收通知方可通过消息校对接口来校对消息的一致性。

方案1和方案2的不同点:
1、方案1中接收通知方与MQ接口,即接收通知方案监听 MQ,此方案主要应用与内部应用之间的通知。
2、方案2中由通知程序与MQ接口,通知程序监听MQ,收到MQ的消息后由通知程序通过互联网接口协议调用接收通知方。此方案主要应用于外部应用之间的通知,例如支付宝、微信的支付结果通知。

RocketMQ实现最大努力通知型事务

业务说明

本实例通过RocketMq中间件实现最大努力通知型分布式事务,模拟充值过程。本案例有账户系统和充值系统两个微服务,其中账户系统的数据库是bank1数据库,其中有张三账户。充值系统的数据库使用bank1_pay数据库,记录了账户的充值记录。业务流程如下图:
在这里插入图片描述

交互流程如下:
1、用户请求充值系统进行充值。
2、充值系统完成充值将充值结果发给MQ。
3、账户系统监听MQ,接收充值结果通知,如果接收不到消息,MQ会重复发送通知。接收到充值结果通知账户系统增加充值金额。
4、账户系统也可以主动查询充值系统的充值结果查询接口,增加金额。

pay实现如下功能:
1、充值接口
2、充值完成要通知
3、充值结果查询接口
2)Dao

@Mapper
@Component
public interface AccountPayDao {
    @Insert("insert into account_pay(id,account_no,pay_amount,result) values(#{id},#{accountNo},#{payAmount},#{result})")
    int insertAccountPay(@Param("id") String id, @Param("accountNo") String accountNo, @Param("payAmount") Double pay_amount, @Param("result") String result);

    @Select("select id,account_no accountNo,pay_amount payAmount,result from account_pay where id=#{txNo}")
    AccountPay findByIdTxNo(@Param("txNo") String txNo);



}

3)Service

@Service
@Slf4j
public class AccountPayServiceImpl implements AccountPayService {

    @Autowired
    AccountPayDao accountPayDao;

    @Autowired
    RocketMQTemplate rocketMQTemplate;

    //插入充值记录
    @Override
    public AccountPay insertAccountPay(AccountPay accountPay) {
        int success = accountPayDao.insertAccountPay(accountPay.getId(), accountPay.getAccountNo(), accountPay.getPayAmount(), "success");
        if(success>0){
            //发送通知,使用普通消息发送通知
            accountPay.setResult("success");
            rocketMQTemplate.convertAndSend("topic_notifymsg",accountPay);
            return accountPay;
        }
        return null;
    }

    //查询充值记录,接收通知方调用此方法来查询充值结果
    @Override
    public AccountPay getAccountPay(String txNo) {
        AccountPay accountPay = accountPayDao.findByIdTxNo(txNo);
        return accountPay;
    }
}

4)Controller

@RestController
public class AccountPayController {

    @Autowired
    AccountPayService accountPayService;

    //充值
    @GetMapping(value = "/paydo")
    public AccountPay pay(AccountPay accountPay){
        //生成事务编号
        String txNo = UUID.randomUUID().toString();
        accountPay.setId(txNo);
        return accountPayService.insertAccountPay(accountPay);
    }

    //查询充值结果
    @GetMapping(value = "/payresult/{txNo}")
    public AccountPay payresult(@PathVariable("txNo") String txNo){
        return accountPayService.getAccountPay(txNo);
    }
}

bank1实现如下功能:
1、监听MQ,接收充值结果,根据充值结果完成账户金额修改。
2、主动查询充值系统,根据充值结果完成账户金额修改。
1)Dao

@Mapper
@Component
public interface AccountInfoDao {
    //修改账户金额
    @Update("update account_info set account_balance=account_balance+#{amount} where account_no=#{accountNo}")
    int updateAccountBalance(@Param("accountNo") String accountNo, @Param("amount") Double amount);


   //查询幂等记录,用于幂等控制
    @Select("select count(1) from de_duplication where tx_no = #{txNo}")
    int isExistTx(String txNo);

    //添加事务记录,用于幂等控制
    @Insert("insert into de_duplication values(#{txNo},now());")
    int addTx(String txNo);

}

2)AccountInfoService

@Service
@Slf4j
public class AccountInfoServiceImpl implements AccountInfoService {

    @Autowired
    AccountInfoDao accountInfoDao;

    @Autowired
    PayClient payClient;

    //更新账户金额
    @Override
    @Transactional
    public void updateAccountBalance(AccountChangeEvent accountChange) {
        //幂等校验
        if(accountInfoDao.isExistTx(accountChange.getTxNo())>0){
            return ;
        }
        int i = accountInfoDao.updateAccountBalance(accountChange.getAccountNo(), accountChange.getAmount());
        //插入事务记录,用于幂等控制
        accountInfoDao.addTx(accountChange.getTxNo());
    }

    //远程调用查询充值结果
    @Override
    public AccountPay queryPayResult(String tx_no) {

        //远程调用
        AccountPay payresult = payClient.payresult(tx_no);
        if("success".equals(payresult.getResult())){
            //更新账户金额
            AccountChangeEvent accountChangeEvent = new AccountChangeEvent();
            accountChangeEvent.setAccountNo(payresult.getAccountNo());//账号
            accountChangeEvent.setAmount(payresult.getPayAmount());//金额
            accountChangeEvent.setTxNo(payresult.getId());//充值事务号
            updateAccountBalance(accountChangeEvent);
        }
        return payresult;
    }
}

@FeignClient(value = "dtx-notifymsg-demo-pay",fallback = PayFallback.class)
public interface PayClient {

    //远程调用充值系统的接口查询充值结果
    @GetMapping(value = "/pay/payresult/{txNo}")
    public AccountPay payresult(@PathVariable("txNo") String txNo);
}

3)监听MQ

@Component
@Slf4j
@RocketMQMessageListener(topic = "topic_notifymsg",consumerGroup = "consumer_group_notifymsg_bank1")
public class NotifyMsgListener implements RocketMQListener<AccountPay> {

    @Autowired
    AccountInfoService accountInfoService;

    //接收消息
    @Override
    public void onMessage(AccountPay accountPay) {
        log.info("接收到消息:{}", JSON.toJSONString(accountPay));
        if("success".equals(accountPay.getResult())){
            //更新账户金额
            AccountChangeEvent accountChangeEvent = new AccountChangeEvent();
            accountChangeEvent.setAccountNo(accountPay.getAccountNo());
            accountChangeEvent.setAmount(accountPay.getPayAmount());
            accountChangeEvent.setTxNo(accountPay.getId());
            accountInfoService.updateAccountBalance(accountChangeEvent);
        }
        log.info("处理消息完成:{}", JSON.toJSONString(accountPay));
    }
}

4)Controller

@RestController
@Slf4j
public class AccountInfoController {

    @Autowired
    private AccountInfoService accountInfoService;

    //主动查询充值结果
    @GetMapping(value = "/payresult/{txNo}")
    public AccountPay result(@PathVariable("txNo") String txNo){
        AccountPay accountPay = accountInfoService.queryPayResult(txNo);
        return accountPay;
    }
}

测试场景

2020-03-12 18:54:21.442 DEBUG 8108 --- [io-9901-exec-10] c.t.m.d.AccountPayDao.insertAccountPay   : ==>  Preparing: insert into account_pay(id,account_no,pay_amount,result) values(?,?,?,?) 
2020-03-12 18:54:21.442 DEBUG 8108 --- [io-9901-exec-10] c.t.m.d.AccountPayDao.insertAccountPay   : ==> Parameters: 4db8776f-fc55-4190-b014-7caa21b77ec0(String), 1(String), 2000.0(Double), success(String)
2020-03-12 18:54:21.455 DEBUG 8108 --- [io-9901-exec-10] c.t.m.d.AccountPayDao.insertAccountPay   : <==    Updates: 1

2020-03-12 18:54:21.464  INFO 20964 --- [MessageThread_3] c.topcheer.eq.message.NotifyMsgListener  : 接收到消息:{"accountNo":"1","id":"4db8776f-fc55-4190-b014-7caa21b77ec0","payAmount":2000.0,"result":"success"}
2020-03-12 18:54:21.466 DEBUG 20964 --- [MessageThread_3] c.t.eq.dao.AccountInfoDao.isExistTx      : ==>  Preparing: select count(1) from de_duplication where tx_no = ? 
2020-03-12 18:54:21.466 DEBUG 20964 --- [MessageThread_3] c.t.eq.dao.AccountInfoDao.isExistTx      : ==> Parameters: 4db8776f-fc55-4190-b014-7caa21b77ec0(String)
2020-03-12 18:54:21.467 DEBUG 20964 --- [MessageThread_3] c.t.eq.dao.AccountInfoDao.isExistTx      : <==      Total: 1
2020-03-12 18:54:21.468 DEBUG 20964 --- [MessageThread_3] c.t.e.d.A.updateAccountBalance           : ==>  Preparing: update account_info set account_balance=account_balance+? where account_no=? 
2020-03-12 18:54:21.468 DEBUG 20964 --- [MessageThread_3] c.t.e.d.A.updateAccountBalance           : ==> Parameters: 2000.0(Double), 1(String)
2020-03-12 18:54:21.470 DEBUG 20964 --- [MessageThread_3] c.t.e.d.A.updateAccountBalance           : <==    Updates: 1
2020-03-12 18:54:21.470 DEBUG 20964 --- [MessageThread_3] c.topcheer.eq.dao.AccountInfoDao.addTx   : ==>  Preparing: insert into de_duplication values(?,now()); 
2020-03-12 18:54:21.470 DEBUG 20964 --- [MessageThread_3] c.topcheer.eq.dao.AccountInfoDao.addTx   : ==> Parameters: 4db8776f-fc55-4190-b014-7caa21b77ec0(String)
2020-03-12 18:54:21.473 DEBUG 20964 --- [MessageThread_3] c.topcheer.eq.dao.AccountInfoDao.addTx   : <==    Updates: 1
2020-03-12 18:54:21.486  INFO 20964 --- [MessageThread_3] c.topcheer.eq.message.NotifyMsgListener  : 处理消息完成:{"accountNo":"1","id":"4db8776f-fc55-4190-b014-7caa21b77ec0","payAmount":2000.0,"result":"success"}

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序猿老徐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值