分布式事务及其解决方案

x33g5p2x  于2022-12-05 转载在 Java  
字(7.6k)|赞(0)|评价(0)|浏览(1092)

文章摘要:介绍了分布式事务的概念和基础理论,并介绍业内的常用分布式事务解决方案及原理。

1. 基础概念

1.1 事务

事务是指逻辑上的一组操作,组成这组操作的各个单元,要么全部成功,要么全部失败。

1.2 本地事务

通常我们都是用关系型数据库的事务特性来实现事务的,而数据库通常和应用在同一个服务器,所以关系型数据库的事务又称为本地事务。

数据库事务的四大特性:ACID

  • Atomic(原子性)事务是一个不可分割的工作单元,事务中的操作要么都发生,要么都不发生;
  • Consistent(一致性)事务完成时,必须使所有数据都保持一致状态;
  • Isolation(隔离性)并发事务所做的修改必须和其他事务所做的修改是隔离的;
  • Duration(持久性)事务完成之后,对数据库中数据的改变是永久性的;

1.3 分布式事务

随着互联网的快速发展,系统由原来的单体应用转变为分布式应用。

分布式系统会把一个应用系统拆分成可独立部署的多个服务,因此需要服务与服务之间远程协作才能完成事务操作,这种分布式系统环境下由不同的服务之间通过网络远程协作完成事务称为分布式事务。

举例:张三给李四转100元

本地事务:

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

在分布式环境下:

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

如果远程调用李四增加100元成功了,但由于网络问题远程调用并没有返回,此时本地事务提交失败回滚了张三减少100元的操作,此时张三和李四的数据就出现不一致了。

在分布式系统中,每一个服务虽然能明确知道自己执行的事务是成功还是失败,但是却无法知道其他分布式节点的事务执行情况,因此会导致分布式事务问题。

2. 分布式事务基础理论

分布式系统中,因为提供服务的各个节点在不同机器上,相互之间通过网络交互,系统不能因为有一点网络问题就导致整个系统无法提供服务,因此,网络因素也成为了分布式事务的考量标准之一。

2.1 CAP理论

  • C:Consistency,一致性。

指写操作后的读操作可以读取到最新的数据状态,当数据分布在多个节点上,从任意结点读取的到数据都是最新的状态。

  • A:Availability,可用性。

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

  • P:Partition tolerance,分区容忍性。

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

一个分布式系统最多只能同时满足:一致性(Consistency)、可用性(Availability)和分区容忍性(Parition tolerance)这三项中的两项。

  • AP:放弃一致性,追求分区容忍性和可用性。大部分分布式系统设计时的选择。
  • CP:放弃可用性,追求一致性和分区容错性。如Zookeeper。
  • CA:放弃分区容错性,不考虑网络因素或结点挂掉的问题。关系型数据库就满足了CA,但不是一个标准的分布式系统。
    一般保证A和P,舍弃C强一致,保证最终一致性。

2.2 BASE理论

BASE是Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)三个短语的缩写。

  • 基本可用:分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。如电商网站交易付款出现问题了,商品依然可以正常浏览。
  • 软状态:由于不要求强一致性,所以BASE允许系统中存在中间状态(也叫软状态),这个状态不影响系统可用性,如订单的"支付中"、“数据同步中”等状态,待数据最终一致后状态改为“成功”状态。
  • 最终一致:最终一致是指经过一段时间后,所有节点数据都将会达到一致。如订单的"支付中"状态,最终会变 为“支付成功”或者"支付失败",使订单状态与实际交易结果达成一致,但需要一定时间的延迟、等待。

BASE理论是对CAP中AP的一个扩展,通过牺牲强一致性来获得可用性,当出现故障允许部分不可用但要保证核心功能可用,允许数据在一段时间内是不一致的,但最终达到一致状态。满足BASE理论的事务,我们称之为“柔性事务”。

3. 分布式事务解决方案

既然存在分布式事务的问题,那就一定有成熟的解决方案,介绍一下业内的常用解决方案及原理。

3.1 X/OpenDTP事务模型

X/Open DTP(X/Open Distributed Transaction Processing Reference Model)是X/Open这个组织定义的一套分布式事务的标准,它定义了规范和API接口,由各个厂商进行具体的实现。这个标准提出了使用二阶段提交(2PC:Two-Phase-Commit)来保证分布式事务的完整性。后来J2EE也遵循了X/OpenDTP规范,设计并实现了java里的分布式事务编程接口规范JTA。

在X/OpenDTP事务模型中,定义了三个角色:

  • AP:Application,应用程序,也就是我们的业务系统。
  • RM:Resource Manager,资源管理器,也就是数据库。
  • TM:Transaction Manager,事务管理器、事务协调者,负责接收来自用户程序(AP)发起的XA事务指令,并调度和协调参与事务的所有RM(数据库),确保事务正确完成。

(1)参与分布式事务的应用程序(AP)先到事务管理器(TM)上注册全局事务。

(2)然后各个AP直接在相应的资源管理器(RM)上进行事务操作。

(3)操作完成以后,各个AP反馈事务的处理结果给到TM。

(4)TM收到所有AP的反馈以后,通过数据库提供的XA接口进行数据提交或者回滚操作。

3.2 2PC

2PC(two-phase-commit)即两阶段提交。它将整个事务流程分为两个阶段:准备阶段(prepare phase)、提交阶段(commit phase)。

(1)准备阶段:RM执行实际的业务操作,但不提交事务,锁定资源。

(2)提交阶段:TM会接受RM在准备阶段的执行结果,如果所有的RM都返回成功,TM将会通知所有RM提交事务,如果所有的RM都提交成功了,则分布式事务成功。否则,只要有任一个RM执行失败,TM会通知所有RM执行回滚操作,分布式事务回滚。提交阶段结束释放资源锁。

优点:原理简单,实现很方便

缺陷:

  • 同步阻塞:在阶段一里执行prepare操作会占用资源,一直到整个分布式事务完成,才会释放资源,这个过程中,如果有其他人要访问这个资源,就会被阻塞住。
  • 单点故障:TM是个单点,如果TM在commit出现故障,那么其它参与者一直处于锁定状态。
  • 事务状态丢失:即使把TM做成一个双机热备的,一个TM挂了自动选举其他的TM出来,但如果TM挂掉的同时,接收到commit消息的某个库也挂了,此时即使重新选举了其它的TM,也不知道这个分布式事务当前的状态。
    -数据不一致问题:在commit阶段,当协调者向所有的参与者发送commit请求后,发生了网络异常导致协调者在还未发完commit请求之前崩溃,可能会导致只有部分的参与者接收到commit请求,剩下没收到commit请求的参与者将无法提交事务,也就可能导致数据不一致的问题。

3.3 3PC

3PC(three-phase-commitment,三阶段提交协议),主要用来解决2PC的同步阻塞问题。3PC分为3个阶段,分别为:CanCommit、Precommit、DoCommit。

(1)CanCommit阶段:TM向各个数据库发送CanCommit消息,然后各个库返回结果。这一阶段主要是确定分布式事务的参与者是否具备了完成commit的条件,并不会执行事务操作。

(2)PreCommit阶段:TM根据数据库的反馈情况来决定是否继续执行事务的PreCommit操作。如果各个数据库都返回成功,则进入PreCommit阶段,TM发送PreCommit消息给各个数据库(相当于2PC里的阶段一),执行各个SQL语句,但不提交。如果有某个库对CanCommit消息返回了失败,则TM发送abort消息给各个库,结束这个分布式事务。

(3)DoCommit阶段:如果各个库对PreCommit阶段都返回了成功,那么发送DoCommit消息给各个库提交事务,各个库如果都返回成功给TM,那么分布式事务成功。如果有个库对PreCommit返回的是失败,或者超时一直没返回,那么TM认为分布式事务失败,直接发abort消息给各个库进行回滚,各个库回滚成功之后通知TM,分布式事务回滚。

与2PC相比,主要有两个改进点:

(1)引入了CanCommit阶段。

(2)在DoCommit阶段,各个库自己也有超时机制。如果一个库收到了PreCommit自己还返回成功了,过了一段时间还没收到TM发送的DoCommit消息或者是abort消息,直接判定为TM可能出故障了,则会自己执行DoCommit操作提交事务。在3PC里面不会因为故障导致某个库一直锁住某个资源,导致长时间的资源阻塞。

3PC的缺陷:

如果TM在DoCommit阶段发送了abort消息给各个库,结果因为脑裂问题,某个库没接收到abort消息,它仍会超时执行commit操作。

所以,无论是2PC,还是3PC,都没法完全保证分布式事务的正确性。

3.4 本地消息表

本地消息表方案最初是国外ebay提出的。

大致步骤如下:

(1)系统A在自己本地一个事务里操作同时,插入一条数据到消息表。

(2)接着系统A将这个消息发送到MQ中去。

(3)系统B接收到消息之后,在一个事务里,往自己本地消息表里插入一条数据,同时执行其他的业务操作,如果这个消息已经被处理过了,那么此时这个事务会回滚,这样保证不会重复处理消息。

(4)系统B执行成功之后,就会更新自己本地消息表的状态以及系统A消息表的状态。

(5)如果系统B处理失败了,那么就不会更新消息表状态,那么此时系统A会定时扫描自己的消息表,如果有没处理的消息,会再次发送到MQ中去,让B再次处理。

本地消息表方案保证了最终一致性,哪怕B事务失败了,但是A会不断重发消息,直到B那边成功为止。

缺点:

最大的问题是严重依赖于数据库的消息表来管理事务。高并发场景下会有问题,扩展性差。一般很少用。

3.5 TCC方案

TCC的全称是:Try、Confirm、Cancel。它其实是用到补偿的概念,分为了三个阶段:

(1)try阶段:这个阶段是对各个服务的资源做检测以及对资源进行锁定或者预留。

(2)confirm阶段:这个阶段是在各个服务中执行实际的操作。

(3)cancel阶段:如果有任何一个服务的业务方法执行出错,那么这里就需要进行补偿,就是执行已经执行成功业务逻辑的回滚操作。

TCC方案涉及3个组件:

(1)主业务服务:它是TCC事务的主控服务,整个分布式事务的编排和管理、执行、回滚操作都是由它来控制。

(2)从业务服务:主要提供try-confirm-cancel 3个接口,try接口里进行锁定资源,confirm接口里执行业务逻辑,cancel接口是回滚逻辑。

(3)业务活动管理器:管理具体的分布式事务的状态,分布式事务中各个服务对应的子事务的状态,同时它也负责去触发各个从业务服务的confirm和cancel接口的执行和调用。

执行流程如下:

(1)主业务服务先在本地开启一个本地事务。

(2)主业务服务向业务活动管理器申请启动一个分布式事务活动,主业务服务向业务活动管理器注册各个从业务活动。

(3)接着,主业务服务调用各个从业务服务的try接口。

(4)如果所有从业务服务的try接口调用成功的话,那么主业务服务提交本地事务,然后通知业务活动管理器调用各个从业务服务的confirm接口。

(5)如果有某个服务的try接口调用失败的话,那么主业务服务回滚本地事务,然后通知业务活动管理器调用各个从业务服务的cancel接口。

(6)如果主业务服务触发了confirm操作,但confirm过程中有失败,那么也会让业务活动管理器通知各个从业务服务cancel。

(7)最后分布式事务结束。

适用场景:

对一致性要求很高的系统的核心链路,如支付、交易相关的场景。严格保证分布式事务要么全部成功,要么全部自动回滚,严格保证链路的正确性。而且,最好各个业务执行的时间都比较短。

3.6 可靠消息最终一致性方案

可靠消息最终一致性方案涉及到4个组件:

(1)上游服务:发送MQ消息通知下游服务执行某个操作。

(2)可靠消息服务:协调上下游服务的消息传递,确保数据一致性,可以认为这个所谓的可靠消息服务是我们自己开发的通用服务,其它服务都基于这个可靠消息服务来实现可靠消息最终一致性的方案。

(3)MQ消息中间件:这个一般是RocketMQ或者是RabbitMQ。

(4)下游服务:要被调用的服务。

执行流程如下:

(1)上游服务发送一个待确认消息给可靠消息服务。

(2)可靠消息服务将这个待确认的消息保存到自己本地数据库里,保存起来,但是不发给MQ,这个时候消息的状态是“待确认”。

(3)上游服务操作本地数据库。

(4)上游服务根据自己操作本地数据库的结果,通知可靠消息服务确认发送消息或者是删除消息。

如果本地数据库操作失败,本地操作回滚,同时上游服务通知可靠消息服务删除消息;如果本地数据库操作成功,本地事务就提交了,接着通知可靠消息服务发送消息。

(5)可靠消息服务将这个消息的状态修改为“已发送”,并且将消息发送到MQ中间件里去。

这个环节是必须包裹在一个事务里的,如果发送MQ失败报错,那么可靠消息服务更新本地数据库里的消息状态为“已发送”的操作也必须回滚,反之如果本地数据库里的消息状态为“已发送”,那么必须成功投递消息到MQ里去。

(6)下游服务从MQ里监听到上游服务发送过来的消息。

(7)下游服务根据消息,在自己本地操作数据库。

(8)下游服务对本地数据库操作完成之后,对MQ进行ack操作,确认这个消息处理成功。

(9)下游服务对MQ进行ack之后,再给可靠消息服务发送个请求,通知该服务说,ok,我这里处理完毕了,可靠消息服务收到通知之后,将消息的状态修改为“已完成”。

各个环节失败的处理预案

(1)上游服务发送一个“待确认”消息给可靠消息服务失败。不会造成任何数据不一致。

(2)可靠消息服务没有成功的将消息保存到本地数据库,此时会返回给上游服务一个失败的消息,上游服务就不会继续往下执行了。

(3)上游服务执行本地数据库操作和发送确认消息两个操作是绑定在一起的。如果失败,本地事务会回滚,然后也不会继续发送确认消息给可靠消息服务。如果本地事务回滚之后,就会发送消息通知可靠消息服务,删除那条消息。

(4)上游服务的本地数据库操作都成功了,但调用可靠消息服务的时候失败了,此时会导致可靠消息服务的数据库里,留着一条状态为“待确认”的消息。

(5)分成两种情况来说:

1)如果上游服务操作本地数据库失败了,会通知可靠消息服务去删除消息,此时可靠消息服务删除消息的操作失败了,会导致有问题,有一个消息“待确认”始终停留在可靠消息服务的数据库里。

2)如果上游服务操作本地数据库成功了,会通知可靠消息服务去确认和投递消息,但是确认消息+投递消息(绑定在一起),现在一起失败了,消息还是“待确认”的状态,而且没有投递到MQ里去,此时也是不对的。

(6)手动ack的,只要MQ没有收到ack的通知,会重新投递消息的。

(7)如果下游服务操作自己本地数据库失败了,本地事务回滚且不会发送ack给MQ,后续MQ会继续不断的重试。

(8)如果下游服务处理完了本地操作,给MQ发送ack的时候失败了。可以考虑将下游服务的本地数据库操作和MQ的ack操作包裹在一个事务里,这样如果MQ ack操作报错了,本地事务直接回滚。

(9)如果下游服务执行成功,且手动ack确认成功,但是在发送给可靠消息服务修改消息状态为“已完成”的时候出错了。这个时候可靠消息服务的数据库的消息状态一直是”已发送“,状态是不对的。

3.7 最大努力通知方案

最大努力通知方案,又称为不是事务的事务。

最大努力通知方案的核心在于最大努力通知服务,这个服务的核心在于根据上游服务定义的重试规则对调用事变的消息,重试几次,最大努力尝试调用成功,跟可靠消息最终一致性的方案。

它跟可靠消息服务的区别在于:

可靠消息服务会保证,如果下游服务执行不成功,会一直不停的重试,直到下游服务执行成功为止,最终达到数据一致性,但是中间可能有很长的一段时间数据是不一致的。
最大努力通知服务,如果一次请求没成功,那么就将消息存到数据库里去,同时记录下它的重试规则,以及上一次重试的时间,是第几次重试,然后开启一个后台线程进行扫描,每次扫出来就根据规则去重新调用下游服务。

3.8 Saga

Saga事务是由一系列的本地事务构成。每一个本地事务在更新完数据库之后,会发布一条消息或一个事务来触发Saga中的下一个本地事务的执行。如果一个本地事务因为某些业务规则无法满足而失败,Saga会执行在这个失败的事务之前成功提交的所有事务的补偿操作。

Saga将每个接口拆分为两个接口,一个是业务接口,一个是补偿接口。先执行业务接口尝试完成整个业务逻辑的操作,如果服务调用链中某个服务的业务接口执行失败了,那么直接对已经执行成功的所有服务都调用其补偿接口,将之前执行成功的业务逻辑回滚。

Saga的实现有两种方式:

基于事件的方式:即每个服务执行成功后发布个事件,下一个服务会监听到这个事件,然后继续执行。优点是去中心化,缺点是多个服务连续调用会导致对消息的监听非常复杂。
基于命令的方式:即通过一个Saga事务流程管理器来负责依次调用和执行各个服务,如果某个服务调用失败,就对之前调用成功的服务依次执行补偿接口。优点是系统比较简单,缺点是整个系统严重于Saga事务流程管理器。

小结

介绍了分布式事务的概念和基础理论,并介绍业内的常用分布式事务解决方案及原理。

在实践中,可以将TCC事务、可靠消息最终一致性方案和最大努力通知等方案结合起来使用。对于服务调用链一致性要求极高的场景,可以用TCC事务来保证,如支付、交易类的业务。对于一些耗时可以做成异步化的服务的调用,可用使用可靠消息最终一致性的方案,基于MQ来做异步化,因为中间加了一个可靠消息服务,可以保证上游服务执行成功了,一定会保证下游服务也会执行成功,如发积分/优惠券等。对于非核心可异步化,且能接受下游业务失败的场景,可使用最大努力通知方案,如Push/短信通知。

相关文章