一文带你理解什么是分布式事务
一文带你理解什么是分布式事务
前言
之前在朋友圈看到有大佬推荐《微服务系统架构》一书,陆陆续续花了数个月阅读完成,是一本非常值得后端开发人员阅读的书籍,里面从分布式开始,一直讲到微服务架构,分布式一致性,分布式事务,虚拟化,事件溯源开发等等,有兴趣的朋友可以买来看看。
这篇文章部分内容摘自此书的分布式事务章节,还有一些则是笔者的理解和网上搜集的方案。
在开始之前,让我们先来讲一个小故事。
在一个古老的森林里,住着各种各样的小动物。它们和谐相处,但有一天,一只叫做小松鼠的小动物,发现了一个问题。每次它们要一起策划一场大型的晚宴时,总是发生混乱,因为食材、准备工作等事务无法很好地协同。
于是,小松鼠决定召集森林里的小动物们开了一次紧急会议,讨论如何解决这个问题。在会议上,各种小动物都纷纷发言,提出了各自的建议。
小松鼠说:“我们需要一种分布式事务的思维,确保每个环节都能协同工作 ,以保证我们的晚宴能够成功。”
老狐狸提出:“我们可以把晚宴的筹备过程分为几个阶段,每个阶段由不同的小动物负责。这样,每个阶段就像一个事务,只有在确认完成后,才能进入下一个阶段。”
小松鼠点头称赞道:“这个主意不错,我们可以将整个筹备过程分为采购、准备、烹饪和布置等几个阶段。每个阶段由不同的小动物团队负责。”
于是,小兔子负责食材采购,小熊负责筹备工作,小狸猫负责烹饪,而小鹿负责布置现场。每个小动物团队在完成自己的任务后,都会向其他团队发送通知,表示自己的任务已经完成,可以进入下一个阶段。
这样一来,整个晚宴的筹备过程就像是一个分布式事务系统,每个小动物团队都在各自的阶段完成任务,确保了晚宴的有序进行。最终,他们成功地举办了一场美妙的晚宴,大家都非常满意。
什么是分布式事务
在当今的软件系统中,一般都是牺牲强一致性来换取系统的高性能和高可用,只需要保证数据的最终一致性,一般来说这个达到最终一致性的时间需要在用户和产品层面是可以接受的。当然有一些系统对于一致性的要求更高,比如金融系统,金融系统一般都需要保持最终结果的强一致性,不然就会发生钱凭空消失的可能。
那么在以上场景中,我们如何保证我们数据的最终一致性呢?
一般来说我们会采用分布式事务、分布式锁等方案。
分布式事务与数据库事务一样,同样具有ACID(原子、一致、隔离、持久)四个数学,最终保证的是系统状态的一致性
那么什么是分布式事务呢?
现在的业务系统一般都是微服务架构,即使不是微服务架构,那么一个系统也会有多个服务来组成。
那么业务的一个完整流程很可能就需要分别调用散落在各个节点的各个不同服务上的接口,这个时候如果我们想要保证这个完整流程的一致性,那么就需要保证请求各个节点的各个服务,要么都成功,要么都失败。
如果使用强一致性方案,基本就靠『多阶段提交』这样的方式,如果采用弱一致性方案,那么可选择的方案就比较多。
分布式事务 VS 分布式一致性协议 VS 分布式锁 VS 数据库事务
分布式一致性
分布式一致性协议针对的是多个节点重复做相同的一件事情 ,主要处理的是多副本之间的一致性,更像是共识算法,比如Paxos算法、ZAB算法、Raft算法、Gossip算法。分布式一致性协议需要处理的任务逻辑是:
每个节点都要完成同一个任务并且保证节点之间的处理状态完全一致
分布式事务
分布式事务则针对的是几个不同的任务或流程 ,它们要捆绑在一起成功或者失败,要么都成功、要么都失败,要保证多个流程的一致性,针对的整体且完整流程的原子性
每个节点都要完成不同任务,并且保证同时成功或者同时失败
分布式锁
分布式锁则用于解决分布式系统中多个进程(任务)之间对共享资源的竞争问题 ,在单机系统中可以使用锁来确保同一时刻只有一个进程可以访问共享资源;在分布式系统下则必须通过分布式锁来做到对共享资源进行同步
每个节点都想拿到资源,但是同一时刻只能有一个节点拿到资源
分布式事务vs数据库事务
数据库事务相对来说比分布式事务会容易实现很多,数据库系统会将跨表事务的问题收拢到系统内部来处理,然后系统内部基于XA或者其他协议,结合MVCC事务锁之类的机制,就可以解决好这个问题
但是对于分布式事务而言,它涉及到的是散落在各个节点的各个服务之间的一致性,每个服务节点的数据存储机制和方案都是不固定的,因此就无法采用数据库事务的解决方案。
我们一般无法收归下游其他服务以及内部不同存储系统之间的事务。因此,针对业务层面的分布式事务的解决方案,一般的做法就是加一层或多层抽象:
● 增加外在的事务存储
● 要求事务参与方遵循某些约束,调用特定的一些API将事务信息进行上报关联
● 引入事务协调者,由它来根据参与方上报的信息做一些事务的驱动逻辑
分布式事务的关键点
● 创建事务一定要一个唯一主键,一般是事务ID,然后基于这个事务ID,关联一些事务信息的存储和各个子任务
● 需要一个协调者 来负责跟踪推进完整个事务,然后各参与者需要遵从一定的规范约束,基于事务ID,以幂等、对账能力为基础,实现相应的API
● 一致性要求高的场景,会有对资源做锁定或预留的做法,最终一致性要求的场景,则只需要符合预期即可。基于对资源要求的不同,会有一些常见的解决方案,例如多阶段协商提交、TCC、事务消息等
分布式事务常见解决方案
强一致性的几种方案
XA分布式事务协议(2PC)
XA分布式事务协议,大概分为两部分:事务管理器和本地资源管理器 。
其中本地资源管理器往往由数据库实现,比如Oracle、DB2都实现了XA接口,而事务管理器作为全局的调度者,负责各个本地资源的提交和回滚。
在分布式事务中,涉及到多个独立的资源管理器和一个事务管理器,XA协议允许事务管理器协调多个资源管理器,以确保分布式事务的一致性和原子性
XA的基本原理是把分布式事务分解成两阶段提交的过程
● 第一阶段(Prepare阶段):事务管理器向所有的资源管理器发送Prepare请求,并等待它们的响应。资源管理器接收到Prepare请求之后,会检查是否能够执行该事务,并返回响应的准备状态。如果所有的资源管理器都返回正确响应,那么进入到第二阶段
● 第二阶段(Commit):事务管理器向所有的资源管理器发送Commit请求,并等待它们的响应。资源管理器接收到对应的Commit请求之后,会根据之前的准备状态执行事务的提交操作,并返回相应的提交状态。如果所有的资源管理器你都返回提交状态,那么分布式事务就被提交成功。
在这个过程中出错了怎么办?
我们可以探讨以下几种情况
- 第一阶段某资源管理器出错:该资源无法响应事务管理器的Prepare请求,事务管理器会在一段时间之后重发请求,如果在达到最大重试次数之后仍然无法正确响应,那么事务管理器会把该资源管理器标记为失败,进而终止整个事务
- 第二阶段某资源管理器出错:该资源无法响应事务管理器的Commit请求,事务管理器会在一段时间之后重发请求,如果在达到最大重试次数之后仍然无法正确响应,那么事务管理器会把该资源管理器标记为失败,会向其他所有的资源管理器发送Rollback请求进行数据回滚,保障数据的一致性
- 事务管理器出错:一般事务管理器是整个XA分布式事务策略的性能瓶颈 ,一旦出错,可能会导致整个分布式事务的终止;我们一般会采用备份和冗余的方式来保持事务管理器的高可用性,如果事务管理器出现故障,可以由备份的事务管理器接管工作
XA的优点是简单易用,应用广泛。缺点主要还是在性能上,可能有单点故障,而且需要阻塞所有的资源管理器。也有一定的可能造成数据不一致,即第二阶段完成之后,因为网络原因只有一部分参与者进行了commit操作
3PC三阶段提交
3PC:Three-phase Commit Protocol 是在2PC之上的扩展的提交协议,主要是为了解决两阶段提交协议的阻塞问题,从原来的两个阶段拓展为三个阶段,增加了超时机制 。相比于2PC更加健壮和高效
3PC包含三个阶段:
● 准备阶段(CanCommit Phase):在准备阶段,事务管理器会向所有参与者发送CanCommit请求,询问它们是否可以进行事务提交。参与者在收到请求后,根据自身状态会做出对应的响应,可以是Yes/No
● 预提交阶段(PreCommit Phase):在预提交阶段,事务管理器根据收集到的所有参与者的响应来决定是否可以进行事务的预提交。当所有参与者都回送了Yes,那么此时协调者会向所有的参与者发送PreCommit请求以进行事务的预提交;参与者收到请求后会执行事务的预提交操作,并且将预提交状态返回给协调者
● 提交阶段(DoCommit Phase):在提交阶段,协调者根据收集到的所有参与者的预提交状态来决定是否最终提交事务。如果都为成功,协调者会向所有参与者发送DoCommit请求,表示最终提交事务;如果有任何参与者预提交状态为失败,那么则会发送DoAbort请求,表示事务终止
3PC较于2PC的优点在于引入了预提交阶段,以及在提交阶段引入了DoAbort请求。这样就可以在准备阶段就能知道是否有参与者不同意提交,避免了2PC中某个参与者失败导致的全阻塞问题,减少阻塞时间,提高分布式事务的效率和可用性
DTS方案
阿里有一个分布式事务框架DTS,用来保障在大规模分布式环境下事务的最终一致性。DTS从架构上分为xts-client和xts-server两部分,前者是一个签入客户端应用的JAR包,主要负责事务数据的写入和处理;后者是一个独立的系统,主要负责异常事务的恢复
最终一致性
TCC分段提交
实现分布式事务,最常用的方法就是二阶段提交协议和TCC,这两个算法的使用场景是不一样的,二阶段提交协议实现的是数据层面的事务 ,比如XA规范采用的就是二阶段提交;TCC实现的是业务层面的事务 ,比如当操作不仅仅是数据库操作,还涉及其他业务系统的访问操作时,就该考虑TCC了
TCC是一个分布式事务的处理模型,将事务拆解成Try Confirm Cancel三个步骤 ,在保证强一致性的同时,最大限度提高了系统的可伸缩性和可用性,又称为补偿事务。它的核心思想是针对每个操作都要注册一个与其对应的确认操作和补偿操作
确认操作和补偿操作必须是幂等的,因为这两个操作可能会失败重试。TCC不依赖于数据库的事务,而是在业务中实现了分布式事务,这样能减轻数据库的压力,一个业务操作需要实现Try、Confrim和Cancel的三个方法,对业务代码的侵入性也更强,实现的复杂度也越高。
本质上是一种乐观锁的方式 进行的分布式事务实现,通过三个阶段的操作来确保分布式事务的一致性,而不像2PC那样使用阻塞的方式来实现
TCC的三个阶段分别是
● Try:在Try阶段,协调者尝试预留所有参与者需要的资源,相当于一种试探机制,事务发起者会检查所有参与者的资源是否可用,并进行资源的预留。如果资源都可用,协调者会执行正常的业务操作,但并不提交事务
● Confirm:在Confirm阶段,协调者会向所有的参与者发出确认请求,要求提交事务。各个参与者会真正执行之前预留的业务操作,并将操作结果提交
● Cancel:如果任何一个参与者在Confirm阶段失败,或者在指定时间内没有收到Confirm请求,协调者会向所有参与者发出Cancel请求,参与者进行事务的回滚操作,取消之前的预留操作
与2PC的区别
- 阻塞与非阻塞:2PC是阻塞的,在准备阶段和提交阶段都可能阻塞;TCC是非阻塞的,在预留资源字段并不提交事务,只有所有资源都预留成功后才提交确认,这样能够提高事务执行效率
- 使用场景:基于乐观锁的TCC更适合业务的一致性,2PC更适合保证数据的一致性;且TCC的可用性要高于2PC
MQ实现分布式事务
MQ方案也称为非事务消息,这种方式比较常见,一个是由于市面上很多这种成熟的非事务消息的解决方案,一个是由于这些MQ的性能和吞吐量都比较好,可以满足大部分的业务场景
一个典型的流程如下就是:生产者先执行本地事务并将消息落库,状态标记为待发送,然后发送消息。如果发送成功,则将消息改为发送成功;如果发送失败则不修改标记
然后会起一个定时任务,定是从数据库捞取在一定时间内待发送的消息并将消息发送。为确保消息一定能消费,消费者一般采用手动ACK机制,并且最好需要支持幂等
在消费者端可能面临的问题是
1.消费者消费到消息之后,消费者要保证对应的业务操作要执行成功之后才能主动ACK。如果业务执行失败,消息不能失效或者丢失,这个可以用消息队列的持久化机制+备份的思想进行解决
2.消费者消费消息要能够在业务层面保持幂等,因为消费可能会失败,因此只有具有幂等性才能不影响业务,具体的方案可以采用唯一主键来解决
SAGA长流程分布式事务
SAGA用于处理有序的一长串的长流程的事务,相对来说,性能更好,没有资源锁定,无流程阻塞,但是不保证事务间的隔离性和原子性 ,需要业务侧根据需要处理可能的问题
SAGA的每个子事务都有一个补偿的接口,如果执行到某个阶段失败之后,则对已经成功的子事务按照栈顺序一次进行补偿操作。采用的就是将一个分布式事务拆成多个小的本地事务的思想,来保证分布式系统中的数据一致性。每个步骤都是一个本地事务,每个本地事务都是幂等的,即使在失败和重试的情况下,也不会产生额外的影响
Saga模式通常由以下几个步骤组成
● 发起者:事务发起者,负责启动和协调整个Saga事务的执行
● 局部事务:一个小的事务,每个局部事务对应一个本地操作或者服务
● 补偿:每个局部事务都有一个对应的补偿操作,用于回滚或者撤销之前的操作,如果一个局部事务失败,Saga将执行补偿操作来回滚已经执行的操作
● 协调器:负责控制整个Saga事务的执行流程,包括局部事务的提交和回滚
Saga分布式事务的优缺点
可靠性较强,而且将一个大的事务分成多个本地小事务,更加灵活;同时,Saga需要设计每个本地事务的逻辑和对应的回退操作,更加复杂,也需要精确的定义每个步骤的执行顺序,增加系统的复杂性