当前位置: 首页 > 后端技术 > Java

分布式事务处理方案大PK!

时间:2023-04-02 01:58:59 Java

@[toc]我同意写天津项目。最近这个分布式事务算是一个副任务。今天是最后一篇文章。宋大哥再出一篇小文,和小伙伴们总结一下分布式事务。首先说一个大原则:不会用分布式事务就别用。毕竟使用起来还是有些麻烦的。当然,不会用和不会用是两回事。一、分布式事务基础理论要学习分布式事务,有一些基础理论是我们首先需要了解的。1.1本地事务本地事务是指将多条语句作为一个整体进行操作的功能。通过数据库事务,可以保证事务范围内的所有操作都可以成功或失败。如果事务失败,效果就和不执行这些SQL一样,不会对数据库数据有任何改变。即事务是原子的,一个事务中的一系列操作要么全部成功,要么全部失败。一般来说,事务有四个属性:Atomic:原子性,将事务中的所有SQL作为一个原子工作单元执行,要么执行全部,要么不执行;Consistent:一致性,事务完成后,所有数据的状态都是一致的。以银行转账为例,如果A账户减去100,那么B账户必须加100;Isolation:隔离,如果多个事务并发执行,每个事务所做的修改必须与Othertransaction隔离一致;Duration:持久化,即事务完成后,对数据库数据的修改被持久化存储。这四种特性通常被称为ACID特性。之前宋哥录制过相关视频,这里不再赘述。https://www.bilibili.com/video/BV1Eq4y1R7Ds1.2分布式事务在我们的项目用微服务实现的时候,分布式事务是一个比较常见的问题,我们也会遇到很多相关的场景。就拿我们前两天讲的商品订单的分布式事务为例,如下一共有五个服务,结构如下图所示:eureka:这个是服务注册中心。account:这是账户服务,可以查询/修改用户的账户信息(主要是账户余额)。order:这是订单服务,可以在这里下订单。storage:这是一个仓储服务,可以查询/修改商品的库存数量。bussiness:这里是business,用户的订单会在这里完成。用户下单时,调用业务中的接口,业务中的接口调用自己的服务。服务中通过feign调用storage中的接口扣除库存,然后通过feign调用订单中的接口创建订单(order不仅会创建订单,还会扣除用户账户的余额创建订单时)。对于这三个操作,我们希望它们可以同时成功或同时失败。但是,如上图所示,三个微服务都有各自的DB,是三个完全不同的DB,相当于三个不同的本地事务。按照传统的本地事务规则,我们显然不能同时实现三个操作同时成功或同时失败。如果要实现存储、订单、账户操作同时成功或同时失败,就不得不考虑分布式事务。最后我们来看一下分布式事务的概念:分布式事务是指事务的参与者、支持事务的服务器、资源服务器、事务管理器分别位于不同的节点。数据库的操作执行成功与否,不仅取决于本地DB的执行结果,还取决于第三方系统的执行结果。分布式事务保证这些操作要么全部成功,要么全部失败。本质上,分布式事务是为了保证不同数据库的数据一致性。1.3CAP定理(CAPtheorem),有时也称为布鲁尔定理(Brewer'stheorem),它指出对于一个分布式计算系统,不可能同时满足以下三点:一致性(Consistency):在分布中是否系统中的所有数据备份同时具有相同的值。(相当于所有节点访问同一份最新的数据副本)。可用性:集群中部分节点发生故障后,集群整体是否还能响应客户端的读写请求。(数据更新的高可用性)。分区容忍度(Partitiontolerance):我觉得这个对于一些小伙伴来说可能有点难以理解。先简单说一下分区:因为我们是一个分布式系统,分布式系统中不同的微服务位于不同的网络节点上,当出现网络故障或者节点故障时,不同的服务之间是无法通信的,也就是说,发生分区;再来看分区容错:就是当我们的系统发生分区的时候,系统必须还能运行,不允许罢工!一般来说,在分布式系统中,分区的概率还是比较高的。没有分区的系统不是分布式系统,而是单体应用。CAP原则的本质要么是AP,要么是CP,要么是AC,但是没有CAP。因为在分布式系统中,P是不可避免的。如果不选择P,一旦发生分区,整个分布式系统将完全无法使用。这样的系统太脆弱了。所以对于分布式系统,我们只能考虑发生分区错误时如何选择一致性和可用性(选择一致性是指服务在一定时间内不可用,选择可用性是指服务虽然一直可用但是返回的数据不一致)。根据一致性和可用性的不同选择,开源分布式系统往往分为CP系统和AP系统。当系统出现分区故障,任何客户端请求卡住或超时,但系统的各个节点总是会返回一致的数据,那么这个系统就是一个CP系统,比如Zookeeper。如果一个系统出现分区故障,客户端仍然可以访问系统,但是获取的数据有的是新数据,有的是旧数据,那么这个系统就是AP系统,比如Eureka。1.4因为BASE不能同时满足CAP,所以又出现了BASE理论。BASE理论指的是:BasicallyAvailable:当分布式系统发生故障时,允许其失去部分可用性,即保证核心可用。SoftStateSoftState:允许系统有一个不影响系统整体可用性的中间状态。最终一致性:经过一定时间后,系统中的所有数据副本最终可以达到一致状态。BASE理论的核心思想是即使无法实现强一致性,也应该采用合适的方法来保证最终一致性。BASE理论本质上是CAP理论的延伸,是CAP中AP方案的补充。1.5刚性事务和柔性事务分为刚性事务和柔性事务。刚性事务(如单个数据库中的本地事务)完全遵循ACID规范,即数据库事务正确执行的四个基本要素:原子性(Atomicity)一致性(Consistency)隔离性(Isolation)持久性(Durability)灵活transactions,主要只是分布式事务。为了满足可用性、性能和降级服务的需求,灵活事务遵循BASE理论:基本可用性(BasicAvailability)灵活状态(Softstate)最终一致性(Eventualconsistency)当然,灵活事务也部分遵循ACID规范:Atomicity:Strictlyfollowtheconsistency:事务完成后严格遵循一致性;事务中的一致性可以适当放宽隔离:并行事务Interim不受影响;事务中间结果的可见性让安全放宽持久化:严格遵守灵活事务有不同的分类,但基本上都可以看作是分布式事务的解决方案:两阶段类型:分布式事务的两阶段提交,对应技术上XA,JTA/JTS,这是分布式环境下事务处理的典型模式。补偿型:我们上一篇介绍的TCC甚至是一种补偿型交易。在Try成功的情况下,如果事务需要回滚,会使用Cancel作为补偿机制回滚Try操作;TCC的每个操作事务都是本地化的,并尽可能早地提交(无两阶段约束);当全局事务需要回滚时,通过另一个本地事务实现“补偿”行为。TCC将资源层的两阶段提交协议转化为业务层,成为业务模型的一部分。异步保证型:将一些有同步冲突的事务操作改为异步操作,避免争用数据库事务,例如消息事务机制。Best-effort通知类型:通过通知服务器(消息通知),允许失败,有补充机制。2.分布式事务实践2.1XA先说XA。XA是典型的两阶段提交(2PC,Two-phasecommitprotocol),两阶段提交是一种强一致性设计。在两阶段提交中,一般会引入一个事务协调者角色来协调和管理各个事务参与者,比如我们上一篇文章中使用的seata-server,其实就是一个事务协调者。所谓两阶段分别是指准备阶段和提交阶段。XA规范是由X/Open组织定义的分布式事务处理(DTP,DistributedTransactionProcessing)标准。XA规范描述了全局事务管理器和本地资源管理器之间的接口。XA规范的目的是允许在同一个事务中访问多个资源(如数据库、应用服务器、消息队列等),从而使ACID属性在应用程序之间保持有效。XA规范使用两阶段提交来保证所有资源同时提交或回滚任何特定事务。XA规范是在1990年代初期提出的。目前几乎所有的主流数据库如MySQL、Oracle、MSSQL等都提供了对XA规范的支持。XA事务的基础是两阶段提交协议。需要交易协调员来确保所有交易参与者都准备就绪(阶段1)。如果协调器收到所有参与者都准备就绪的消息,它会通知所有事务它们已准备好提交(阶段2)。MySQL在这个XA事务中扮演的是参与者的角色,而不是协调者(事务管理器)。MySQL的XA事务分为内部XA和外部XA。外部XA可以参与外部分布式事务,这需要应用层作为协调者的介入;内部XA事务用于同一实例下的跨引擎事务,Binlog作为协调器。比如存储引擎commit时,需要将提交的信息写入binarylog,这是一个分布式的内部XA事务,但是binarylog的参与者是MySQL本身。MySQL在XA事务中扮演参与者的角色,而不是协调者。XA事务的特点是:简单易懂,易于开发。资源锁定时间长,并发度低。2.23PC3PC主要是为了弥补2PC的不足而产生的。2PC的缺点是什么?同步阻塞:2PC执行过程中,所有参与节点(即一个分支事务)都是事务阻塞。当参与者占用公共资源时,其他第三方节点访问公共资源时必须处于阻塞状态。2PC执行过程中,资源被锁定。单点故障:在2PC中,事务协调器起着举足轻重的作用。由于事务协调器的重要性,一旦事务协调器失效,事务参与者将一直处于阻塞状态。尤其是在第二阶段,如果协调者失效,所有参与者仍处于锁定事务资源的状态,无法继续完成事务操作。还有一个问题是,如果事务协调器在commit命令发出之前就宕机了,此时虽然可以重新选举一个新的协调器,但是仍然不能解决事务参与者因为事务协调器宕机而阻塞的问题。状态问题。3PC试图解决2PC的这些问题。3PC主要是将2PC中的第一个stage再次一分为二,这样3PC就有了三个不同的stage:CanCommit、PreCommit和DoCommit。但是,3PC并不能解决2PC的所有问题。3PC主要解决单点故障问题,减少阻塞。一旦事务参与者(分支事务)不能及时收到事务协调器的信息,分支事务会默认执行commit,而不是持有事务资源,处于阻塞状态,但是这种机制也带来了新的问题,假设事务协调器向每个分支事务发送一个abort命令,但是由于网络问题,分支事务没有及时收到命令,分支事务等待超时后执行commit操作,这样就和其他事务一样了收到中止命令和执行回滚的分支事务之间存在数据不一致。我们来看看3PC流程:CanCommit阶段:这个阶段做的事情很简单,就是事务协调器询问每个分支事务,你有能力完成这个事务吗?如果都返回yes,则进入第二阶段;如果返回no或等待响应超时,事务将中断,并向所有分支事务发送中止请求。PreCommit阶段:此时事务协调器会向所有分支事务发送PreCommit请求,分支事务收到后开始执行事务操作,并在事务日志中记录Undo和Redo信息。分支执行完事务操作后(此时属于未提交事务状态),会反馈“Ack”给事务协调器,表示我准备提交了,等待事务的下一步协调员。DoCommit阶段:在Phase2中,如果所有分支事务节点都可以进行PreCommit提交,事务协调器将从“预提交状态”变为“提交状态”,然后向所有分支事务节点发送“doCommit”请求,收到后提交请求,分支交易节点分别执行交易提交操作,并向协调节点反馈“Ack”消息,协调节点收到所有参与者的Ack消息后完成交易。反之,如果一个分支交易节点没有完成PreCommit反馈或者反馈超时,协调器会向所有参与节点发送abort请求,从而中断交易。2.3TCCTCC(Try-Confirm-Cancel)的概念最早是由PatHelland在2007年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文中提出的。TCC模式主要有以下优缺点:优点:性能提升:通过具体服务降低控制资源锁的粒度,不会锁住整个资源。数据最终一致性:基于Confirm和Cancel的幂等性,保证交易最终确认或取消,保证数据一致性。可靠性:解决了XA协议协调器的单点故障问题。主要业务方发起并控制整个业务活动。业务活动管理器也变成多点,引入集群。缺点:对微服务的侵入性强。微服务的每一个事务都要实现try、confirm、cancel三个方法。开发成本高,以后维护改造成本也高。为了满足事务的一致性要求,try、confirm、cancel接口必须实现幂等操作,这在一定程度上增加了开发工作量。TCC主要分为两个阶段,步骤如下:Try阶段(一阶段):尝试执行,完成所有业务检查(一致性),并预留必要的业务资源(准隔离)。Confirm阶段(第二阶段):确认执行确实是在执行业务,不做任何业务检查,只使用Try阶段预留的业务资源。Confirm操作满足需求和幂等性,Confirm执行失败后需要重试。Cancel阶段:取消执行,释放Try阶段预留的业务资源。Cancel操作也需要满足幂等性。Cancel阶段的异常处理方案与Confirm阶段基本相同。在我们之前的文章中,宋大哥也给大家举了一个TCC的例子,这里就不再赘述了。2.4SAGASAGA首次出现在1987年HectorGarcaa-Molrna和KennethSalem的论文SAGAS中。本文的核心思想是将长事务拆分为多个短事务,由Saga事务协调器进行协调。如果每个短交易都提交成功,那么全局交易就会正常完成。如果一个步骤失败,则根据补偿操作以相反的顺序一次调用一个。Saga事务的特点是:高并发,不需要像XA事务那样长时间锁定资源。需要定义正常运行和补偿运行(回滚),开发工作量比XA大。一致性较弱。对于转账,可能会出现A用户扣了钱,最终转账失败的情况。SAGA适用场景很多,适用于长事务或者对中间结果不敏感的业务场景。2.5本地消息表本地消息表最初是由ebay架构师DanPritchett在2008年发表给ACM的一篇文章中提出的。顾名思义,本地消息表就是存储本地消息的表,这些消息一般是放在数据库中,然后在执行业务的时候,业务的执行和将消息放入消息表的操作都是放在同一个交易中。这样可以保证消息放到本地表中,业务必须一起成功执行。当一个操作执行成功后,进入下一个操作。如果下次操作调用成功,可以直接将消息表的消息状态改为成功;如果下一次任务调用失败,没关系,会有后台任务定期读取本地消息表,过滤掉没有成功的消息,然后调用相应的服务(重试),再改服务更新成功后消息的状态。重试需要保证对应服务的方法是幂等的,一般会有最大重试次数。如果超过最大重试次数,可以记录告警,供人工处理。根据上面的描述,其实可以看到本地的消息表其实是实现了最终一致性的,也就是容忍暂时的数据不一致。本地消息表的特点:长事务只需要拆分成多个任务,使用方便。生产者需要创建额外的消息表。每个本地消息表都需要轮询(如果失败则重试)。如果消费者的逻辑不能重试成功,就需要更多的机制来回滚操作。根据本地消息表的特点,我们可以发现,本地消息表适用于可以异步执行且后续操作不需要回滚的业务。2.6消息事务本方案的核心思想是通过消息中间件将全局事务转化为局部事务,并通过消息中间件保证每个分支事务最终都能调用成功。宋大哥在使用RabbitMQ之前写过一篇文章:使用RabbitMQ处理分布式事务,后来发现使用阿里巴巴的RocketMQ(4.3之后)可以更好的实现分布式事务。RocketMQ是一个最终一致性的分布式事务,也就是说它保证消息的最终一致性,而不是像2PC、3PC、TCC那样的强一致性分布式事务。在RocketMQ中,有一种消息叫做HalfMessage,HalfMessage是指暂时不能被Consumer消费的消息。虽然Producer已经成功将消息发送给Broker,但是消息被标记为暂时无法投递。这种状态下的消息称为半消息。这时候Producer需要对消息进行二次确认,Consumer才能消费。RocketMQ是基于HalfMessage的分布式事务。举个转账的例子:A服务先发一个HalfMessage给Brock,消息中携带B服务要加100元的信息。当服务A知道HalfMessage发送成功后,启动本地事务。执行本地事务(会出现三种情况:1.执行成功;2.执行失败;3.由于网络等原因没有响应)3.1如果本地事务成功,则A向Broker发送Commit服务器,以便B服务可以消费该消息。3.2如果本地事务失败,则A向Broker服务器发送Rollback,上述半消息将被直接删除。3.3如果是由于网络或生产者应用重启等原因。结果A没有对HalfMessage进行二次确认。这时候Broker服务器会定时扫描半消息已经很久的消息,会主动向A端询问消息的最终状态(Commit或者Rollback)。此操作也称为消息返回。查看。可能有朋友会说,万一B最终执行失败了怎么办?在这种情况下,我们几乎可以断定是导致异常的代码有问题,因为消费端的RocketMQ是有重试机制的。如果不是代码问题,一般重试几次就会成功。是不是导致多次重试失败的代码也没关系。手动记录和处理异常。经过人工处理,交易可以达到最终一致性。2.7尽力通知发起方通过一定的机制尽力通知接收方业务处理结果。具体包括:有一定的消息重试机制。因为通知的接收者可能还没有收到通知,所以此时肯定有某种机制可以重试消息。消息整理机制。如果接收者尽最大努力仍未收到通知,或者接收者消费消息后想再次消费消息,则接收者可以主动向通知者查询消息信息以满足需求。前两节介绍的本地消息表和事务消息都是可靠消息。这与我们在此介绍的尽力而为通知有何不同?可靠的消息一致性:消息的发起者需要保证消息的发送和发送给接收者。消息的可靠性由发起者保证。Best-effortnotification:消息的发送方尽量将业务处理结果通知接收方,但可能收不到消息。这种情况下,接收方需要主动调用发送方接口查询业务处理结果。这时,消息可靠性的关键是接收者。就这样。在具体的解决方案上,besteffort通知需要消息发起方提供一个接口,让被通知方可以通过该接口查询业务处理结果。尽力而为通知适用于业务通知类型。最常见的场景是支付回调。支付服务收到第三方服务支付成功通知后,首先更新自己库中的订单支付状态,同时通知订单服务支付成功。如果同步通知失败,则通过异步步骤不断调用订单服务的接口。尽力而为通知更像是一种业务设计。在基础设施层,可以直接使用二阶段消息,或者交易消息,本地消息表等来实现。3.总结好了,学习分布式事务解决方案,最大的感触就是:没有灵丹妙药!在上一篇文章中,宋哥也和大家聊了很多实用的解决方案,也在TienChin项目中录制了相应的分布式事务视频。欢迎一起讨论。参考资料:https://help.aliyun.com/docum...https://cloud.tencent.com/dev...https://zh.m.wikipedia.org/zh...https://zhuanlan.zhihu.com/p/…