当前位置: 首页 > 科技观察

基于Seata探索分布式事务的实现

时间:2023-03-21 17:14:11 科技观察

作者:京东物流技术与数据智能部张硕1.背景知识随着业务的快速发展和业务的日益复杂,几乎每个公司的系统都会从单机变为分布式,尤其是微服务架构。随之而来,我们不可避免地会遇到分布式事务的问题。本文总结了几种通过seata框架实现分布式事务的解决方案。1.1ACID关系型数据库具备解决复杂事务场景的能力,关系型数据库的事务符合ACID特性。Atomicity:原子性(全部或全无)Consistency:一致性(数据库只有一种状态,不存在未确定的状态)Isolation:隔离(事务不互相干扰)Durability:持久性(一旦事务提交,数据库记录保持不变)1.2CAPCAP指的是分布式系统,包括三个要素:一致性(consistency)、可用性(availability)、分区容忍度(partitiontolerance),三者不能结合。C:Consistency,一致性,所有的数据变化都是同步的。A:Availability,可用性,即在可接受的时间范围内正确响应用户请求。P:Partitiontolerance,分区容错,即当某个节点或网络分区发生故障时,系统仍能提供满足一致性和可用性的服务。1.3BASEBASE理论主要是解决CAP理论中分布式系统的可用性和一致性无法结合的问题。BASE理论包括以下三个要素:BA:BasicallyAvailable,基本可用。S:SoftState,软状态,状态可以在一段时间内不同步。E:EventuallyConsistent,最终一致,最终的数据是一致的,而不是时不时的保持强一致性。2实现方式2.1两阶段提交第一阶段(准备阶段)TM通知所有参与事务的RM,并向每个RM发送准备消息。RM收到消息后进入准备阶段,要么直接返回失败,要么创建并执行一个本地事务,写入本地事务日志(redo和undolog),但不commit(这里只进行最少的commit操作)耗时的最后一步留给第二阶段执行)。第二阶段(commit/rollbackstage)Seata框架基于双阶段提交模式。从设计的角度来看,我们可以将整体分为三大模块,分别是TM、RM、TC。具体解释如下:TM(TransactionManager):全局事务管理器控制全局事务边界,负责全局事务开启、全局提交、全局回滚。RM(ResourceManager):资源管理器控制分支事务,负责分支注册、状态报告,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。TC(TransactionCoordinator):事务协调器维护全局事务的运行状态,负责协调和驱动全局事务的提交或回滚。一个典型的分布式事务流程:TM向TC申请启动一个全局事务,全局事务创建成功并生成一个全局唯一的XID。XID在微服务调用链的上下文中传播。RM向TC注册分支事务,将其纳入XID对应的全局事务的管辖范围。TM向TC发起针对XID的全局提交或回滚解决方案。TC调度由XID管理的所有分支事务以完成提交或回滚请求。2.2XA在XA模式下,每一个XA事务都是一个事务参与者。分布式事务启动后,先执行第一阶段的“xastart”、“businessSQL”、“xaend”和“xaprepare”,完成XA事务的执行和预提交;如果是第二阶段提交,则执行"xacommit",如果是回滚,则执行"xarollback"。这确保所有XA事务都被提交或回滚。不管Phase2的解析是commit还是rollback,事务资源的锁都必须一直保持到Phase2完成后才被释放。对于一个正常运行的业务来说,最后大概率应该有90%以上的交易提交成功。我们可以在第一阶段提交本地交易吗?这样在90%以上的情况下,都可以节省Phase2持有锁的时间,提高整体效率。分支事务中数据的本地锁由本地事务管理,在分支事务Phase1结束时释放。这时候其他本地事务就可以读取到最新的数据了。-同时,随着本地事务结束,连接也被释放。-分支事务中数据的全局锁由事务协调器管理。当Phase2的全局提交决定后,可以立即释放全局锁。注意先释放锁,再进行分支事务的commit过程。只有决定全局回滚时,才会持有全局锁,直到分支的Phase2结束,即所有分支事务的回滚结束。这种设计大大减少了分支事务对资源(数据和连接)的锁定时间,为提高整体并发和吞吐量提供了基础。但是,分布式事务的隔离级别发生了变化。XA模式和下面的AT模式一样,是对业务的非侵入式解决方案;但与AT模式不同的是,XA模式通过XA指令将快照数据和行锁委托给数据库来完成,这样XA模式更加轻量级。2.3ATAT模式是一种非侵入式的分布式事务解决方案。在AT模式下,用户只需要关注自己的“业务SQL”,即全局事务的第一阶段,Seata框架会自动生成该事务的第二阶段提交和回滚操作。第一阶段,应用需要使用Seata的JDBC数据源代理,也就是上面提到的RM概念,所有对DB的操作都是通过SeataRM代理完成的。在这一层代理中,Seata会自动控制SQL的执行、提交和回滚。Seataagent会将更新前后业务数据的数据镜像(beforeImage&afterImage)组织成一个回滚日志,利用本地事务的ACID特性,将业务数据的更新和回滚日志的写入提交到相同的本地交易。这样就可以保证提交的业务数据的任何更新,都必须有对应的回滚日志。然后,在提交本地事务之前,需要通过RM向TC注册本地分支。在注册过程中,会根据刚才执行的SQL获取所有涉及到的数据主键,并以resourceId+tableName+rowPK作为锁键向TC申请所有涉及数据的写锁,并执行的Commit流程获取所有相关数据的写锁后的本地事务。如果没有获取到任何一行数据的写锁,TC会以fastfail的形式回复RM,RM会以重试+超时的机制重复这个过程,直到超时。RM完成本地事务后,将本地事务的执行情况上报给TC,完成业务RPC的调用过程。Phase2case1:如果TM决定全局提交,此时分支事务实际上已经提交,TC立即释放全局事务的所有锁,然后异步调用RM清除回滚日志,Phase2可以很快完成。case2:如果决策是全局回滚,RM收到协调器的回滚请求,通过XID和BranchID找到对应的回滚日志记录,通过回滚记录生成并执行反向更新SQL,完成分支回滚。当分支回滚成功结束时,通知TC回滚完成,然后TC释放所有与分支事务相关的锁。注意:RM在回滚时,会先和afterImage比较:-如果一致:执行反向SQL-如果不一致:再和beforeImage比较-如果一致:表示不需要执行回滚SQL,数据已经restored-如果不一致:说明有脏数据,此时会抛出异常,需要人工处理。2.4TCCTCC模式需要用户根据自己的业务场景实现Try、Confirm、Cancel三种操作;事务发起者首先在TC中注册全局事务,然后执行第一阶段的Try方法。如果在第二阶段提交,TC会执行每个RM的Confirm方法。如果第二阶段回滚,TC会执行每个RM的Cancel方法。与AT模式一样,Seata在实际方法的执行中增加了一个切面,即拦截所有对TCC接口的调用。在调用Try接口时,如果发现是在全局事务中,切面会先向TC注册一个分支事务。与AT不同的是,TCC注册了一个没有加锁的分支事务,注册完成后才执行原来的RPC调用。当请求链接调用完成后,TC通过分支事务的资源ID回调到正确的参与者,执行相应TCC资源的Confirm或Cancel方法。TCC模式的整体框架比AT简单,主要是扫描TCC接口,注册资源,拦截接口调用,注册分支事务,最后回调二阶段接口。核心其实就是TCC接口的实现逻辑。1)使用原则从TCC模型的框架可以看出,TCC模型的核心在于TCC接口的设计。用户在接入TCC时,大部分工作都集中在如何实现TCC服务上。这是TCC模型的主要问题。对业务的侵入性比较大,实现TCC服务需要花费很大的精力。设计一套TCC接口最重要的是什么?主要有两点。第一点,操作需要分两个阶段进行。与XA等传统模型相比,TCC(Try-Confirm-Cancel)分布式事务模型的特点是不依赖RM对分布式事务的支持,而是通过分解业务逻辑来实现分布式事务。TCC分布式事务模型需要业务系统提供三块业务逻辑:1.初步运行Try:完成所有业务检查,并预留必要的业务资源。2、Confirm操作Confirm:真正执行业务逻辑,不做任何业务检查,只使用Try阶段预留的业务资源。所以只要Try操作成功,Confirm就一定成功。另外,Confirm操作需要幂等,保证一个分布式事务只能成功一次。3、取消操作Cancel:释放Try阶段预留的业务资源。同样,取消操作也需要满足幂等性。因此,TCC模型的隔离思想是对业务进行改造,在第一阶段结束后,从底层数据库资源层面的锁定,转为上层业务层面的锁定,释放底层数据库锁资源,并放宽分布式事务锁协议,最小化锁的粒度,最大化业务并发性能。第二点是根据自己的业务模型来控制并发。Seata框架本身只提供了两阶段原子提交协议来保证分布式事务的原子性。事务隔离需要交给业务逻辑来实现。隔离的本质是控制并发,防止并发事务操作同一资源造成混乱。例如:“A账户有100元,交易T1会扣30元,交易T2也会扣30元,存在并发。”在Try操作的第一阶段,需要使用数据库资源级别的锁来检查账户的可用余额。如果余额足够,则将预留的业务资源添加到各自的冻结中,并扣除交易金额。第一阶段结束后,虽然数据库层面的资源锁被释放,但是资金被业务隔离,不允许被除本次交易外的其他并发交易使用。2)异常控制空回滚空回滚是指对于分布式事务,如果不调用TCC资源Try方法,则调用第二阶段的Cancel方法。Cancel方法需要识别这是一个空回滚,然后直接返回成功。cancel需要识别空回滚,直接返回成功。关键是要识别这个空回滚。这个想法很简单。需要知道第一阶段是否执行。如果执行了,就是正常的回滚;如果不执行,则为空回滚。因此,需要额外的事务控制表,其中包含分布式事务ID和分支事务ID。第一阶段的Try方法中会插入一条记录,表示第一阶段已经执行完毕。在Cancel界面读取记录。如果记录存在,则正常回滚;如果记录不存在,它将回滚为空。Suspension暂停是指对于一个分布式事务,第二阶段的Cancel接口先于Try接口执行。因为允许空回滚,Cancel接口认为Try接口还没有执行,空回滚直接返回成功。对于Seata框架来说,认为分布式事务二阶段接口已经执行成功,整个分布式事务结束。但是之后Try方法才真正开始执行,预留业务资源。回想一下前面提到的事务并发控制的业务锁定,对于一个Try方法预留的业务资源,只能使用分布式事务。但是,Seata框架认为,分布式事务已经结束,也就是说,当这种情况发生时,任何人都无法处理分布式事务第一阶段预留的业务资源。比如进行RPC调用时,先注册分支事务,然后再执行RPC调用。如果此时RPC调用的网络拥塞,通常RPC调用有超时时间。RPC超时后,发起方会通知TC回滚分布式事务,可能回滚后,RPC请求到达参与者并真正执行,导致挂起。幂等性是指对于同一个分布式事务的同一个分支事务,重复调用该分支事务的第二阶段接口。因此需要TCC的两阶段Confirm和Cancel接口保证幂等性,不会被重用或释放资源。如果幂等控制没有做好,很可能会造成资金流失等严重问题。解决方案Try方法主要需要考虑两个问题。一是Try方法需要能够告诉二期接口业务资源预留成功。二是检查第二阶段是否已经执行,如果已经执行完成,则不会执行Confirm方法。因为Confirm方法不允许空回滚,即Confirm方法必须在Try方法之后执行。所以Confirm方法只需要关注重复提交的问题即可。需要有交易执行记录表,可以先锁定交易记录。如果交易记录为空,说明是空提交,不允许,终止执行。如果交易记录不为空,继续检查状态是否初始化。如果是,说明第一阶段执行正确,第二阶段可以正常执行。如果状态为提交,则认为是重复提交,直接返回成功即可;如果状态回滚,也是异常。已回滚的事务无法重新提交。需要能够拦截这种异常情况,并报警。取消方法。Cancel方法与Confirm方法略有不同,因为Cancel方法允许空回滚,Try方法如果先执行则需要知道Cancel已经执行。首先还是锁定交易记录。如果事务记录为空,则认为Try方法还没有执行,即空回滚。在空回滚的情况下,应该先插入一条事务记录,保证后面的Try方法不会被再次执行。如果插入成功,说明Try方法还没有执行,空回滚继续执行。如果插入失败,则认为正在执行Try方法,等待TC重试即可。如果一开始交易记录不为空,说明已经执行了Try方法,接下来查看状态是否初始化。如果是,则还没有执行其他二阶段方法,正常执行Cancel逻辑。如果状态是回滚,说明这是一次重复调用,允许幂等,直接返回成功即可。如果状态为committed,也是异常。已提交的事务不能再次回滚。2.5SagaSaga模式是Seata即将开源的长事务解决方案。Saga模式下,分布式事务有多个参与者,每个参与者都是一个反向补偿服务,需要用户根据业务场景实现其正向操作和反向回滚操作。在分布式事务的执行过程中,各个参与者的前向操作是依次执行的。如果所有转发操作都执行成功,则分布式事务被提交。如果任何一个正向操作失败,分布式事务将返回执行之前参与者的反向回滚操作,回滚已提交的参与者,使分布式事务回到初始状态。Saga转发服务和补偿服务也需要业务开发者来实现。有点像TCC模式结合了Try流程和Confirm流程。所有参与者直接执行Try+Confirm。如果有人失败,他们会以相反的顺序取消。由于该模式主要用于长交易场景,通常由事件驱动,在参与者之间异步执行。Saga模式适用于业务流程较长,需要保证事务最终一致性的业务系统。Saga模式会一次性提交本地事务,在无锁和长进程的情况下可以保证性能。Saga模式的优点是:一次性提交本地数据库事务,无锁,高性能;参与者可以使用事务驱动异步执行,吞吐量高;补偿服务是正向服务的“逆向”,简单易懂,易于实施;缺点:Saga模式是一次性提交本地数据库事务,并且没有“reserve”动作,隔离性无法保证。事务隔离纵观Seata提供的所有分支事务模式,除了AT模式和XA模式可以运行在readcommitted隔离级别,其他模式都运行在readuncommitted级别。必要时,应用需要通过业务逻辑的巧妙设置,解决分布式事务隔离级别带来的问题。AT模式使用全局写排他锁来保证事务之间的写隔离,默认定义全局事务为读未提交。在隔离级别,全局事务读取不提交。并不是说本地事务的db数据没有正常提交,而是第二阶段commit|全局事务的回滚还没有处理完(也就是全局锁还没有释放),此时其他事务会读取第一阶段提交的内容。如果有些应用需要实现globalreadcommitted,AT也提供了相应的机制来实现,即selectforupdate+@GlobalLock,执行该命令时,RM会去TC确认锁是否为他人所拥有,所以如果一个分布式事务T1在进行时,另一个事务T2会因为锁冲突而阻塞后续代码的执行。当前面的分布式事务T1结束时,对应的资源锁被释放,T2可以读取到对应的2.6消息组件,使用MQ组件实现的两阶段提交。该方案涉及3个模块:上游应用、业务执行、MQ消息发送。可靠的消息服务和MQ消息组件协调上下游消息的传递,保证上下游数据的一致性。下游应用程序监听MQ消息并执行自己的业务。上游应用将本地业务执行和消息发送绑定在同一个本地事务中,保证要么本地操作成功发送MQ消息,要么都失败回滚。上游应用将待确认消息发送给可靠消息系统。可靠消息系统保存待确认消息,返回给上游应用执行本地服务。上游应用通知可靠消息系统确认服务已经执行并发送消息。可靠消息系统将消息状态修改为发送状态,并将消息传递给MQ中间件。下游应用监听MQ消息组件,获取消息。下游应用根据MQ消息体信息处理本地业务。下游应用自动向MQ组件发送ACK确认消息。下游应用通知可靠消息系统消息消费成功。可靠消息将消息的状态更改为已完成。异常处理上游异常可靠消息服务定期监测消息的状态。如果有状态为pendingconfirmation的消息超时,说明上游应用与可靠消息交互的第4步或第5步出现异常。可靠消息查询超时消息待确认状态向上游应用查询业务执行状态。如果业务没有执行,则删除消息,保证业务的一致性和可靠的消息服务。如果业务已经执行,则修改待发送消息的状态,将消息发送给MQ组件。下游异常可靠消息服务周期性查询状态为发送超时的消息。可靠消息在MQ组件中重新投递消息给下游应用监听消息,在满足幂等性的情况下重新执行业务。下游应用程序通知可靠的消息服务消息已被成功消费。在实际过程中,还需要引入人工干预功能。例如,引入了重传次数限制,如果消息超过了重传次数限制,则将该消息变为死消息,等待人工处理。3总结3.1AT其实是一个自己实现的XAtransactiononsqlsupport。其实可以知道AT在sql支持上是远远不如XA模式的。AT需要做sql解析后面的实现,只能自己解决。目前只能靠社区的贡献这是一个长期的关键问题,很多用户选择在AT模式下重写sql以获得AT模式支持,而XA则是sql支持的全胜。3.2隔离AT模式是通过解析SQL获取涉及到的主键id,生成行锁。即AT模式的隔离由全局锁来保证,粒度细到行级。锁信息存储在seata服务器端。XA的隔离级别由本地数据库保证,锁存储在各个本地数据库中。一旦在XA模式下执行prepare,XA事务就不能再重新进入,也不能和其他XA事务共享锁,因为XA协议只是通过XID启动一个事务,并没有分支这种东西交易。也就是说,他只关心自己。3.3侵入性通过以上信息,可以发现谁在下层,侵入性较小。因此,数据库本身支持的XA模式无疑是侵入性最小、使用成本最低的。XA的RM其实是在数据库中,而AT作为中间件层部署在应用端,不依赖于数据库本身的协议支持,这对于微服务架构来说至关重要。应用层不需要针对本地事务和分布式事务的不同场景适配多套不合理的驱动。补偿交易处理机制建立在交易资源之上。事务资源本身并不知道分布式事务。有一个根本问题是事务资源不感知分布式事务,即无法实现真正??的全局一致性。比如一条库存记录在补偿事务处理中,会从100减到50,这时候仓库管理员在连接数据库的时候会查询到50。事务异常回滚后,库存会得到补偿,回滚到100,显然仓库管理员查询到的50是脏数据。XA的价值是多少?与补偿事务不同,XA协议需要事务资源本身来提供对规范和协议的支持。由于事务资源感知并参与分布式事务处理过程,事务资源从任何角度都可以保证数据访问的有效隔离。比如上面的XA事务处理过程中,中间状态50不会被查询(当然隔离级别上面要读提交),为了满足全局一致性。