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

最经典的7个分布式事务解决方案都在这里

时间:2023-04-01 18:56:54 Java

下面我给大家送上java学习资料。从单体到分布式,尤其是微服务架构。以后难免会遇到分布式事务的问题。本文总结了分布式事务最经典的解决方案,分享给大家。基础理论在讲解具体方案之前,我们先来了解一下分布式事务涉及的基础理论知识。我们以转移为例。A需要转100元给B,那么需要给A的余额-100元,给B的余额+100元。整个转账必须保证,A-100和B+100同时成功,否则同时失败。看看在各种场景下如何解决这个问题。事务将多条语句作为一个整体进行操作的功能称为数据库事务。数据库事务可以保证事务范围内的所有操作可以全部成功或全部失败。事务有四个属性:原子性、一致性、隔离性和持久性。这四种特性通常被称为ACID特性。原子性:一个事务中的所有操作要么完成要么不完成,不会在中间某个环节结束。如果事务执行过程中出现错误,将恢复到事务开始前的状态,就好像事务从未执行过一样。一致性(consistency):事务开始前和事务结束后,数据库的完整性没有被破坏。完整性包括不违反外键约束、应用程序定义的约束等。隔离性:数据库允许多个并发事务同时读取、写入和修改其数据的能力。隔离可以防止多个事务并发执行时交叉执行导致的数据不一致。持久性(persistence):事务处理结束后,对数据的修改是永久性的,即使系统出现故障也不会丢失。分布式事务银行跨行转账业务是典型的分布式事务场景。假设A需要跨行给B转账,那么涉及到两家银行的数据。转账的ACID不能通过数据库的本地事务来保证,只能通过分布式事务来保证。解决。分布式事务是指事务发起者、资源和资源管理者、事务协调者分别位于分布式系统的不同节点上。在上述转账业务中,用户A-100的操作和用户B+100的操作不在同一个节点上。本质上,分布式事务是为了保证分布式场景下数据操作的正确执行。分布式环境下的分布式事务,为了满足可用性、性能和降级服务的需求,降低一致性和隔离性的要求,一方面遵循BASE理论(BASE相关理论,涉及内容很多,有兴趣同学们,可以参考BASE理论):基本可用性(BasicAvailability)、软状态(Softstate)、最终一致性(Eventualconsistency)同理,分布式事务也部分遵循ACID规范:原子性:严格遵循一致性:事务后的一致性完成严格遵守;事务中的一致性可以适当放宽隔离:不影响并行事务;事务中间结果的可见性允许安全放宽持久化:严格遵循分布式事务的解决方案Two-phasecommit/XAXA是由X/Open组织提出的分布式事务规范,XA规范主要定义了(全局)事务管理器(TM)和(本地)资源管理器(RM)。mysql等本地数据库在XA中起到了RM的作用。XA分为两个阶段:第一阶段(prepare):即所有参与者RM准备执行事务并锁定所需资源。当参与者准备就绪时,它会向TM报告它已准备就绪。第二阶段(commit/rollback):当事务管理器(TM)确认所有参与者(RM)准备就绪后,向所有参与者发送commit命令。目前主流的数据库基本都支持XA事务,包括mysql、oracle、sqlserver、postgre等。XA事务由一个或多个资源管理器(RM)、一个事务管理器(TM)和一个应用程序(ApplicationProgram)组成。以上述转账为例,一个成功完成的XA交易的时序图如下:如果任何参与者准备失败,TM会通知所有完成准备的参与者回滚。XA事务的特点是:简单易懂、易于开发、长时间锁定资源、低并发。如果读者想进一步研究XA,go语言可以参考DTM(https://github.com/yedf/dtm),Java语言可以参考seataSAGASaga是本篇数据库论文saga中提到的解决方案。它的核心思想是将一个长事务拆分成多个本地短事务,由Saga事务协调器协调。如果正常结束,则正常完成。如果某个步骤失败,将以相反的顺序调用一次补偿操作。以上述转账为例,一个成功完成的SAGA事务的时序图如下:开发量比XA交易弱,对于转账,可能会出现用户A扣了钱,最后转账失败的情况。论文中有很多SAGA内容,包括两种恢复策略,包括并发执行分支事务。我们这里的讨论只包括最简单的SAGASAGA,适用场景很多,比如长事务,对中间结果不敏感的业务场景。读者如果想深入研究SAGA,go语言可以参考DTM((https://github.com/yedf/dtm)),java语言可以参考seataTCC的TCC(Try-Confirm-Cancel)概念,这是由PatHelland于2007年发表的《Life beyond Distributed Transactions:an Apostate’s Opinion》论文首次提出。TCC分为3个阶段Try阶段:尝试执行,完成所有业务检查(一致性),预留必要的业务资源(准隔离)Confirm阶段:确认即执行实际执行业务,不进行任何业务检查,只使用Try阶段预留的业务资源,Confirm操作需要幂等设计,Confirm失败后需要重试。Cancel阶段:取消执行,释放Try阶段预留的业务资源。Cancel阶段的异常处理方案与Confirm阶段基本一致,需要幂等设计。以上述转账为例,金额通常在Try中冻结,但不扣除,在Confirm中扣除,在Cancel中解冻。一个成功完成的TCC事务的时序图如下:TCC的特点是:高并发,无Long-termresourcelocking。开发量大,需要提供Try/Confirm/Cancel接口。一致性好,不会出现SAGA扣钱最后转账失败的情况。TCC适用于订单业务和对中间状态有约束的业务。读者如果想进一步研究TCC,go语言可以参考DTM((https://github.com/yedf/dtm)),Java语言可以参考seata本地消息表localmessagetable这个解决方案原是发表于ebay架构师DanPritchett于2008年加入ACM,其设计核心是通过消息异步保证需要分布式处理的任务的执行。大致流程是这样的:写本地消息和业务操作放在一个事务中,保证了业务和发送消息的原子性,要么都成功,要么都失败。容错机制:当扣余额交易失败时,直接回滚交易,失败则无后续步骤轮询生产消息,余额增加交易失败则重试.本地消息表的特点:长事务只需要拆分成多个任务,使用简单的生产者需要额外创建一个消息表。每个本地消息表都需要轮询消费者的逻辑。如果重试失败,则需要更多的机制来回滚操作。适用于可以异步执行的业务,以及后续操作不需要回滚的业务事务消息在上面的本地消息表方案中,生产者需要额外创建一个消息表并轮询本地消息表,这有沉重的业务负担。阿里巴巴开源的RocketMQ4.3及以后版本正式支持事务消息,本质上是将本地消息表放到RocketMQ上,解决生产端消息发送和本地事务执行的原子性问题。事务性消息的发送和提交:发送一条消息(半消息)服务端存储消息,根据发送结果响应消息的写入结果执行本地事务(写入失败则半消息不可见)此时业务,不执行本地逻辑)根据本地事务状态执行Commit或Rollback(Commit操作发布消息,消息对消费者可见)。正常发送的流程图如下:补偿过程:对于没有Commit/Rollback的事务消息(处于pending状态的消息),从服务器发起一次“Checkback”Producer收到checkback消息,返回本地事务的状态对应消息,是Commit或者Rollback。事务消息方案与本地消息表机制非常相似,主要区别在于将原来相关的本地表操作换成了反向查找接口事务消息的特点如下:长事务只需要拆分成多个任务,并提供反向查询接口。如果简单消费者的逻辑无法通过重试,则需要更多的机制来回滚操作。可以异步执行的业务,后续操作不需要回滚。如果读者想进一步研究事务性消息,可以参考rocketmq。为了方便大家学习事务性消息,DTM((https://github.com/yedf/dtm))也提供了best-effortnotification的简单实现。通知者通过一定的机制,尽最大努力将业务处理结果通知给接收者。具体包括:有一定的消息重复通知机制。因为通知的接收者可能还没有收到通知,这时候肯定有一定的机制来重复通知消息。消息整理机制。如果接收者尽最大努力仍未收到通知,或者接收者消费消息后想再次消费消息,则接收者可以主动向通知者查询消息信息以满足需求。上面介绍的本地消息表和事务性消息都是可靠消息。它与此处介绍的尽力而为通知有何不同?可靠的消息一致性,发起通知方需要保证消息发出,消息发送到接收通知方。消息可靠性的关键是由发起通知者来保证的。尽力通知。发起方尽量将业务处理结果通知通知方,但可能收不到消息。在这种情况下,通知方需要主动调用通知方接口查询业务处理结果和通知的可靠性。关键在于通知的接收者。在解决方案上,besteffortnotification需要:提供一个接口,让通知的接收者可以通过接口查询业务处理结果消息队列ACK机制。逐渐增加通知间隔,直到达到通知要求的时间窗上限,然后不再通知。尽力而为通知适用于业务通知类型。例如,微信交易的结果是通过best-effort通知方式通知各个商户。既有回调通知又有通知。交易查询接口AT交易模式这是阿里开源项目seata中的一种交易模式,在蚂蚁金服中也称为FMT。好处是这种事务模式的用法和XA模式类似。无需为业务编写各种补偿操作,回滚由框架自动完成。缺点也和AT类似,有长期锁,不满足高并发场景。感兴趣的同学可以参考seata-AT。分布式事务中的网络异常可能会导致分布式事务各个方面的网络和业务故障等问题。这些问题都需要分布式事务的业务端实现三个特性:防空回滚、幂等、防挂起。下面用TCC事务说明这些异常:EmptyrollbackEmptyrollback:在没有调用TCC资源Try方法的情况下,调用第二阶段的Cancel方法。Cancel方法需要识别这是一个空回滚,然后直接返回success。这是因为当某个分支事务宕机或网络异常时,会记录该分支事务调用失败。这个时候Try阶段其实并没有执行。当故障恢复时,分布式事务回滚会调用第二阶段的Cancel方法,导致空回滚。Idempotent幂等性:由于任何请求都可能出现网络异常和重复请求,所有的分布式事务分支都需要保证幂等性Suspension挂起:对于分布式事务,两阶段的Cancel接口比Try接口先Execute更强大。原因是RPC调用分支事务try时,先注册分支事务,然后再执行RPC调用。如果此时RPC调用的网络拥塞,RPC超时后,TM会通知RM回滚分布式事务,可能回滚完成后,RPC请求到达actor进行实际执行。让我们看一张网络异常的时序图来更好地理解上面的问题。业务处理请求4时,Cancel在Try之前执行,需要处理空回滚业务处理请求6时,Cancel重复执行,需要幂等业务处理请求8时,Try在Cancel之后执行,需要被处理。面对上述复杂的网络异常,各公司提出的解决方案是通过业务方的唯一键查询关联操作是否完成。如果完成,直接返回成功。相关判断逻辑复杂,容易出错,业务负担大。在DTM项目中,出现了一种子事务屏障技术。使用这种技术,可以达到这种效果。看示意图:所有这些请求,在子事务屏障之后:异常请求会被过滤掉;正常请求将通过障碍。开发者使用子事务屏障后,上述异常都得到妥善处理,业务开发者只需要关注实际的业务逻辑,负担大大减轻。子事务屏障提供了方法ThroughBarrierCall。方法原型为:funcThroughBarrierCall(db*sql.DB,transInfo*TransInfo,busiCallBusiFunc)。业务开发人员在busiCall中编写自己的相关逻辑,调用该函数。ThroughBarrierCall保证在空回滚、挂起等场景下不会调用busiCall;当业务被重复调用时,具有幂等控制,保证只提交一次。子事务屏障将管理TCC、SAGA、XA、事务消息等,也可以扩展到其他领域。子事务屏障技术的原理是在本地数据库中创建一个分支事务状态表sub\_trans\_barrier,唯一key为全局事务id-子事务id-子事务分支名称(try|confirm|cancel)开启事务如果是Try分支,则insertignore插入gid-branchid-try,如果插入成功,则调用barrier中的逻辑如果是Confirm分支,则insertignore插入gid-branchid-confirm,如果插入成功,则调用barrier中的逻辑如果是Cancel分支,则insertignore插入gid-branchid-try,再插入gid-branchid-cancel,如果try没有插入则cancel插入成功,thenInvokethelogicinthebarrierbarrier中的逻辑返回成功,提交事务,返回成功barrier中的逻辑返回错误,回滚事务,返回错误在这种机制下,与网络异常相关的问题是解决空补控制——如果不执行Try,直接执行Cancel,则Cancel中插入gid-branchid-try会成功,不会遵循barrier中的逻辑,保证了空补的幂等控制control-任何分支都不能重复插入唯一键,保证不会重复执行反挂确保防挂控制是SAGA事务类似的机制。子事务屏障技术由DTM((https://github.com/yedf/dtm))首创。它的意义在于设计一个简单易实现的算法,并提供一个易于使用的界面。首先,它的意义在于设计一个简单易实现的算法,并提供一个简单易用的接口。有了这两项的帮助,开发者就可以完全从处理网络异常中解脱出来。该技术目前需要搭配DTM事务管理器,SDK已经提供给go语言的开发者。计划用于其他语言的SDK。对于其他的分布式事务框架,只要提供合适的分布式事务信息,就可以按照上述原则快速实现技术。总结本文介绍了分布式事务的一些基本理论,并对常用的分布式事务解决方案进行了说明。文章后半部分还给出了事务异常的原因、分类和优雅的解决方案。-结尾-