什么是分布式事务
分布式事务是指在分布式系统中涉及到多个数据库或多个应用程序之间的事务处理,这些数据库或应用程序可能分布在不同的物理节点上,甚至可能位于不同的地理位置。在分布式事务中,需要确保所有参与者的事务操作都能够保持一致性,即所有参与者的事务要么全部提交成功,要么全部回滚。
举个例子, 比如说我们在是春晚的直播舞台, 每个城市的场地需要安排乐队、灯光、音响等。为了确保音乐会成功,每个地方的准备工作必须协调一致,要么所有城市都准备妥当顺利开演,要么任何一个地方出问题了,所有城市都取消。
假设一个电商系统,用户下单后需要扣减库存、扣减账户余额、生成订单等操作。在单机环境下,可以将这些操作放在同一个事务中,保证原子性、一致性和持久性。但在分布式环境下,可能存在多个服务(如库存服务、账户服务、订单服务)分布在不同的物理节点上,此时需要确保所有服务操作的事务都能够同步进行,避免出现数据不一致的情况。
为了解决分布式事务的问题,出现了一些分布式事务解决方案,如XA协议、TCC事务、最大努力通知等。这些解决方案的实现方式各不相同,但都需要考虑如何确保所有参与者的事务操作能够保持一致性,以及如何处理可能出现的异常情况。
市面上常见的分布式事务的解决方案
分布式事务的目的是保证分布式系统中的多个参与方的数据能够保证一致性。
1.强一致性
1.二阶提交
如果想要实现强一致性,那么就一定要引入一个协调者,通过协调者来协调所有参与者来进行提交或者回滚。所以,这类方案包含基于XA规范的二阶段及三阶段提交、以及支持2阶段提交。
所谓的两个阶段是指:第一阶段:准备阶段(投票阶段)和第二阶段:提交阶段(执行阶段)
一阶段提交时,将数据和回滚日志记录在同一本地事务 二阶段是回滚一阶段的 日志进行反向补偿(如果一个事务失败,其他成功的事务将会回滚)
存在的问题
1、同步阻塞问题。执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
2、单点故障。由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
2.三阶提交
3PC最关键要解决的就是协调者和参与者同时挂掉的问题,所以3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。
相比于二阶提交的优点就是,会去询问上一个协调者是不是执行成功了。比如说我们约了一个朋友后天去吃饭(CanCommit), 朋友也都安排好时间答应了,到第二天,我会和朋友打电话,确认明天是否能够正常出行(PreCommit), 但是到第三天朋友会临时有急事, 就不去了, 然而这个时候我们就可以在出门前和他打一个电话, 询问他今天是否能正常出行(DoCommit),然后朋友就会给我们一个结果, 能不能出门(commit操作), 如果不行的话我们就只能改约了。
相比于2PC的优点就是:如果挂掉的那台机器已经执行了commit,那么协调者可以从所有未挂掉的参与者的状态中分析出来,并执行commit。如果挂掉的那个参与者执行了rollback,那么协调者和其他的参与者执行的肯定也是rollback操作。
存在的问题
在doCommit阶段,如果参与者无法及时接收到来自协调者的doCommit或者rebort请求时,会在等待超时之后,会继续进行事务的提交。比如说我们第三天出门的时候,打电话给朋友没有接到, 我们会认为他已经出门了, 我们也会继续进行操作 。所以,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。
2.最终一致性
1.基于本地消息表实现分布式事务
本地消息表其实也是借助消息来实现分布式事务的。
这个方案的主要思想是将分布式事务拆分为本地事务和消息事务两个部分,本地事务在本地数据库中进行提交或回滚,而消息事务则将消息写入消息中间件中,以实现消息的可靠投递和顺序性。一般来说的做法是,在发送消息之前,先创建一条本地消息,并且保证写本地业务数据的操作,和写本地消息记录的操作在同一个事务中。这样就能确保只要业务操作成功,本地消息一定可以写成功。然后再基于本地消息,调用MQ发送远程消息。消息发出去之后,等待消费者消费,在消费者端,接收到消息之后,做业务处理,处理成功后再修改本地消息表的状态。
简单来说, 就是我们A服务创建了一个本地消息, 基于本地消息发送给B服务, B服务接受到消息后消费, 然后再发送消息到A服务,消息发出去之后,等待A消费者消费,在消费者端,接收到消息之后,做业务处理,处理成功后再修改A本地消息表的状态。
这里我们只需要关注几个节点 如果3发送消息节点失败, 我们可以根据本地消息表的轮询,确保MQ能够正常到B服务,如果6,7失败, 这个时候就比较特殊了, 因为其实两个服务的数据其实是一致的, 但是A服务的本地消息表状态是错误的...., 这个时候我们可以基于3 去发送消息, 在上方改进, 查询B服务的状态, 如果消费成功后, 则修改A的本地消息表的状态。
缺点
1、消息堆积扫表慢
2、集中式扫表会影响正常业务
3、定时扫表存在延迟问题
2.最大努力通知
所谓最大努力通知,换句话说就是并不保证100%通知到。这种分布式事务的方案,通常也是借助异步消息进行通知的。
发送者将消息发送给消息队列,接收者从消息队列中消费消息。在这个过程中,如果出现了网络通信故障或者消息队列发生了故障,就有可能导致消息传递失败,即消息被丢失。因此,最大努力通知无法保证每个接收者都能成功接收到消息,但是可以尽最大努力去通知。
下面是一个简单的例子来说明最大努力通知的过程。假设有一个在线商城系统,顾客可以下订单购买商品。当顾客成功下单后,通知顾客订单已经确认。这个通知就可以采用最大努力通知的方式。
最大努力通知这种事务实现方案,一般用在消息通知这种场景中,因为这种场景中如果存在一些不一致影响也不大。
3.seata实现分布式事务
AT 模式:尽管设计为提供强一致性,但在分布式系统中完全实现强一致性是具有挑战性的,尤其是在面对网络分区或节点故障时。因此,尽管AT模式致力于达到强一致性,它在某些故障场景中可能只能保证最终一致性。
TCC 和 Saga 模式:这两种模式都设计为支持最终一致性。它们允许更大的灵活性和可伸缩性,因为它们通过明确的补偿机制来处理事务的不同阶段,从而逐步达到全局的一致性。
如何选择
在选择一个分布式事务方案的时候,需要考虑很多因素,结合自己的业务来做考量选择。一般来说可以有以下几种选择方式:
1、实现成本:根据项目开发和维护的难度、成本等方面来选择合适的分布式事务方案。这几种方案中,TCC和2PC的实现成本最高,业务侵入性也比较大。
另外,事务消息、本地消息表和最大努力通知都依赖消息中间件,所以如果已有业务已经接入了消息中间件的话,那么使用成本还算可控,否则就需要考虑消息中间件部署、维护和接入成本。而且同样是消息中间件,也不是所有的都支持事务消息,这个也是需要考量的一个重要因素。
2、一致性要求:在一致性方面,2PC和TCC属于是可以保证强一致性的,而其他的几种方案是最终一致性的方案。
根据业务情况,比如下单环节中,库存扣减和订单创建可以用强一致性来保证。而订单创建和积分扣减就可以用最终一致性即可。而对于一些非核心链路的操作,如核对等,可以用最大努力通知即可。
3、可用性要求:根据CAP理论,可用性和一致性是没办法同时保证的,所以对于需要保证高可用的业务,建议使用最大努力通知等最终一致性方案;对于可用性要求不高,但是需要保证高一致性的业务,可使用2PC等方案。
4、数据规模:对于利用消息中间的这种方案,其实不是特别适合业务量特别大的场景,有可能出现消息堆积导致一致性保障不及时。对于数据量大的场景,可以考虑Seata方案。