彻底结决分布式事务
1、事务
1.1、本地数据库事务
事务是由单独单元的一个或者多个sql语句组成,在这个单元中,每个mysql语句是相互依赖的。而整个单独单元作为一个不可分割的整体,如果单元中某条sql执行失败或者产生错误,整个单元将会回滚,所有受到影响的数据将会返回到事务开始之前的状态;如果单元中所有的SQL语句均执行成功,则事务被顺利执行。
回顾一下数据库事务的四大特性 ACID:
A(Atomic) 原子性:一个事务不可再分割,要么都执行要么都不执行。
C(Consistency) 一致性:一个事务的执行会使数据从一个一致状态切换到另一个一致状态。
I(Isolation) 隔离性:一个事务的执行不收其他事务的干扰。
D(Durability) 持久性:一个事务一旦提交,则会永久的改变数据库的数据。
数据库事务在实现时会将一次事务涉及的所有操作全部纳入到一个不可分割的执行单元中,该执行单元中的所有操作要么全都成功,要么全都失败,只要其中任一操作执行失败,都会导致整个事务的回滚。
1.2、分布式事务
随着互联网的快速发展,软件系统由原来的单体应用转变为分布式应用,下图描述了单体应用向微服务的演变:
分布式系统会把一个应用系统拆分为可独立部署的多个服务,因此需要服务与服务之间远程协作才能完成事务操
作,这种分布式系统环境下由不同的服务之间通过网络远程协作完成事务称之为分布式事务,例如用户注册送积分
事务、创建订单减库存事务,银行转账事务等都是分布式事务。
我们知道本地事务依赖数据库本身提供的事务特性来实现,因此以下逻辑可以控制本地事务:
begin transaction;
//1.本地数据库操作:张三减少金额
//2.本地数据库操作:李四增加金额
commit transation;
但是在分布式环境下,会变成下边这样:
begin transaction;
//1.本地数据库操作:张三减少金额
//2.远程调用:让李四增加金额
commit transation;
可以设想,当远程调用让李四增加金额成功了,由于网络问题远程调用并没有返回,此时本地事务提交失败就回滚
了张三减少金额的操作,此时张三和李四的数据就不一致了。
因此在分布式架构的基础上,传统数据库事务就无法使用了,张三和李四的账户不在一个数据库中甚至不在一个应
用系统里,实现转账事务需要通过远程调用,由于网络问题就会导致分布式事务问题。
1.3、分布式事务产生的场景
1、典型的场景就是微服务架构 微服务之间通过远程调用完成事务操作。 比如:订单微服务和库存微服务,下单的同时订单微服务请求库存微服务减库存。 简言之:跨JVM进程产生分布式事务。
2、单体系统访问多个数据库实例 当单体系统需要访问多个数据库(实例)时就会产生分布式事务。 比如:用户信息和订单信息分别在两个MySQL实例存储,用户管理系统删除用户信息,需要分别删除用户信息及用户的订单信息,由于数据分布在不同的数据实例,需要通过不同的数据库链接去操作数据,此时产生分布式事务。 简言之:跨数据库实例产生分布式事务。
3、多服务访问同一个数据库实例 。比如:订单微服务和库存微服务即使访问同一个数据库也会产生分布式事务,原因就是跨JVM进程,两个微服务持有了不同的数据库链接进行数据库操作,此时产生分布式事务。
2、分布式事务基础理论
2.1、CAP理论
CAP是 Consistency、Availability、Partition tolerance三个词语的缩写,分别表示一致性、可用性、分区容忍性。
2.1.1、C-Consistency:
一致性是指写操作后的读操作可以读取到最新的数据状态,当数据分布在多个结点上,从任意结点读取到的数据都是最新的状态。
如何实现一致性?
1、写入数据库后要将数据同步到从数据库中。
2、写入主数据库后,在向从数据库同步期间要将从数据库锁定,待同步完成后再释放锁,以免在新数据写入成功后,向从数据库查询到旧的数据。
分布式系统一致性的特点:
1、由于存在数据同步问题,写操作的响应会有一定的延迟。
2、为了保证数据一致性会对资源进行暂时的锁定,待数据同步完成后释放锁定资源。
3、如果请求数据同步失败的节点则会返回错误信息,一定不会返回旧数据。
2.1.2、A-Availability:
可用性是指任何事务操作都可以得到响应结果,且不会出现响应超时或者响应错误。
如何实现可用性?
1、写入主数据库后要将数据同步到从数据库。
2、由于要保证从数据库的可用性,不可将从数据库中的资源进行锁定。
3、即时数据还没有同步过来,从数据库也要返回要查询的数据,哪怕是旧数据,如果连旧数据也没有则可以按照
约定返回一个默认信息,但不能返回错误或响应超时 。
分布式系统可用性的特点:
1、 所有请求都有响应,且不会出现响应超时或响应错误 。
2.1.3、P - Partition tolerance:
通常分布式系统的各各结点部署在不同的子网,这就是网络分区,不可避免的会出现由于网络问题而导致结点之间通信失败,此时仍可对外提供服务,这叫分区容忍性 。
如何实现分区容忍性?
1、尽量使用异步取代同步操作,例如使用异步方式将数据从主数据库同步到从数据,这样结点之间能有效的实现
松耦合。
2、添加从数据库结点,其中一个从结点挂掉其它从结点提供服务 。
分布式分区容忍性的特点:
1、分区容忍性分是布式系统具备的基本能力 。
2.1.4、CAP的组合方式
1)AP:
放弃一致性,追求分区容忍性和可用性。这是很多分布式系统设计时的选择。
例如:
上边的商品管理,完全可以实现AP,前提是只要用户可以接受所查询的到数据在一定时间内不是最新的即可。
通常实现AP都会保证最终一致性,后面讲的BASE理论就是根据AP来扩展的,一些业务场景 比如:订单退款,今
日退款成功,明日账户到账,只要用户可以接受在一定时间内到账即可。
2)CP:
放弃可用性,追求一致性和分区容错性,我们的zookeeper其实就是追求的强一致,又比如跨行转账,一次转账请
求要等待双方银行系统都完成整个事务才算完成。
3)CA:
放弃分区容忍性,即不进行分区,不考虑由于网络不通或结点挂掉的问题,则可以实现一致性和可用性。那么系统
将不是一个标准的分布式系统,我们最常用的关系型数据就满足了CA 。
2.1.5、总结
通过上面我们已经学习了CAP理论的相关知识,CAP是一个已经被证实的理论:一个分布式系统最多只能同时满足
一致性(Consistency)、可用性(Availability)和分区容忍性(Partition tolerance)这三项中的两项。它可以作为我们进行架构设计、技术选型的考量标准。对于多数大型互联网应用的场景,结点众多、部署分散,而且现在的集群规模越来越大,所以节点故障、网络故障是常态,而且要保证服务可用性达到N个9(99.99…%),并要达到良好的响应性能来提高用户体验,因此一般都会做出如下选择:保证P和A,舍弃C强一致,保证最终一致性。
2.2、BASE理论
2.2.1、理解强一致性和最终一致性
CAP理论告诉我们一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容忍
性(Partition tolerance)这三项中的两项,其中AP在实际应用中较多,AP即舍弃一致性,保证可用性和分区容忍性,但是在实际生产中很多场景都要实现一致性,比如前边我们举的例子主数据库向从数据库同步数据,即使不要一致性,但是最终也要将数据同步成功来保证数据一致,这种一致性和CAP中的一致性不同,CAP中的一致性要求在任何时间查询每个结点数据都必须一致,它强调的是强一致性,但是最终一致性是允许可以在一段时间内每个结点的数据不一致,但是经过一段时间每个结点的数据必须一致,它强调的是最终数据的一致性。
2.2.2、Base理论介绍
BASE 是 Basically Available(基本可用)、**Soft state(软状态)**和 **Eventually consistent (最终一致性)**三个短语的缩写。BASE理论是对CAP中AP的一个扩展,通过牺牲强一致性来获得可用性,当出现故障允许部分不可用但要保证核心功能可用,允许数据在一段时间内是不一致的,但最终达到一致状态。满足BASE理论的事务,我们称之为“柔性事务”。
- 基本可用:分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。如,电商网站交易付款出
现问题了,商品依然可以正常浏览。 - 软状态:由于不要求强一致性,所以BASE允许系统中存在中间状态(也叫软状态),这个状态不影响系统可用
性,如订单的"支付中"、“数据同步中”等状态,待数据最终一致后状态改为“成功”状态。 - 最终一致:最终一致是指经过一段时间后,所有节点数据都将会达到一致。如订单的"支付中"状态,最终会变
为“支付成功”或者"支付失败",使订单状态与实际交易结果达成一致,但需要一定时间的延迟、等待。
3、分布式事务解决防范之2PC(两阶段提交)
3.1、什么是2PC?
2PC即两阶段提交协议,是将整个事务流程分为两个阶段,准备阶段(Prepare phase)、提交阶段(commit
phase),2是指两个阶段,P是指准备阶段,C是指提交阶段。
- 准备阶段(Prepare phase):事务管理器给每个参与者发送Prepare消息,每个数据库参与者在本地执行事务,并写本地的Undo/Redo日志,此时事务没有提交。(Undo日志是记录修改前的数据,用于数据库回滚,Redo日志是记录修改后的数据,用于提交事务后写入数据文件) 。
- 提交阶段(commit phase):如果事务管理器收到了参与者的执行失败或者超时消息时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据事务管理器的指令执行提交或者回滚操
作,并释放事务处理过程中使用的锁资源。注意:必须在最后阶段释放锁资源。
3.2、2PC存在的问题
1、性能问题
无论在第一阶段的过程还是在第二阶段,所有的参与者资源和协调者资源都是被锁住的,只有所有节点准备完毕,事务协调者才会通知进行全局提交,参与者进行本地事务提交后才会释放资源,这样的过程比较漫长,对性能影响较大。
2、单点故障问题
由于协调者的重要性,一旦协调者发生故障。参与者会一致阻塞下去,尤其在第二阶段,协调者发生故障,那么所有的参与者还处于资源锁定状态,无法继续完成事务操作。(虽然协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)。
2PC出现单点问题的三种情况:
(1)协调者正常,参与者宕机
由于协调者无法收集所有参与者的反馈,会陷入阻塞情况。
解决方案:引入超时机制,如果协调者在超过指定时间还没有收到参与者的反馈,事务就失败,向所有节点发送回滚事务的请求。
(2)协调者宕机,参与者正常
无论出于哪个阶段,由于协调者宕机,无法发送提交请求,所有处于执行了操作但是未提交的参与者都会陷入阻塞情况。
解决方案:引入协调者备份,同时协调者需记录操作日志,当检测到协调者宕机一段时间后,协调者备份取代协调者,并读取操作日志,向所有参与者询问状态。
(3)协调者和参与者都宕机
1、发生在第一阶段:因为第一阶段,所有参与者都没有真正执行commit,所以只需要重新在剩余的参与者中重新选出一个协调者,新的协调者再重新执行第一阶段和第二阶段即可。
2、发生在第二阶段 并且 挂了的参与者在挂掉之前没有收到协调者的指令。也就是上面的第4步挂了,这是可能协调者还没有发送第4步就挂了。这种情形下,新的协调者重新执行第一阶段和第二阶段操作。
3、发生在第二阶段 并且 有部分参与者已经执行完commit操作。就好比这里订单服务A和支付服务B都收到协调者 发送的commit信息,开始真正执行本地事务commit,但突发情况,Acommit成功,B确挂了。这个时候目前来讲数据是不一致的。虽然这个时候可以再通过手段让他和协调者通信,再想办法把数据搞成一致的,但是,这段时间内他的数据状态已经是不一致的了! 2PC 无法解决这个问题。
4、分布式事务解决防范之3PC(三阶段提交)
4.1、什么是3PC?
三阶段提交协议(3PC)主要是为了解决两阶段提交协议的阻塞问题,2PC存在的问题是当协调者宕机时,参与者不能做出最后的选择。因此参与者可能在协调者恢复之前保持阻塞。三阶段协议是二阶段提交的改进版本。
与二阶段提交不同的是,三阶段提交有两个改动点。
-
引入超时机制
-
3PC把2PC的准备阶段再次一分为二,三阶段提交分别为CanCommit、PreCommit、DoCommit三个阶段。
1、CanCommit阶段
之前2PC的一阶段是本地事务执行结束后,最后不Commit,等其他服务都结束并返回yes后,由协调节点发起commit才真正执行commit,而这里的CanCommit指的是尝试获取数据库锁,如果可以,则返回yes。
这阶段主要分为2步
-
事务询问 协调者 向 参与者 发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待 参与者 的响应。
-
响应反馈 参与者 接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No。
2、PreCommit阶段
在阶段一中,如果所有的参与者都返回Yes的话,那么就会进入PreCommit阶段进行事务预提交。这里的PreCommit阶段 跟上面的第一阶段是差不多的,只不过这里 协调者和参与者都引入了超时机制 (2PC中只有协调者可以超时,参与者没有超时机制)。
3、DoCommit阶段
跟2PC提交阶段差不多。
4.2、总结
相比较2PC而言,3PC对于协调者(Coordinator)和参与者(Partcipant)都设置了超时时间,而2PC只有协调者才拥有超时机制。这解决了一个什么问题呢?
这个优化点,主要是避免了参与者在长时间无法与协调者节点通讯(协调者挂掉了)的情况下,无法释放资源的问题,因为参与者自身拥有超时机制会在超时后,自动进行本地commit从而进行释放资源。而这种机制也侧面降低了整个事务的阻塞时间和范围。
另外,通过CanCommit、PreCommit、DoCommit三个阶段的设计,相较于2PC而言,多设置了一个缓冲阶段保证了在最后提交阶段之前各参与节点的状态是一致的。
以上就是3PC相对于2PC的一个提高(相对缓解了2PC中的前两个问题),但是3PC依然没有完全解决数据不一致的问题。
5、分布式事务解决防范之TCC
5.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的3个阶段:
1、Try阶段:做业务检查(一致性)及资源预留(隔离),此阶段仅是一个初步操作,它和后续的Confirm 一起才能
真正构成一个完整的业务逻辑。
2、Confirm 阶段:做确认提交 ,Try阶段所有分支事务执行成功后开始执行 Confirm。通常情况下,采用TCC则
认为 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。若Confirm阶段真的出错了,需引入重试机制或人工处理。
3、Cancel 阶段:在业务执行错误需要回滚的状态下执行分支事务的业务取消,预留资源释放。通常情况下,采
用TCC则认为Cancel阶段也是一定成功的。若Cancel阶段真的出错了,需引入重试机制或人工处理 。
TM在发起全局事务时生成全局事务记录,全局事务ID贯穿整个分布式事务调用链条,用来记录事务上下文,
追踪和记录状态,由于Confirm 和cancel失败需进行重试,因此需要实现为幂等,幂等性是指同一个操作无论请求
多少次,其结果都相同。
5.2、TCC需要注意的三种异常
1、空回滚
在没有调用 TCC 资源 Try 方法的情况下,调用了二阶段的 Cancel 方法,Cancel 方法需要识别出这是一个空回
滚,然后直接返回成功。
出现原因是当一个分支事务所在服务宕机或网络异常,分支事务调用记录为失败,这个时候其实是没有执行Try阶
段,当故障恢复后,分布式事务进行回滚则会调用二阶段的Cancel方法,从而形成空回滚。
解决思路是关键就是要识别出这个空回滚。思路很简单就是需要知道一阶段是否执行,如果执行了,那就是正常回
滚;如果没执行,那就是空回滚。前面已经说过TM在发起全局事务时生成全局事务记录,全局事务ID贯穿整个分
布式事务调用链条。再额外增加一张分支事务记录表,其中有全局事务 ID 和分支事务 ID,第一阶段 Try 方法里会
插入一条记录,表示一阶段执行了。Cancel 接口里读取该记录,如果该记录存在,则正常回滚;如果该记录不存
在,则是空回滚。
2、幂等
通过前面介绍已经了解到,为了保证TCC二阶段提交重试机制不会引发数据不一致,要求 TCC 的二阶段 Try、
Confirm 和 Cancel 接口保证幂等,这样不会重复使用或者释放资源。如果幂等控制没有做好,很有可能导致数据
不一致等严重问题。
解决思路在上述“分支事务记录”中增加执行状态,每次执行前都查询该状态。
3、悬挂
悬挂就是对于一个分布式事务,其二阶段 Cancel 接口比 Try 接口先执行。
出现原因是在 RPC 调用分支事务try时,先注册分支事务,再执行RPC调用,如果此时 RPC 调用的网络发生拥堵,
通常 RPC 调用是有超时时间的,RPC 超时以后,TM就会通知RM回滚该分布式事务,可能回滚完成后,RPC 请求
才到达参与者真正执行,而一个 Try 方法预留的业务资源,只有该分布式事务才能使用,该分布式事务第一阶段预
留的业务资源就再也没有人能够处理了,对于这种情况,我们就称为悬挂,即业务资源预留后没法继续处理。
解决思路是如果二阶段执行完成,那一阶段就不能再继续执行。在执行一阶段事务时判断在该全局事务下,“分支
事务记录”表中是否已经有二阶段事务记录,如果有则不执行Try。
5.3、总结
如果拿TCC事务的处理流程与2PC两阶段提交做比较,2PC通常都是在跨库的DB层面,而TCC则在应用层面的处
理,需要通过业务逻辑来实现。这种分布式事务的实现方式的优势在于,可以让应用自己定义数据操作的粒度,使
得降低锁冲突、提高吞吐量成为可能。
而不足之处则在于对应用的侵入性非常强,业务逻辑的每个分支都需要实现try、confirm、cancel三个操作。此
外,其实现难度也比较大,需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。
6、分布式事务解决方案之可靠消息最终一致性
6.1、什么是可靠消息最终一致性?
可靠消息最终一致性方案是指当事务发起方执行完成本地事务后并发出一条消息,事务参与方(消息消费者)一定能
够接收消息并处理事务成功,此方案强调的是只要消息发给事务参与方最终事务要达到一致。
此方案是利用消息中间件完成,如下图:
事务发起方(消息生产方)将消息发给消息中间件,事务参与方从消息中间件接收消息,事务发起方和消息中间件
之间,事务参与方(消息消费方)和消息中间件之间都是通过网络通信,由于网络通信的不确定性会导致分布式事
务问题。
因此可靠消息最终一致性方案要解决以下几个问题:
1、本地事务与消息发送的原子性问题
本地事务与消息发送的原子性问题即:事务发起方在本地事务执行成功后消息必须发出去,否则就丢弃消息。即实
现本地事务和消息发送的原子性,要么都成功,要么都失败。本地事务与消息发送的原子性问题是实现可靠消息最
终一致性方案的关键问题。
2、事务参与方接收消息的可靠性
事务参与方必须能够从消息队列接收到消息,如果接收消息失败可以重复接收消息。
3、消息重复消费问题
由于网络2的存在,若某一个消费节点超时但是消费成功,此时消息中间件会重复投递此消息,就导致了消息的重
复消费。
要解决消息重复消费的问题就要实现事务参与方的方法幂等性。
6.2、解决方案
6.2.1本地消息表方案
本地消息表这个方案最初是eBay提出的,此方案的核心是通过本地事务保证数据业务操作和消息的一致性,然后
通过定时任务将消息发送至消息中间件,待确认消息发送给消费方成功再将消息删除。
1、用户注册
用户服务在本地事务新增用户和增加 ”积分消息日志“。(用户表和消息表通过本地事务保证一致)
下边是伪代码 :
begin transaction;
//1.新增用户
//2.存储积分消息日志
commit transation;
这种情况下,本地数据库操作与存储积分消息日志处于同一个事务中,本地数据库操作与记录消息日志操作具备原
子性。
2、定时任务扫描日志
如何保证将消息发送给消息队列呢?
经过第一步消息已经写到消息日志表中,可以启动独立的线程,定时对消息日志表中的消息进行扫描并发送至消息
中间件,在消息中间件反馈发送成功后删除该消息日志,否则等待定时任务下一周期重试。
3、消费消息
如何保证消费者一定能消费到消息呢?
这里可以使用MQ的ack(即消息确认)机制,消费者监听MQ,如果消费者接收到消息并且业务处理完成后向MQ
发送ack(即消息确认),此时说明消费者正常消费消息完成,MQ将不再向消费者推送消息,否则消费者会不断重试向消费者来发送消息。
积分服务接收到”增加积分“消息,开始增加积分,积分增加成功后向消息中间件回应ack,否则消息中间件将重复
投递此消息。
由于消息会重复投递,积分服务的”增加积分“功能需要实现幂等性。
6.2.2、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);
6.2.3、小结
可靠消息最终一致性就是保证消息从生产方经过消息中间件传递到消费方的一致性,本案例使用了RocketMQ作为
消息中间件,RocketMQ主要解决了两个功能:
1、本地事务与消息发送的原子性问题。
2、事务参与方接收消息的可靠性。
可靠消息最终一致性事务适合执行周期长且实时性要求不高的场景。引入消息机制后,同步的事务操作变为基于消
息执行的异步操作, 避免了分布式事务中的同步阻塞操作的影响,并实现了两个服务的解耦。
new TransactionMQProducer("ProducerGroup");
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
//设置TransactionListener实现
producer.setTransactionListener(transactionListener);
//发送事务消息
SendResult sendResult = producer.sendMessageInTransaction(msg, null);
6.2.3、小结
可靠消息最终一致性就是保证消息从生产方经过消息中间件传递到消费方的一致性,本案例使用了RocketMQ作为
消息中间件,RocketMQ主要解决了两个功能:
1、本地事务与消息发送的原子性问题。
2、事务参与方接收消息的可靠性。
可靠消息最终一致性事务适合执行周期长且实时性要求不高的场景。引入消息机制后,同步的事务操作变为基于消
息执行的异步操作, 避免了分布式事务中的同步阻塞操作的影响,并实现了两个服务的解耦。
分布式事务之阿里开源Seata
Seata的分布式事务解决方案是业务层面的解决方案,业务层上无需关心分布式事务机制的约束,它将给我们的微服务架构带来质的提升,只依赖于单台数据库的事务能力。
Seata 内部定义了 3个模块来处理全局事务和分支事务的关系和处理过程,这三个组件分别是:
1、Transaction Coordinator (TC): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。
2、Transaction Manager ™: 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
3、Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
其中,TM是一个分布式事务的发起者和终结者,TC负责维护分布式事务的运行状态,而RM则负责本地事务的运行。如下图所示:
下面是一个分布式事务在Seata中的执行流程:
1)TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。
2)XID 在微服务调用链路的上下文中传播。
3)RM 向 TC 注册分支事务,接着执行这个分支事务并提交(重点:RM在第一阶段就已经执行了本地事务的提交/回滚),最后将执行结果汇报给TC。
4)TM 根据 TC 中所有的分支事务的执行情况,发起全局提交或回滚决议。
5)TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
分支事务为什么能直接提交?
Seata能够在第一阶段直接提交事务,是因为Seata框架为每一个RM维护了一张UNDO_LOG表(这张表需要客户端自行创建),其中保存了每一次本地事务的回滚数据。因此,二阶段的回滚并不依赖于本地数据库事务的回滚,而是RM直接读取这张UNDO_LOG表,并将数据库中的数据更新为UNDO_LOG中存储的历史数据。
Seata的执行流程
1)首先TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。
2)XID 在微服务调用链路的上下文中传播。
3)RM 开始执行这个分支事务,RM首先解析这条SQL语句,生成对应的UNDO_LOG记录。
下面是一条UNDO_LOG中的记录:
{
"branchId": 641789253,
"undoItems": [{
"afterImage": {
"rows": [{
"fields": [{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "GTS"
}, {
"name": "since",
"type": 12,
"value": "2014"
}]
}],
"tableName": "product"
},
"beforeImage": {
"rows": [{
"fields": [{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "TXC"
}, {
"name": "since",
"type": 12,
"value": "2014"
}]
}],
"tableName": "product"
},
"sqlType": "UPDATE"
}],
"xid": "xid:xxx"
}
可以看到,UNDO_LOG表中记录了分支ID,全局事务ID,以及事务执行的redo和undo数据以供二阶段恢复。
4)RM在同一个本地事务中执行业务SQL和UNDO_LOG数据的插入。在提交这个本地事务前,RM会向TC申请关于这条记录的全局锁。如果申请不到,则说明有其他事务也在对这条记录进行操作,因此它会在一段时间内重试,重试失败则回滚本地事务,并向TC汇报本地事务执行失败。如下图所示:
5)RM在事务提交前,申请到了相关记录的全局锁,因此直接提交本地事务,并向TC汇报本地事务执行成功。此时全局锁并没有释放,全局锁的释放取决于二阶段是提交命令还是回滚命令。
6)TC根据所有的分支事务执行结果,向RM下发提交或回滚命令。
7)RM如果收到TC的提交命令,首先立即释放相关记录的全局锁,然后把提交请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。异步队列中的提交请求真正执行时,只是删除相应UNDO LOG 记录而已。
8)RM如果收到TC的回滚命令,则会开启一个本地事务,通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。将 UNDO LOG 中的后镜与当前数据进行比较:
如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理。
否则,根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句并执行,然后提交本地事务达到回滚的目的。
最后释放相关记录的全局锁。
上面的方案是Seata的默认模式,称为AT模式,它是类似于 XA 方案的两段式提交方案,并且是对业务无侵入,但是这种机制依然是需要依赖数据库本地事务的 ACID 特性。