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

终于有人澄清了“分布式事务”!

时间:2023-03-22 11:06:24 科技观察

一个复杂的系统往往是从一个小而简单的系统衍生出来的。为了满足不断增长的业务需求,系统的复杂度不断增加,逐渐从单一架构向分布式架构发展。分布式系统架构的设计主要着眼于:高性能、高可用性、高扩展性。图片来自PexelsDistributedTransactions高可用性是指系统能够不间断地执行功能,代表系统的可用性,是设计系统时必须遵循的原则之一。高可用的实现无非就是冗余。就存储的高可用而言,问题不在于如何进行数据备份,而在于如何避免数据不一致对业务的影响。对于分布式系统来说,要保证分布式系统中的数据一致性,需要一个能够保证数据在子系统中始终一致,避免业务问题的解决方案。这种实现称为分布式事务,要么一起成功,要么一起失败,必须是一个整体事务。理论基础在具体方案讲解之前,需要先了解一下分布式中数据设计的理论基础,CAP理论和BASE理论,为后续实践做铺垫。CAP理论CAP,ConsistencyAvailabilityPartitiontolerance的缩写:Consistency:一致性,对于一个客户端来说,读操作可以返回最新的写操作结果。可用性:可用性,非故障节点在合理的时间内返回合理的响应。Partitiontolerance:分区容错。当发生网络分区时,系统可以继续提供服务。你知道什么是网络分区吗?因为在分布式系统中,系统必须部署在多台机器上,网络不能保证100%可靠,所以网络分区必须存在,即P必须存在。网络分区发生后,可用性和一致性问题就出现了。我们要在两者之间做一个权衡,所以有两种架构:CP架构AP架构①CP架构当网络分区发生时,为了保证一致性,必须拒绝请求,否则不能保证一致性:当没有网络分区,系统A和系统B的数据是一致的,X=1。修改系统A的X为2,X=2。当发生网络分区时,系统A和系统B之间的数据同步失败,系统B的X=1。当客户端请求系统B时,为了保证一致性,系统B应该拒绝服务请求,并返回错误代码或错误信息。上述方法违反了可用性要求,只满足一致性和分区容错,即CP、CAP理论忽略了网络延迟,忽略了从系统A向系统B同步数据的网络延迟。CP架构保证客户端获取数据时,一定是最新的写操作,否则获取异常信息,永远不会出现数据不一致的情况。②AP架构当发生网络分区时,为了保证可用性,系统B可以返回旧值保证系统可用性:当没有发生网络分区时,系统A和系统B的数据是一致的,X=1。修改系统A的X为2,X=2。当发生网络分区时,系统A和系统B之间的数据同步失败,系统B的X=1。当客户端请求系统B时,为了保证可用性,此时系统B应该返回旧值,X=1。上述方法违反了一致性要求,只满足可用性和分区容错性,即AP。AP架构保证了无论客户端在获取数据时返回的是最新值还是旧值,系统都必须可用。CAP理论侧重于数据的粒度而不是整体系统设计策略。BASE理论BASE理论指的是BasicAvailable、SoftState和EventualConsistency。其核心思想是即使无法实现强一致性,也应该采用合适的方法来保证最终一致性。BASE,BasicallyAvailableSoftStateEventualConsistency的缩写:BA:BasicallyAvailable基本可用。当分布式系统发生故障时,允许其失去部分可用性,即保证核心可用。S:SoftState,让系统有一个不影响系统整体可用性的中间状态。E:Consistencyfinalconsistency,系统中的所有数据副本经过一定时间后最终可以达到一致的状态。BASE理论本质上是CAP理论的延伸,是CAP中AP方案的补充。DistributedTransactionProtocolX/OpenXAProtocolXA是Tuxedo提出的分布式事务协议。XA规范主要定义了(全局)事务管理器(TransactionManager)和(本地)资源管理器(ResourceManager)之间的接口。XA接口是一个双向的系统接口,在事务管理器(TransactionManager)和一个或多个资源管理器(ResourceManager)之间形成了沟通的桥梁。XA协议使用两阶段提交方法来管理分布式事务。XA接口为资源管理器和事务管理器之间的通信提供了一个标准接口。2PC:Two-phasecommitprotocol两阶段提交是指一种算法(Algorithm),旨在使基于分布式系统架构的所有节点在提交事务时保持一致性。通常,两阶段提交也称为协议(Protocol)。在分布式系统中,虽然每个节点都可以知道自己的操作是成功还是失败,但是无法知道其他节点的操作是成功还是失败。当一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个组件作为协调器,统一控制所有节点(称为参与者)的操作结果,并最终指示这些节点是否进行操作结果realcommit(如将更新数据写入磁盘等)。因此,两阶段提交的算法思想可以概括为:参与者通知协调器操作成功或失败,然后协调器根据每个参与者决定是提交操作还是中止操作所有参与者的反馈信息。两阶段提交算法的建立基于以下假设:在分布式系统中,有一个节点作为协调者(Coordinator),其他节点作为参与者(Cohorts)。并且节点之间可以进行网络通信。所有节点都使用预先写入的日志,日志写入后保存在可靠的存储设备上,即使节点损坏,日志数据也不会消失。所有节点都不会永久损坏,即使损坏后仍然可以恢复。两阶段提交分为两个阶段:投票阶段提交阶段投票阶段准备:协调器询问所有参与者是否可以执行提交操作,并开始等待每个参与者的响应。参与者执行事务操作,如果执行成功则返回Yes响应,如果执行失败则返回No响应。如果协调器接受参与者响应超时,它也会认为事务操作执行失败。提交阶段Commit:如果第一阶段所有参与者都返回Yes响应,协调器向所有参与者发送提交请求,所有参与者提交事务。如果第一阶段有一个或多个参与者返回No响应,则协调器向所有参与者发送回滚请求,所有参与者执行回滚操作。两阶段提交的优点:尽量保证数据的强一致性,但不是100%一致。双阶段提交的缺点:单点故障,由于协调器的重要性,一旦协调器发生故障,参与者将一直处于阻塞状态,尤其是在第二阶段,当协调器发生故障时,所有参与者都处于锁定事务资源状态,无法继续完成事务操作。同步阻塞,由于所有节点在执行操作时都是同步阻塞的,当参与者占用公共资源时,其他第三方节点必须处于阻塞状态才能访问公共资源。数据不一致。第二阶段,当协调者向参与者发送提交事务请求时,在发送提交事务请求的过程中发生本地网络异常或协调者失败,将导致只有部分参与者收到提交事务要求。这些参与者收到提交交易的请求后,将执行交易提交操作。但是,其他没有收到提交交易请求的参与者不能提交交易。这导致分布式系统中的数据不一致。双阶段提交的问题:如果协调者在第二阶段发送提交请求后挂掉了,而唯一收到这条消息的参与者执行完也挂掉了,即使协调者通过选举协议产生了新的协调者并且通知其他参与者提交或回滚操作可能与此已执行参与者执行的操作不同。当挂掉的参与者恢复时,会出现数据不一致的情况。3PC:Three-phasecommitprotocol三阶段提交(Three-phasecommit),旨在解决两阶段提交协议的不足。与两阶段提交不同,三阶段提交是一种“非阻塞”协议。三阶段提交在两阶段提交的第一阶段和第二阶段之间插入了一个准备阶段,使得在原来的两阶段提交中,参与者投票后,由于协调器崩溃或错误,参与者在解决了无法知道是提交还是中止的“不确定状态”造成的潜在相当大的延迟问题。三阶段提交的三个阶段:CanCommitPreCommitDoCommit①查询阶段:CanCommit协调器向参与者发送Commit请求,参与者如果可以提交则返回Yes响应,否则返回No响应。②准备阶段:PreCommit协调器根据查询阶段参与者的响应判断是执行交易还是中断交易:如果所有参与者都返回Yes,则执行交易。如果一个或多个参与者返回否或超时,则中止交易。参与者在执行操作后返回ACK响应,同时开始等待最后的命令。③提交阶段:DoCommit协调器在准备阶段根据参与者的响应判断是执行事务还是中断事务:如果所有参与者都返回正确的ACK响应,则提交事务。如果一个或多个参与者收到不正确的ACK响应或超时,则中止交易。如果参与者未能及时收到协调者的提交或中断事务请求,它将在等待超时后继续提交事务。协调者收到所有参与者的ACK响应并完成事务。解决两阶段提交的问题:在三阶段提交中,如果协调器在第三阶段发送提交请求后挂掉,并且唯一接受的参与者在执行提交操作后也挂掉,则协调器将通过选举协议催生了一个新的协调员。两阶段提交的问题在于,新的协调者不确定已经执行事务的参与者是提交了事务还是中断了事务。但是在三阶段提交中,必须经过二阶段的重新确认,那么二阶段一定已经正确执行了事务操作,只等待事务提交。因此,新的协调器可以从第二阶段开始分析应该执行的操作,并提交或中断事务操作,这样即使挂掉的参与者恢复,数据也是一致的。因此,三阶段提交解决了两阶段提交可能存在的协调者和参与者同时挂掉的数据一致性问题和单点故障问题,减少了阻塞。因为一旦参与者没有及时收到协调者的信息,他就会默认提交事务,而不是持有事务资源,处于阻塞状态。三阶段提交的问题:在提交阶段,如果发送中断事务请求,但由于网络问题,部分参与者没有收到请求。然后参与者会在等待超时后执行提交事务操作,这样由于网络问题提交事务的参与者的数据将与收到中断事务请求的参与者的数据不一致。所以2PC和3PC都不能保证分布式系统中的数据100%一致。举个解决方案的例子:在一个电商网站中,当用户下单一个商品时,需要在order表中创建一个订单数据,同时需要当前商品的剩余库存在库存表中进行修改。两步操作是添加和修改。我们必须保证这两个步骤必须同时成功或同时失败,否则会出现业务问题。成立时:业务量不大,用户量少,系统只有单一的结构,订单表和库存表在一个数据库中。这时候可以使用MySQL本地事务来保证数据的一致性。发展期:业务快速发展,用户量增加,单数据出现性能瓶颈。数据库按业务纬度分为订单数据库和库存数据库。由于跨库、跨机器,MySQL的本地事务已经无法保证Inventory数据的一致性。成熟期:业务扩展,单一的架构已经不能满足需求,演化为分布式系统。此时订单和库存已经拆分为两个子系统提供服务,子系统之间通过RPC进行通信。但是不管系统怎么开发,都要保证业务不出问题,订单和库存的数据是一致的。这个时候我们就必须思考应该如何保证服务之间的数据一致性。具有多个数据源的强一致分布式事务单一架构。在业务开发中,必须先对订单库进行操作,但不提交交易,然后对库存库进行操作,不提交交易。如果两个操作都成功,则事务一起提交,如果一个操作失败,则两个都回滚。基于2PC/XA协议的JTA:我们已经知道2PC和XA协议的原理,而JTA是Java规范,是XA在Java上的实现。JTA(JavaTransactionManager):事务管理器:常用方法,可以开启、回滚、获取事务。begin(),rollback()...XAResouce:资源管理,通过Session进行事务管理,commit(xid)...XID:每个事务分配一个特定的XID。JTA的主要原则是两阶段提交。当整个业务完成时,只有第一阶段提交。在第二阶段提交之前,它将检查所有其他事务是否已提交。如果有错误或者没有提交,那么第二阶段不会提交,而是直接回滚,这样所有的事务都会被回滚。基于JTA方案,实现了分布式事务的强一致性。JTA的特点:基于两阶段提交,可能存在数据不一致的情况。交易时间过长,阻塞。这种跨库操作是否应该出现在正常的架构设计中,我觉得不应该。如果按照业务拆分将数据源拆分成数据库,我们应该同时拆分服务。宜遵循一个系统只操作一个数据源(主从无所谓),避免以后多个系统可能调用一个数据源的情况。最终一致的分布式事务方案JTA方案适用于在单一架构中存在多个数据源时实现分布式事务,但对于微服务间的分布式事务则无能为力。我们需要使用其他方案来实现分布式事务。①本地消息表本地消息表的核心思想是将分布式事务拆分成本地事务进行处理。以本文为例,在订单系统中添加一个消息表,将新订单和新消息放入一个事务中完成,然后通过轮询的方式查询消息表,推送消息到MQ,库存系统消费MQ.执行流程:订单系统,添加订单和消息,在交易中提交。订单系统通过定时任务轮询状态为未同步的消息表发送给MQ。如果发送失败,请重试发送。库存系统,接收MQ消息,修改库存表,需要保证幂等操作。如果修改成功,则调用RPC接口修改订单系统消息表的状态为完成或直接删除消息。如果修改失败,可以先不管,等待重试。由于业务问题,订单系统中的消息可能会重复发送,所以为了避免这种情况,您可以记录发送次数,当次数达到限制时,报警并人工接入处理;库存系统需要保证幂等性,避免同一条消息被多次消费后数据一致。本地消息表方案实现最终一致性。需要在业务系统中增加一张消息表,在业务逻辑中多一个插入DB的操作,性能会有所损失,最终一致性的间隔时间主要由定时任务的间隔时间决定。决定。②MQ消息事务消息事务的原理是通过消息中间件将两个事务异步解耦。订单系统执行自己的本地事务并发送MQ消息,库存系统接收消息并执行自己的本地事务。乍一看好像和本地消息表的实现差不多,只是省略了对本地消息表的操作和轮询发送MQ的操作,但实际上两种方案的实现是不一样的。消息事务必须保证业务操作和消息发送的一致性。如果业务操作成功,则消息也必须成功传递。消息事务依赖于消息中间件的事务消息。RocketMQ基于消息中间件的两阶段提交实现,支持事务消息。执行过程:将Prepare消息发送给消息中间件。发送成功后,执行本地事务。如果事务执行成功,则Commit,消息中间件将消息发送给消费者。如果事务执行失败,会回滚,消息中间件会删除Prepare消息。消费者收到消息并消费它。如果消费失败,会继续重试。该方案也实现了最终一致性。相比本地消息表实现方案,无需构建消息表,不再依赖本地数据库事务,更适合高并发场景。③BestEffortNotificationBestEffortNotification比前两种方案实现更简单,适用于一些对最终一致性要求不高的业务,如支付通知、短信通知等。以支付通知为例,业务系统调用支付平台进行支付,支付平台进行支付。操作支付完成后,支付平台会尽量通知业务系统支付操作是否成功,但会有最大通知次数。如果超过此次数通知失败,则不再通知,业务系统调用支付平台提供查询接口,供业务系统查询支付操作是否成功。执行过程:业务系统调用支付平台的支付接口并在本地记录,支付状态为进行中。支付平台在进行支付操作后,无论成功还是失败,都需要将结果通知给业务系统。如果一直通知失败,会按照重试规则进行重试。达到最大通知数量后,将不再发出通知。支付平台提供查询订单支付操作结果的接口。业务系统根据一定的业务规则在支付平台上查询支付结果。该方案也实现了最终一致性。④补偿交易TCCTCC,Try-Confirm-Cancel的简称,对于每一个操作,都需要有相应的确认和取消操作。操作成功时调用确认操作,操作失败时调用取消操作,类似于两阶段提交,只不过这里的提交和回滚是针对业务的,所以基于分布式事务onTCC也可以看作是对业务的一种补偿机制。TCC的三个阶段:Try阶段:检查业务系统,储备资源。确认阶段:确认并提交业务系统。当Try阶段执行成功和Confirm阶段执行时,默认的Confirm阶段不会出错。即:只要Try成功,Confirm就一定成功。取消阶段:业务执行出错需要回滚时取消业务,释放预留资源。Try阶段是对业务系统进行检查,预览资源,如订单、入库等操作。需要检查剩余库存是否充足,并进行储备。如果预留操作是为可用库存数量创建一个字段,那么Try阶段的操作就是对这个可用库存数量进行操作。例如下一个订单减去一??个库存:执行过程:Try阶段:订单系统设置当前订单状态为付款,库存系统检查当前剩余库存数量是否大于1,然后设置可用库存数量到剩余库存数量-1。如果Try阶段执行成功,执行Confirm阶段,修改订单状态为支付成功,修改剩余库存数量为可用库存数量。如果Try阶段执行失败,执行Cancel阶段,修改订单状态为支付失败,修改可用库存数量为剩余库存数量。基于TCC实现分布式事务,代码逻辑比较复杂,需要将原有接口的逻辑拆分为三个接口:Try、Confirm、Cancel。基于TCC的分布式事务框架:ByteTCC,github.com/liuyangmingtcc-transaction:github.com/changmingxi看完之后,你应该对分布式事务有了一个大概的了解。在实际生产中,我们应该尽量避免使用分布式事务,如果可以转化为本地事务,就使用本地事务。如果一定要使用分布式事务,就需要从业务角度多考虑哪种方案更合适。简而言之,在采取行动之前要多思考。