分布式事务详解
2021.08.02 阅读量次概览
什么是事务?举个例子,你去超市买东西,“一手交钱,一手交货"就是一个事务的例子。 交钱和交货必须同时成功,事务才算成功,其中有一个环节失败,事务将会撤销所有已成功的活动。 所以事务可以看作是一次重大的活动,它由不同的小活动组成,这些活动要么全部成功,要么全部失败。
在计算机系统中,更多的是通过关系型数据库来控制事务,这是利用数据库本身的事务特性来实现的,因此叫做数据库事务。 由于应用程序主要靠关系型数据库来控制事务,而数据库通常和应用在同一个服务器上,所以基于关系型数据库的事务又叫做本地事务。
数据库事务的四大特性ACID:
- A(
Atomic
):原子性,构成事务的所有操作,要么都执行完,要么全不执行,不可能出现部分成功部分失败的情况。 - C(
Consistency
):一致性,在事务执行前后,数据库的一致性约束没有被破坏。比如:张三向李四转100元,转账前和转账后的数据正确状态这叫一致性,如果出现张三转100元,李四账户没有增加100元这就出现了数据错误,就没有达到一致性。 - I(
Isolation
):隔离性,数据库中事务都是并发的,隔离性是指并发的两个事务的执行互不干扰,一个事务不能看到其他事务运行过程的中间状态。通过配置事务的隔离级别可以避免脏读、重复读等问题。 - D(
Durability
):持久性,事务执行完毕后,该事务对数据的更改会被持久化到数据库,且不会被回滚。
数据库事务在实现时,会将一次事务涉及的所有操作全部纳入到一个不可分割的执行单元,该执行单元中的所有操作要么都成功,要么都失败,只要其中任一操作执行失败,都将会导致事务回滚。
随着互联网的发展,软件系统由原来的单体应用转变为分布式应用。分布式系统会把一个应用系统拆分为多个可独立部署的程序,因此需要服务与服务之间的远程协作才能完成事务操作,这种分布式系统环境下由不同服务之间通过网络协作完成的事务被称为分布式事务。 例如,用户注册送积分、创建订单减少库存、银行转账事务等。
本地事务与分布式事务:
- 本地事务:
begin transation; // 1.本地数据库操作:张三减少金额 // 2.本地数据库操作:李四增加金额 commit transation;
- 分布式事务:
begin transation; // 1.本地数据库操作:张三减少金额 // 2.远程调用:李四增加金额 commit transation;
在2.分布式事务
中如果李四增加金额成功,但是由于网络原因,远程调用并没有返回,此时本地事务提交失败就会回滚张三减少金额的操作,张三李四的金额就不一致了。
因此在分布式架构的基础上,传统数据库事务就无法使用了,张三和李四的账户不在同一个数据库中甚至不在一个系统中,实现转账事务需要通过远程调用,但由于网络的问题就会导致分布式事务问题。
分布式事务产生场景:
- 最典型的是微服务架构,微服务之间通过远程调用完成事务操作。比如,订单微服务和库存微服务,下单的同时订单微服务请求库存微服务减少库存。简而言之,就是跨JVM进程产生的分布式事务。
- 单体系统访问多个数据库实例,就会产生分布式事务。比如,用户和订单信息分别存储在两个MySQL中,用户管理系统删除用户信息,需要分别删除用户信息及用户的订单信息,由于数据分布在不同的数据库,就需要通过不同的数据库连接去操作数据,此时产生分布式事务。
- 多个微服务访问同一个数据库实例。比如,订单微服务和库存微服务即使访问同一个数据库也会产生分布式事务,原因是跨JVM进程,两个微服务持有了不同的数据库连接进行数据库操作,此时产生分布式事务。
分布式事务基础
分布式事务之所以叫做分布式事务,是因为提供服务的各个结点分布在不同的机器上,相互之间通过网络交互。不能因为有网络问题就导致整个系统无法提供服务,网络因素成了分布式事务的考量标准之一。因此,分布式事务需要更进一步的理论支持。
CAP理论
CAP是Consistency
、Availability
、Partition tolerance
三个词语的缩写,分别表示一致性、可用性、分区容忍性。
为了方便对CAP理论的理解,结合电商系统中的一些业务场景来理解CAP,如下图是商品信息管理的执行流程:
- 商品服务请求主数据库写入商品信息(增、删、改);
- 主数据库向商品服务响应写入成功;
- 商品服务请求从数据库获取商品信息;
- C(
Consistency
):一致性是指写操作后的读操作可以读取到最新的数据状态,当数据分布在多个结点上,从任意结点读取到的数据都是最新状态。 上图中,商品的信息的读写要满足一致性就是要实现如下目标:1.商品服务写入主数据库成功,则向从数据查询新数据也成功;2.商品服务写入主数据库失败,则向从数据查询新数据也失败; 这里一致性指的就是主从数据库数据的一致性。在写入主数据库后,将数据同步到从数据库,并在同步期间锁定从数据库,来防止在数据同步完成之前查询到旧数据。 分布式系统中,一致性的特点在于写操作的响应可能会有延迟,因为数据库同步的过程中需要暂时锁定资源,需要待同步完成后再释放锁定的资源。 - A(
Availability
):可用性是指任何事务操作都可以获得响应结果,且不会出现响应超时或响应错误。 上图中,商品信息的读取满足可用性就是要实现如下目标:1.从数据库接收到数据查询的请求则立即能够响应数据的查询结果;2.从数据库不允许出现响应超时或响应错误的情况; 写入主数据库后要将数据数据同步到从数据库,但由于要保证从数据库的可用性,不可将从数据库中的资源进行锁定。 即使数据还没有从主数据库同步过来,从数据库也要返回查询的数据,哪怕是旧数据,如果连旧数据也没有则可以按照约定返回一个默认信息,但不能返回错误或超时。 分布式系统可用性的特点就是所有请求都要有响应,且不会出现响应超时或响应错误。 - P(
Partition tolerance
):分布式系统的各个结点部署在不同的子网,这就是网络分区,不可避免的会出现由于网络问题而导致结点之间的通信失败,此时仍可以对外提供服务,这就是分区容忍性。 上图中,商品信息的读写满足分区容忍性就是要实现如下目标:1.主数据库向从数据库同步数据失败,不影响读写操作;2.其中一个结点挂掉不影响另外一个结点提供服务; 实现分区容忍性尽量使用异步取代同步操作,例如使用异步的方法将数据从数据库同步到从数据库,这样的结点之间才能有效的解耦合。添加从数据库结点,其中一个从数据结点挂掉其他从结点来提供服务。分区容忍性是分布式系统具备的基本的能力。
在所有分布式事务场景中不会同时具备CAP三个特征,因为在具备了P的前提下C和A是不能共存的。
例如,下图满足了P分区容忍性
- 主数据库通过网络向从数据库同步数据,可以认为是主从数据库部署在不同的网络分区,通过网络进行交互
- 当主数据库和从数据库之间网络出现问题,不影响主数据库和从数据库对外提供服务
- 其中一台结点挂掉后,不影响另外一个结点对外提供服务
在分区容忍性存在的前提下,一致性和可用性存在矛盾:
- 如果要实现一致性,则必须保证数据一致性,在数据同步的时候防止向从数据库查询不一致的数据则需要将从数据库数据锁定,待同步完成后解锁,如果同步失败,从数据库必须要返回错误信息或者超时信息。
- 如果要实现可用性则必须保证数据可用性,即不管任何时候都可以向从数据库查询数据,且不会响应超时信息或错误信息。
在分布式的环境下,一致性和可用性只能存在一种,即AP、CP:
- AP:放弃一致性,追求分区容忍性和可用性。这是很多分布式系统设计时的选择。
- CP:放弃可用性,追求一致性和分区容忍性。
zookeeper
就是一个追求强一致性的例子;例如,跨行转账,一次转账请求需要等待双方银行都完成整个事务才算完成。
如果不在分布式的环境下,一致性和可用性其实是不矛盾的:
- CA:放弃分区容忍性,即不进行分区,不考虑网络结点不通畅或者结点挂掉的情况,则可以实现一致性和可用性。那么系统将不是一个标准的分布式系统,最常用的关系型数据库就满足了CA。
CAP是一个已经被证实的理论,一个分布式系统最多只能同时满足一致性、可用性、分区容忍性中的两种。它可以作为我们进行架构设计、技术选型的考量标准。对于大型互联网应用场景来说,结点众多、部署分散,而且现在的集群规模越来越大,所以结点故障、网络故障是常态,而且要保证服务可用性达到N个9(99.99..%),并要达到良好的响应性能来提高用户的体验。 因此一般都会做出以下选择,保证可用性和分区容忍性即AP,舍弃C,强一致性,保证最终一致性。
BASE理论
CAP理论说明一个分布式系统最多能同时满足一致性、可用性、分区容忍性这三项中的两项。其中AP在实际应用中比较多,AP舍弃一致性,保证可用性和分区容忍性,但是在实际生产中很多场景都要实现一致性,比如主数据库向从数据库同步数据,即使不要一致性,但是最终也要将数据同步成功来保证数据一致,这种一致性和CAP中的一致性不同。 CAP中的一致性要求在任何时间查询每个结点数据都必须保证一致,但是最终一致性是允许在一段时间内每个结点数据不一致,经过一段时间后每个结点的数据必须一致,它强调的是数据的最终一致性。
BASE理论强调的就是最终一致性,BASE是Basically Available
(基本可用)、Soft state
(软状态)、Eventually consistent
(最终一致性) 这三个短语的缩写。
BASE理论是对于CAP中AP的一个扩展,通过牺牲强一致性来获得可用性,当出现故障允许的部分不可用但要保证核心功能可用,允许数据在一段时间内是不一致的,但最终达到一致的状态。满足BASE理论的事务,我们称之为柔性事务。
- 基本可用:分布式系统出现故障时,允许损失部分可用的功能,保证核心功能可用。例如,电商网站交易付款出现问题,商品依然可以正常浏览。
- 软状态:由于不要求强一致性,所以BASE允许系统中存在中间状态(软状态),这个状态不影响系统的可用性。例如,订单的"支付中、数据同步中"等状态,待数据最终一致后状态改为"成功"状态。
- 最终一致性:最终一致是指经过一段时间后,所有的结点数据将会达到一致。例如,订单的"支付中"状态,最终会变为"支付成功"或"支付失败”,使订单状态与交易结果达成一致,但需要一定的时间等待。
分布式事务解决方案
针对不同的分布式场景常见的解决方案有2PC、TCC、可靠消息最终一致性、最大努力通知这几种。
解决方案 | 描述 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
2PC (Two-Phase Commit) | 一种经典的分布式事务协议,确保所有参与者要么全部提交,要么全部回滚。 | 确保了事务的原子性,事务一致性较强。 | 协调者崩溃或参与者崩溃可能导致事务不一致或阻塞。 | 需要强一致性保证的场景。 |
TCC (Try-Confirm-Cancel) | 通过预留资源、确认提交或取消操作来保证分布式事务的一致性。 | 相比2PC更具弹性,支持更高的可用性和容错性。 | 实现复杂,需处理预留资源和取消操作的逻辑。 | 适用于需要灵活处理和补偿的场景。 |
可靠消息最终一致性 | 通过消息中间件保证消息可靠投递,最终实现数据一致性。 | 高可用性,适用于网络中断和系统崩溃情况,能够实现最终一致性。 | 消息处理可能会出现延迟,需要处理重复消息和消息丢失等问题。 | 数据一致性需求不那么强,但需容忍最终一致性的场景。 |
最大努力通知 | 通过尽最大努力发送消息,尝试确保消息到达,但不保证最终一致性。 | 实现简单,性能开销小,适用于不严格要求一致性的场景。 | 消息可能会丢失或重复处理,无法保证一致性。 | 不需要严格一致性的场景,如日志记录等。 |
2PC最大的诟病是一个阻塞协议。RM在执行分支事务后需要等待MT决定,此时服务会堵塞并锁定资源,由于其堵塞机制和最差时间复杂度较高,因此这种设计不能适应随着事务涉及的服务数量增多而扩展的需要,很难用于并发较高以及事务生命周期较长的分布式服务中。
如果拿TCC的事务处理流程与2PC做比较,2PC通常都是在跨库的DB层面,而TCC则在应用层面的处理,需要通过业务逻辑来实现。这种分布式事务的实现方式优势在于,可以让应用自己定义数据库操作的粒度,使得降低锁冲突、提高吞吐量。不足之处是对应用的侵入性非常强,业务逻辑的每个分支都要实现try、confirm、cancel三个操作;此外实现难度也比较大,需要按照网络状态、系统故障的等不同失败原因进行不同回滚策略。
可靠消息最终一致性适合执行周期长且实时性要求不高的业务场景。引入消息机制后,同步的事务操作变为基于消息执行的异步操作,避免了分布式事务中同步堵塞操作的影响,并实现了两个服务之间的解耦。
最大努力通知是分布式事务中要求最低的一种,适用于一些最终一致性时间敏感度低的业务;允许发起通知方处理失败,在接收通知方收到失败通知后积极进行失败处理,无论发起通知方如何处理结果都不会影响到接收方的后续处理;发起通知方需要提供查询执行情况的接口,用于接收通知方校对结果。
在实际开发中,可靠消息最终一致性是较为常见的解决方案,特别是在微服务架构和高并发场景中,它能够提供较好的灵活性和扩展性。如果对于需要严格事务保证的系统,2PC和TCC可以提供更强的一致性保障。
2PC
2PC即两阶段提交协议,是将整个事务流程分为两个阶段:准备阶段(Prepare
)、提交阶段(Commit
)。
举例,张三、李四聚餐,饭店老板要求先买单才能出票。张三李四都不愿请客,只能AA。只有张三和李四都付款,老板才能出票安排就餐。 准备阶段,老板要求张三付款,张三付款;老板要求李四付款,李四付款;提交阶段:老板出票,两人拿票纷纷入座就餐。
例子中形成了一个事务,若张三或李四其中一人拒绝付款,或者钱不够,老板都不会出票,并且把已收的钱退回。 整个事务过程由事务管理器和参与者组成,老板就是事务管理器,张三、李四就是事务参与者,事务管理器负责抉择整个分布式事务的提交和回滚,事务参与者负责自己的本地事务提交和回滚。
在计算机中部分关系型数据库如Oracle、MySql支持两阶段提交协议:
- 准备阶段:事务管理器给每个参与者发送
Prepare
消息,每个数据库参与者执行本地事务,并写本地的Undo/Redo
日志,此时事务还没有提交。Undo日志是记录修改前的数据,用于数据库回滚;Redo日志是记录修改后的数据,用于提交事务后写入数据文件。
- 提交阶段:如果事务管理器收到了参与者的执行失败或超时的消息,那么直接给每个参与者发送回滚消息;否则,发送提交消息。参与者根据事务管理器的指令执行提交或回滚的操作,并释放事务处理过程中使用的锁资源。
成功情况:
失败情况:
XA方案
2PC的传统方案是在数据库层面实现的,如Oracle
、MySql
都支持2PC协议。为了统一标准减少行业内不必要的对接成本,需要制定标准化的处理模型即接口标准,国际开放标准组织OpenGroup
定义了分布式处理模型DTP(Distributed Transaction Processing Reference Model
)。
DTP模型定义如下角色:
- AP:
Application Program
即应用程序,可以理解为使用分布式事务的程序。 - RM:
Resource Manager
即资源管理器,可理解为事务的参与者,一般情况是指一个数据库实例,通过资源管理器对该数据库进行控制,资源管理器控制这分支事务。 - TM:
Transacition Manager
即事务管理器,负责协调事务和事务管理,它控制着全局的事务,管理事务的生命周期,并协调各个资源管理器。全局事务是指分布式处理事务环境中,需要操作多个数据库共同完成一个动作,这个工作即是一个全局事务。
以新用户注册送积分为例:
执行流程如下:
- 应用程序持有数据库和积分库两个数据源。
- 应用程序通过事务管理器通知用户库的资源管理器新增用户,同时也通知积分库的资源管理器为该用户增加积分,资源管理器此时并未提交事务,此时用户和积分资源锁定。
- 事务管理器收到执行回复,只要有一方失败则分别向其他方发起事务回滚,回滚完毕,资源释放锁;或事务管理器收到执行回复,全部成功,此时向所有资源管理器发起提交事务,提交完毕,资源释放。
整个2PC的事务管理流程涉及到三个角色:AP、RM、TM。AP指的是使用2PC分布式事务的应用程序;RM指的是资源管理器,它控制这分支事务;TM指的是事务管理器,它控制着全局事务。三个角色之间的交互方式如下:
- TM向AP提供程序编程接口,AP通过TM提交或回滚事务;
- TM交易中间件通过XA接口来通知RM数据库事务的开始、结束以及提交、回滚等;
在准备阶段RM执行实际的业务操作,但是不提交事务,资源锁定。在提交阶段TM会接受RM在准备阶段的执行回复,只要任何一个RM执行失败,TM会通知所有的RM进行回滚操作,否则TM将会通知RM提交事务,提交阶段结束释放资源。
DTP模型定义TM和RM之间通讯的接口规范叫XA,简单理解为数据库提供的2PC接口协议,基于数据库的XA协议来实现2PC又称为XA方案。 XA方案需要本地数据库支持XA协议,资源锁需要等到准备阶段和提交阶段结束才释放,所以性能较差。
Seata方案
Seata是由阿里中间件团队发起的开源项目Fescar,后更名为Seata,它是一个开源的分布式事务框架。
Seata的设计目标其一是对业务零侵入,因此从业务无侵入的2PC入手,在传统方案2PC的基础上演进,并解决2PC方案面临的问题。 Seata把一个分布式的事务理解为一个包含了若干分支事务的全局事务,它通过对本地关系型数据库的分支事务的协调来驱动完成全局事务,是工作在应用层的中间件。全局事务的职责是协调其下管辖的分支事务达成一致,要么一起成功提交事务,要么一起回滚失败。此外,通常分支事务本身就是一个关系型数据库的本地事务。 主要优点是性能较好,且不长时间占用连接资源,它以高效并对业务0入侵的方式解决微服务场景下面临的分布式事务问题,目前提供AT模式(即2PC)和TCC模式的分布式事务解决方案。
与传统2PC的模型类似,Seata定义了3个组件来协调分布式事务的处理过程:
- TM:
Transaction Manager
事务管理器,需要嵌入(jar包)应用程序中工作,负责开启一个全局事务,并最终向TC发起全局提交或全局回滚的指令。 - RM:
Resource Manager
资源管理器,控制分支事务,负责分支事务注册,状态回报,并接收事务调节器的指令,驱动分支事务的提交和回滚。 - TC:
Transaction Coordinator
事务协调器,它是独立的中间件,需要独立运行部署,它维护全局事务的运行状态,接收TM指令发起全局事务的提交与回滚,负责与RM通信协调各个分支事务的提交回滚。
以新用户注册送积分举例:
具体执行流程:
- 用户服务器的事务管理器向事务协调器申请开启一个全局事务,全局事务的创建成功并生成一个全局唯一XID。
- 用户服务器的资源管理器向事务协调器注册分支事务,该分支事务在用户服务执行新增用户逻辑,并将其纳入XID对应全局事务的管辖。
在执行注册分支事务的时候,这里事务已经提交了,提交后可以释放资源,从而提升程序性能。
- 用户服务器执行分支事务,向用户表插入一条记录。
- 逻辑执行到远程分布式调用积分服务时,XID在微服务调用链路的上下文中传播。积分服务的资源管理器向事务协调器注册了分支事务,该分支事务执行增加积分的逻辑,并将其纳入XID对应的全局事务的管辖。
- 积分服务执行分支事务,向积分记录表插入一条记录,执行完毕后,返回用户服务。
- 用户服务分支事务执行完毕。
- 事务管理器向事务协调器针对XID的全局提交或者回滚决策。
- 事务协调器调度XID下管辖的全部分支事务完成提交或回滚的请求。
Seata实现2PC与传统2PC的差别:
- 架构方面,传统2PC方案的RM实际上是在数据库层,RM本质上就是数据库自身,通过XA协议实现,而Seata的RM则是以jar包的形式作为中间件层部署在应用程序这一侧的。
- 两阶段提交方面,传统2PC无论第二阶段的决策是
commit
还是rollback
,事务性资源的锁都要保持到第二阶段才释放。而Seata的做法是在第一阶段就将本地事务提交,这样可以省去第二阶段持有锁的时间,提高整体效率。
Seata两阶段提交协议的演变:
-
一阶段:Seata会拦截,解析SQL语义,找到SQL要更新的数据,在业务员数据更新前,将其保存为
before image
,随后执行业务SQL。在业务数据更新后,将其保存为after image
,插入UNDO LOG
回滚日志;提交前向TC注册分支并生成行锁。 随后本地事务提交,将本地事务提交的结果上报给TC,由TC协调。 -
二阶段-提交:执行提交操作,说明SQL执行顺利,因业务SQL在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
-
二阶段-回滚:要执行回滚操作,说明SQL执行不顺利,Seata需要回滚一阶段已经执行的业务SQL还原数据。回滚方式是用
before image
还原数据,但是在还原前还要校验脏写,对比数据库当前业务数据和after image
。 对比数据库当前业务数据和after image
。
TCC
TCC是Try
、Confirm
、Cancel
三个词语的缩写,TCC要求每个分支事务实现三个操作,预处理Try
、确认Confirm
、撤销Cancel
。
Try
操作做业务检查及资源预留,Confirm
做业务确认操作,Cancel
实现一个与Try
相反的操作,即回滚操作。
TCC的三个阶段:
Try
阶段是做业务检查一致性及资源预留,此阶段仅是一个初步操作,它和后续的Confirm
一起才能真正的构成一个完整的业务逻辑。Confirm
阶段是确认提交,Try
阶段所有的分支事务执行成功后开始执行Confirm
。通常情况下,采用TCC则认为Confirm
阶段是不会出错的。即只要Try
成功,Confirm
一定成功。若Confirm
阶段真的出错了,需要引入重试机制或进行人工处理。Cancel
阶段是在业务执行错误需要回滚的状态下,执行分支事务的业务取消了,预留资源释放。通常情况下,采用TCC则认为Cancel
阶段也是一定成功的。如果Cancel
阶段真的出错了,需要引入重试机制或进行人工处理。
TM首先发起所有分支事务的Try
操作,任何一个分支事务的Try
操作执行失败,TM将会发起所有分支事务的Cancel
操作。
若Try
操作全部成功,TM将发起所有分支事务的Confirm
操作,其中Confirm
或Cancel
操作失败,TM会重试。
TM即事务管理器,TM事务管理器可以实现为独立的服务,也可以让全局事务发起方充当TM的角色。TM独立出来是为了成为公用的组件,为了考虑系统结构和软件复用。
TM发起全局事务时生成全局事务记录,全局事务ID贯穿整个分布式事务调用链,用来记录事务上下文,追踪和记录状态。
由于Confirm
和Cancel
失败需要进行重试或人工处理,因此需要实现幂等性,幂等性是指同一个操作无论请求多少次,其结果都相同。
分布式事务执行成功情况:
分布式事务执行失败情况:
Hmily方案
Hmily是一个高性能分布式事务TCC开源框架。基于Java语言来开发(JDK1.8),支持Dubbo,Spring Cloud等RPC框架进行分布式事务。
支持以下特性:
- 支持嵌套事务;
- 采用
disruptor
框架进行事务日志的异步读写,与RPC框架的性能毫无差别; - 支持
SpringBoot-Starter
项目启动,使用简单; - RPC框架支持:Dubbo,SpringCloud;
- 本地事务存储支持:Redis,Mongodb,ZooKeeper,MySql;
- 事务日志序列化支持:Java,Hessian,Kryo,Protostuff;
- 采用
Aspect AOP
切面思想与Spring无缝集成,天然支持集群; - RPC事务恢复,超时异常恢复等;
Hmily是一个轻量级的TCC事务框架不需要部署独立的事务协调服务,但需要提供一个数据库来进行日志存储。
Hmily利用AOP对参与分布式事务的本地方法与远程方法进行拦截处理,通过多方拦截,事务参与者能透明的调用到另一方的Try
、Confirm
、Cancel
方法,传递事务上下文;并记录事务日志,酌情进行补偿,重试等。
Hmily实现的TCC服务与普通的服务一样,只需要暴露一个接口,也就是它的Try
业务。Confirm/Cancel
业务逻辑,只是因为全局事务提交/回滚的需要才提供的,因此Confirm/Cancel
业务只需要被Hmily TCC事务框架发现即可,不需要被调用它的其他业务服务所感知。
要实现TCC协议,必须要实现三个方法:Try
、Confirm
、Cancel
,其中最关键的方法是Try
方法,Try方法是一个事务的起点,三个方法会由不同的线程来分别调用。
TCC需要注意三种异常处理:
- 空回滚:在没有调用TCC资源
Try
方法的情况下,调用了二阶段的Cancel
方法,Cancel
方法需要识别出这是一个空回滚,然后直接返回成功。 出现原因是当一个分支事务所在的服务器宕机或网络异常,分支事务调用记录为失败,这个时候其实是没有执行Try
阶段,当故障恢复后,分布式事务进行回滚则会调用二阶段的Cancel
方法,从而形成空回滚。 解决思路关键就是要标识这个空回滚,就是要知道Try
阶段是否已经执行了,如果执行了那就是正常回滚;如果没执行,那就是空回滚。可以在try阶段执行完毕后向表里插入一条记录进行标识,如果记录存在则Try
阶段执行了,如果不存在Try
阶段则未执行。 - 幂等:为了保证TCC二阶段提交重试机制不会引发数据不一致,要求TCC二阶段
Try
、Confirm
、Cancel
接口保证幂等性,这样不会重复使用或示释放资源。如果幂等控制没有做好,很有可能导致数据不一致等严重问题。 - 悬挂:悬挂就是对于一个分布式事务,其二阶段
Cancel
接口对比Try
接口先执行。 出现原因是在RPC调用分支事务时,先注册分支事务,在执行RPC调用。如果此时RPC调用的网络发生错误、RPC超时后,TM就会通知RM回滚该分布式事务,可能回滚完成后RPC请求才到达,然后执行,而一个Try
方法预留的业务资源,只有该分布式事务才能使用,该分布式事务第一阶段预留的业务资源也就在也没有人能够处理,对于这种情况就称为悬挂,即业务资源预留后没办法处理。
可靠消息最终一致性
可靠消息最终一致性是指当事务发起方执行完本地事务后并发出一条消息,事务参与方(消息消费者)一定能接收到消息并成功处理事务,此方案强调的是只要消息发给事务参与方最终事务要达到一致。
此方案利用消息中间件完成,事务发起方将消息发送给消息中间件,事务参与方从消息中间件接收消息,事务发起方和消息中间件之间,事务参与方和消息中间件之间都是通过网络进行通信,由于网络通信的不稳定会导致分布式事务问题。
此方案利用消息中间件完成,因此可靠消息最终一致性方案要解决几个问题:
- 本地事务与消息发送的原子性:即事务发起方在本地事务执行成功后消息必须发出去,否则就丢弃消息。即实现本地事务和消息发送的原子性,要么都成功,要么都失败。本地事务与消息发送的原子性问题是实现可靠消息最终一致性方案的关键。
- 事务参与方接收消息的可靠性:事务参与方必须能够从消息队列接收消息,如果接收消息失败可以重复接收消息。
- 消息重复消费问题:由于网络存在问题,若某一个消费结点超时但是消费成功,此时消息中间件会重复投递此消息,就导致了消息重复消费问题。要解决消息重复消费问题就要实现事务参与方的方法的幂等性。
本地消息表方案
本地消息表该方案最初是eBay提出,此方案的核心是通过本地事务保证数据业务操作和消息的一致性,然后通过定时任务将消息发送至消息中间件,待确认消息发送给消费方成功后在将消息删除。
以用户注册送积分举例,用户服务负责添加用户,积分服务负责添加积分。
执行流程:
- 用户注册,用户服务在本地新增用户和增加积分消息日志。用户表和消息表本地事务保持一致,这种情况下本地数据库操作与存储积分消息日志处于同一个事务中,本地数据库操作记录与记录消息日志操作具备原子性。
- 当消息已经写入到消息日志中去后,可以启动独立的线程,定时对消息日志表中的消息进行扫描并发送至消息中间件,在消息中间件反馈发送成功后删除该消息日志,否则等待定时任务下一周期重试。
- 使用MQ的ACK机制即消息确认机制,消费者监听MQ,如果消费者接收到消息并且业务处理完成后想MQ发送ACK,此时说明消费者正常消费消息完成,MQ将不再向消费者推送消息,否则生产者会不断重试向消费者发送消息。 由于消息会重复投递,因此积分服务的增加积分操作功能要实现幂等性。
RocketMQ方案
RocketMQ是阿里巴巴的分布式消息中间件,于2012年开源并在2017年正式称为Apache的项目。ApacheRocketMQ4.3之后的版本正式支持事务消息,并为分布式事务提供了便利性支持。
RocketMQ事务消息设计则主要是为了解决生产者端的消息发送与本地事务执行的原子性问题。RockerMQ设计中broker
与生产者端的双向通信能力,使得broker
天生可以作为一个事务协调者存在。
RockerMQ的高可用机制以及可靠消息设计原则则为事务消息在系统发生异常时依然能够保证达成事务的最终一致性。
执行流程:
- MQ的发起方会向MQ发送一条消息,MQ发起方会监听MQ是否响应;
- MQ收到该消息会响应消息发起方,代表发送给MQ成功;但是此时MQ并未发送消息给消费者;
- MQ发起方开始执行本地事务,执行完本地事务后,给MQ发送
commit
消息,此时MQ将该消息更改为可消费状态,消费者可以消费该消息;如果消费者执行本地事务失败,给MQ发送rollbac
k消息,MQ会将该消息丢弃; - 消费者方利用MQ的ACK机制来保证消息一定被消费到。 如果发起方与MQ之间的网络出现问题,但MQ里的消息不会一直存着。MQ服务会定回查消费者方本地事务状态(实现MQ事务回查接口),如果事务已经提交了消费者仍然可以接收到该消息,如果没有提交MQ则丢弃该消息;
最大努力通知
最大努力通知,顾名思义就是发起通知方通过一定机制,最大努力的将业务处理结果通知到接收方。
举个例子,账户系统调用充值系统接口,充值系统完成处理后向账户账户接口发起充值结果通知,如果通知失败则充值充值系统则按策略进行重复通知。 账户系统接收到充值系统的通知后修改充值状态,如果账户系统未接受到通知,账户系统会主动调用充值系统接口进行结果查询。
最大努力通知特点:
- 有一定的消息重复通知机制。因接收方可能没有收到通知,此时要有一定的机制对消息重复通知。
- 消息校对机制。如果发起方尽最大努力也没有通知到接收方,或接收方消费消息后要再次消费,此时可由接收方主动向通知方查询处理结果来满足需求。
最大努力通知与可靠消息一致性方案不同点:
- 解决方案思想不同:
- 可靠消息一致性,发起通知方需要保证将消息发送出去,并且将消息发送到接收方,消息的可靠性关键由发起方来保证。
- 最大努力通知,发起通知尽最大努力的将业务处理结果通知接收方,但是消息可能接收不到,此时需要接收通知方主动调用发起方的查询业务处理结果接口,通知的可靠性关键在接收方。
- 业务场景不同:
- 可靠消息一致性关注的是交易过程事务的一致性,以异步的方式完成交易。
- 最大努力通知关注的是交易后的通知事务,即将交易结果可靠的通知出去。
- 技术解决方向不同:
- 可靠消息一致性要解决消息从发出到接收的一致性,即发出消息并且被接收到。
- 最大努力通知无法保证发出到接收的一致性,只提供消息接收的可靠性机制。消息接收可靠机制指,最大努力的将通知发送给接收方,当消息无法被接收方接收时,由接收方主动查询该消息。
采用MQ的ACK机制可以实现最大努力通知。
MQ方案1
执行流程:
- 发起通知方将通知发送给MQ;
- 接收方监听MQ;
- 接收通知方接收消息,业务处理完成回应ACK,接收方若没有回应ACK则MQ重复通知;
- 如果消息没有发送出去可由接收方主动请求发起方查询业务结果接口,通过该接口校对消息的一致性;
方案1中接收通知方MQ接口,即接收通知方监听MQ,此方案主要应用于应用与内部应用之间的通知。
MQ方案2
交互流程:
- 发起通知方将消息通知给MQ。使用可靠消息一致方案中的事务消息保证本地事务与消息的原子性,最终将通知先发给MQ;
- 通知程序监听MQ,接收MQ消息。通知程序如果没有回应ACK则MQ会重复通知;
- 通知程序通过互联网接口协议调用接收通知方接口,完成通知。通知程序调用接收方接口成功就表示通知成功,即MQ消费成功,MQ将不再向通知程序投递消息;
- 接收通知方可通过消息校对接口来校对消息的一致性;
方案2中由于通知程序与MQ接口,通知程序监听MQ,收到MQ消息后由通知程序通过互联网接口协议调用接收通知方,此方案主要应用于外部应用之间的通知。