分布式事务原理及解决方案
一、分布式事务
首先奉上一张关于事务的相关概念图,给大家做个总览:
1.1 名词解释
- 事务:事务是由一组操作构成的可靠的独立的工作单元,事务具备
ACID
的特性,即原子性、一致性、隔离性和持久性。 - 本地事务:当事务由资源管理器本地管理时被称作本地事务。本地事务的优点就是支持严格的
ACID
特性,高效,可靠,状态可以只在资源管理器中维护,而且应用编程模型简单。但是本地事务不具备分布式事务的处理能力,隔离的最小单位受限于资源管理器。 - 全局事务:当事务由全局事务管理器进行全局管理时成为全局事务,事务管理器负责管理全局的事务状态和参与的资源,协同资源的一致提交回滚。
TX
协议:应用或者应用服务器与事务管理器的接口。XA
协议:全局事务管理器与资源管理器的接口。XA
是由X/Open
组织提出的分布式事务规范。该规范主要定义了全局事务管理器和局部资源管理器之间的接口。主流的数据库产品都实现了XA
接口。XA
接口是一个双向的系统接口,在事务管理器以及多个资源管理器之间作为通信桥梁。之所以需要XA
是因为在分布式系统中从理论上讲两台机器是无法达到一致性状态的,因此引入一个单点进行协调。由全局事务管理器管理和协调的事务可以跨越多个资源和进程。全局事务管理器一般使用XA
二阶段协议与数据库进行交互。AP
:应用程序,可以理解为使用DTP
(Data Tools Platform
)的程序。RM
:资源管理器,这里可以是一个DBMS
或者消息服务器管理系统,应用程序通过资源管理器对资源进行控制,资源必须实现XA
定义的接口。资源管理器负责控制和管理实际的资源。TM
:事务管理器,负责协调和管理事务,提供给AP
编程接口以及管理资源管理器。事务管理器控制着全局事务,管理事务的生命周期,并且协调资源。- 两阶段提交协议:
XA
用于在全局事务中协调多个资源的机制。TM
和RM
之间采取两阶段提交的方案来解决一致性问题。两节点提交需要一个协调者(TM
)来掌控所有参与者(RM
)节点的操作结果并且指引这些节点是否需要最终提交。两阶段提交的局限在于协议成本,准备阶段的持久成本,全局事务状态的持久成本,潜在故障点多带来的脆弱性,准备后,提交前的故障引发一系列隔离与恢复难题。 BASE
理论:BA
指的是基本业务可用性,支持分区失败,S
表示柔性状态,也就是允许短时间内不同步,E
表示最终一致性,数据最终是一致的,但是实时是不一致的。原子性和持久性必须从根本上保障,为了可用性、性能和服务降级的需要,只有降低一致性和隔离性的要求。CAP
定理:对于共享数据系统,最多只能同时拥有CAP
其中的两个,任意两个都有其适应的场景,真是的业务系统中通常是ACID
与CAP
的混合体。分布式系统中最重要的是满足业务需求,而不是追求高度抽象,绝对的系统特性。C
表示一致性,也就是所有用户看到的数据是一样的。A
表示可用性,是指总能找到一个可用的数据副本。P
表示分区容错性,能够容忍网络中断等故障。
分布式事务与分布式锁的区别?
分布式锁解决的是分布式资源抢占的问题;分布式事务和本地事务是解决流程化提交问题。
1.2 本地事务简介
1.3 分布式事务的基本概念
1.3.1 分布式环境的事务复杂性
当本地事务要扩展到分布式时,它的复杂性进一步增加了。
存储端的多样性
首先就是存储端的多样性。本地事务的情况下,所有数据都会落到同一个DB
中,但是,在分布式的情况下,就会出现数据可能要落到多个DB
,或者还会落到Redis
,落到MQ
等中。
存储端多样性,如下图所示:
事务链路的延展性
本地事务的情况下,通常所有事务相关的业务操作,会被我们封装到一个Service
方法中。而在分布式的情况下,请求链路被延展,拉长,一个操作会被拆分成多个服务,它们呈现线状或网状,依靠网络通信构建成一个整体。在这种情况下,事务无疑变得更复杂。
事务链路延展性,如下图所示:
基于上述两个复杂性,期望有一个统一的分布式事务方案,能够像本地事务一样,以几乎无侵入的方式,满足各种存储介质,各种复杂链路,是不现实的。
至少,在当前,还没有一个十分成熟的解决方案。所以,一般情况下,在分布式下,事务会被拆分解决,并根据不同的情况,采用不同的解决方案。
1.3.2 什么是分布式事务?
对于分布式系统而言,需要保证分布式系统中的数据一致性,保证数据在子系统中始终保持一致,避免业务出现问题。分布式系统中对事务要么一起成功,要么一起失败,必须是一个整体性的事务。
分布式事务指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。
简单的说,在分布式系统上一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务节点上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。
举个例子:在电商网站中,用户对商品进行下单,需要在订单表中创建一条订单数据,同时需要在库存表中修改当前商品的剩余库存数量,两步操作一个添加,一个修改,我们一定要保证这两步操作一定同时操作成功或失败,否则业务就会出现问题。
任何事务机制在实现时,都应该考虑事务的ACID
特性,包括:本地事务、分布式事务。对于分布式事务而言,即使不能都很好的满足,也要考虑支持到什么程度。
典型的分布式事务场景:
1. 跨库事务
跨库事务指的是,一个应用某个功能需要操作多个库,不同的库中存储不同的业务数据。下图演示了一个服务同时操作2
个库的情况:
2. 分库分表
通常一个库数据量比较大或者预期未来的数据量比较大,都会进行水平拆分,也就是分库分表。如下图,将数据库B
拆分成了2
个库:
对于分库分表的情况,一般开发人员都会使用一些数据库中间件来降低sql操作的复杂性。如,对于sql
:insert into user(id,name) values (1,"tianshouzhi"),(2,"wangxiaoxiao")
。这条sql
是操作单库的语法,单库情况下,可以保证事务的一致性。但是由于现在进行了分库分表,开发人员希望将1
号记录插入分库1
,2
号记录插入分库2
。所以数据库中间件要将其改写为2
条sql
,分别插入两个不同的分库,此时要保证两个库要不都成功,要不都失败,因此基本上所有的数据库中间件都面临着分布式事务的问题。
3. 微服务化
微服务架构是目前一个比较火的概念。例如某个应用同时操作了9
个库,这样的应用业务逻辑必然非常复杂,对于开发人员是极大的挑战,应该拆分成不同的独立服务,以简化业务逻辑。拆分后,独立服务之间通过RPC
框架来进行远程调用,实现彼此的通信。下图演示了一个3
个服务之间彼此调用的架构:
Service A
完成某个功能需要直接操作数据库,同时需要调用Service B
和Service C
,而Service B
又同时操作了2
个数据库,Service C
也操作了一个库。需要保证这些跨服务的对多个数据库的操作要不都成功,要不都失败,实际上这可能是最典型的分布式事务场景。
分布式事务实现方案必须要考虑性能的问题,如果为了严格保证ACID
特性,导致性能严重下降,那么对于一些要求快速响应的业务,是无法接受的。
1.4 CAP定理
1.4.1 分布式事务的理论基础
数据库事务ACID
四大特性,无法满足分布式事务的实际需求,这个时候又有一些新的大牛提出一些新的理论。
1.4.2 CAP定理
CAP
定理是由加州大学伯克利分校Eric Brewer
教授提出来的,他指出WEB
服务无法同时满足一下3
个属性:
- 一致性(
Consistency
):客户端知道一系列的操作都会同时发生(生效) - 可用性(
Availability
):每个操作都必须以可预期的响应结束 - 分区容错性(
Partition tolerance
):即使出现单个组件无法可用,操作依然可以完成
具体地讲在分布式系统中,一个Web
应用至多只能同时支持上面的两个属性。因此,设计人员必须在一致性与可用性之间做出选择。
2000年7月Eric Brewer
教授仅仅提出来的是一个猜想,2
年后,麻省理工学院的Seth Gilbert
和Nancy Lynch
从理论上证明了CAP
理论,并且而一个分布式系统最多只能满足CAP
中的2
项。之后,CAP
理论正式成为分布式计算领域的公认定理。
所以,CAP
定理在迄今为止的分布式系统中都是适用的!
1、一致性(Consistency)
数据一致性指“all nodes see the same data at the same time”,即更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致,不能存在中间状态。
分布式环境中,一致性是指多个副本之间能否保持一致的特性。在一致性的需求下,当一个系统在数据一致的状态下执行更新操作后,应该保证系统的数据仍然处理一致的状态。
例如对于电商系统用户下单操作,库存减少、用户资金账户扣减、积分增加等操作必须在用户下单操作完成后必须是一致的。不能出现类似于库存已经减少,而用户资金账户尚未扣减,积分也未增加的情况。如果出现了这种情况,那么就认为是不一致的。
数据一致性分为强一致性、弱一致性、最终一致性
- 如果的确能像上面描述的那样时刻保证客户端看到的数据都是一致的,那么称之为强一致性。
- 如果允许存在中间状态,只要求经过一段时间后,数据最终是一致的,则称之为最终一致性。
- 此外,如果允许存在部分数据不一致,那么就称之为弱一致性。
2、可用性(Availability)
系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。
两个度量的维度:
(1)有限时间内
对于用户的一个操作请求,系统必须能够在指定的时间(响应时间)内返回对应的处理结果,如果超过了这个时间范围,那么系统就被认为是不可用的 。即这个响应时间必须在一个合理的值内,不让用户感到失望。
试想,如果一个下单操作,为了保证分布式事务的一致性,需要10分钟才能处理完,那么用户显然是无法忍受的。
(2)返回正常结果
要求系统在完成对用户请求的处理后,返回一个正常的响应结果。正常的响应结果通常能够明确地反映出对请求的处理结果,即成功或失败,而不是一个让用户感到困惑的返回结果。比如返回一个系统错误如OutOfMemory
,则认为系统是不可用的。
“返回结果”是可用性的另一个非常重要的指标,它要求系统在完成对用户请求的处理后,返回一个正常的响应结果,不论这个结果是成功还是失败。
3、分区容错性(Partition tolerance)
即分布式系统在遇到任何网络分区故障时,仍然需要能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障。
网络分区,是指分布式系统中,不同的节点分布在不同的子网络(机房/异地网络)中,由于一些特殊的原因导致这些子网络之间出现网络不连通的状态,但各个子网络的内部网络是正常的,从而导致整个系统的网络环境被切分成了若干孤立的区域。组成一个分布式系统的每个节点的加入与退出都可以看做是一个特殊的网络分区。
1.4.3 CAP的应用
1、放弃P
放弃分区容错性的话,则放弃了分布式,放弃了系统的可扩展性
2、放弃A
放弃可用性的话,则在遇到网络分区或其他故障时,受影响的服务需要等待一定的时间,再此期间无法对外提供政策的服务,即不可用
3、放弃C
放弃一致性的话(这里指强一致),则系统无法保证数据保持实时的一致性,在数据达到最终一致性时,有个时间窗口,在时间窗口内,数据是不一致的。
对于分布式系统来说,P
是不能放弃的,因此架构师通常是在可用性和一致性之间权衡。
CAP理论告诉我们:
目前很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。
基于CAP
理论,很多系统在设计之初就要对这三者做出取舍:
任何一个分布式系统都无法同时满足一致性(
Consistency
)、可用性(Availability
)和分区容错性(Partition tolerance
),最多只能同时满足两项。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证最终一致性。
问:为什么分布式系统中无法同时保证一致性和可用性?
答:首先一个前提,对于分布式系统而言,分区容错性是一个最基本的要求,因此基本上我们在设计分布式系统的时候只能从一致性(C
)和可用性(A
)之间进行取舍。
如果保证了一致性(C
):对于节点N1
和N2
,当往N1
里写数据时,N2
上的操作必须被暂停,只有当N1
同步数据到N2
时才能对N2
进行读写请求,在N2
被暂停操作期间客户端提交的请求会收到失败或超时。显然,这与可用性是相悖的。
如果保证了可用性(A
):那就不能暂停N2
的读写操作,但同时N1
在写数据的话,这就违背了一致性的要求。
1.4.4 CAP权衡
通过
CAP
理论,我们知道无法同时满足一致性、可用性和分区容错性这三个特性,那要舍弃哪个呢?
对于多数大型互联网应用的场景,主机众多、部署分散,而且现在的集群规模越来越大,所以节点故障、网络故障是常态,而且要保证服务可用性达到N
个9
,即保证PA
,舍弃C
(退而求其次保证最终一致性)。虽然某些地方会影响客户体验,但没达到造成用户流程的严重程度。对于涉及到钱财这样不能有一丝让步的场景,C
必须保证。网络发生故障宁可停止服务,这是保证CA
,舍弃P
。貌似这几年国内银行业发生了不下10
起事故,但影响面不大,报道也不多,广大群众知道的少。还有一种是保证CP
,舍弃A
。例如网络故障是只读不写。
1.4.5 CAP和ACID中的A和C是完全不一样的
A的区别:
ACID
中的A
指的是原子性(Atomicity
),是指事务被视为一个不可分割的最小工作单元,事务中的所有操作要么全部提交成功,要么全部失败回滚;CAP
中的A
指的是可用性(Availability
),是指集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求;
C的区别:
ACID
一致性是有关数据库规则,数据库总是从一个一致性的状态转换到另外一个一致性的状态;CAP
的一致性是分布式多服务器之间复制数据令这些服务器拥有同样的数据,由于网速限制,这种复制在不同的服务器上所消耗的时间是不固定的,集群通过组织客户端查看不同节点上还未同步的数据维持逻辑视图,这是一种分布式领域的一致性概念;
总之:
ACID
里的一致性指的是事务执行前后,数据库完整性,而CAP
的一致性,指的是分布式节点的数据的一致性。背景不同,无从可比
1.5 BASE定理
CAP
是分布式系统设计理论,BASE
是CAP
理论中AP
方案的延伸,对于C
我们采用的方式和策略就是保证最终一致性;
eBay
的架构师Dan Pritchett
源于对大规模分布式系统的实践总结,在ACM
上发表文章提出BASE
理论,BASE
理论是对CAP
理论的延伸,核心思想是即使无法做到强一致性(StrongConsistency
,CAP
的一致性就是强一致性),但应用可以采用适合的方式达到最终一致性(Eventual Consitency
)。
1.5.1 BASE定理
BASE
是基本可用(Basically Available
)、软状态(Soft state
)和最终一致性(Eventually consistent
)三个短语的缩写。BASE
基于CAP
定理演化而来,核心思想是即时无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。
1、基本可用(Basically Available)
基本可用是指分布式系统在出现不可预知的故障的时候,允许损失部分可用性,但不等于系统不可用。
(1)响应时间上的损失
当出现故障时,响应时间增加
(2)功能上的损失
当流量高峰期时,屏蔽一些功能的使用以保证系统稳定性(服务降级)
2、软状态(Soft state)
指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性。
与硬状态相对,即是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。
3、最终一致性(Eventually consistent)
强调系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。其本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
最终一致性可分为如下几种:
- (1)因果一致性(
Causal consistency
)
即进程A在更新完数据后通知进程B,那么之后进程B对该项数据的范围都是进程A更新后的最新值。 - (2)读己之所写(
Read your writes
)
进程A更新一项数据后,它自己总是能访问到自己更新过的最新值。 - (3)会话一致性(
Session consistency
)
将数据一致性框定在会话当中,在一个会话当中实现读己之所写的一致性。即执行更新后,客户端在同一个会话中始终能读到该项数据的最新值 - (4)单调读一致性(
Monotonic read consistency
)
如果一个进程从系统中读取出一个数据项的某个值后,那么系统对于该进程后续的任何数据访问都不应该返回更旧的值。 - (5)单调写一致性(
Monotoic write consistency
)
一个系统需要保证来自同一个进程的写操作被顺序执行。
BASE
理论是提出通过牺牲一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。
1.5.2 BASE理论的特点
BASE
理论面向的是大型高可用可扩展的分布式系统,和传统的事物ACID
特性是相反的。它完全不同于ACID
的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。但同时,在实际的分布式场景中,不同业务单元和组件对数据一致性的要求是不同的,因此在具体的分布式系统架构设计过程中,ACID
特性和BASE
理论往往又会结合在一起。
1.5.3 BASE理论与CAP的关系
BASE
理论是对CAP
中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于CAP
定理逐步演化而来的。BASE
理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性 。
BASE
理论其实就是对CAP
理论的延伸和补充,主要是对AP
的补充。牺牲数据的强一致性,来保证数据的可用性,虽然存在中间装填,但数据最终一致。
1.5.4 ACID和BASE的区别与联系
ACID
是传统数据库常用的设计理念,追求强一致性模型。BASE
支持的是大型分布式系统,提出通过牺牲强一致性获得高可用性。
ACID
和BASE
代表了两种截然相反的设计哲学,在分布式系统设计的场景中,系统组件对一致性要求是不同的,因此ACID
和BASE
又会结合使用。
二、分布式事务分类
分布式场景下,多个服务同时对服务一个流程,比如电商下单场景,需要支付服务进行支付、库存服务扣减库存、订单服务进行订单生成、物流服务更新物流信息等。如果某一个服务执行失败,或者网络不通引起的请求丢失,那么整个系统可能出现数据不一致的原因。
上述场景就是分布式一致性问题,追根到底,分布式一致性的根本原因在于数据的分布式操作,引起的本地事务无法保障数据的原子性引起。
分布式一致性问题的解决思路有两种,一种是分布式事务,一种是尽量通过业务流程避免分布式事务。分布式事务是直接解决问题,而业务规避其实通过解决出问题的地方(解决提问题的人)。其实在真实业务场景中,如果业务规避不是很麻烦的前提,最优雅的解决方案就是业务规避。
分布式事务实现方案从类型上去分刚性事务、柔型事务:
- 刚性事务满足
CAP
的CP
理论 - 柔性事务满足
BASE
理论(基本可用,最终一致)
2.1 刚性事务
刚性事务:通常无业务改造,强一致性,原生支持回滚/隔离性,低并发,适合短事务。
原则:刚性事务满足CAP
的CP
理论
刚性事务指的是,要使分布式事务,达到像本地式事务一样,具备数据强一致性,从
CAP
来看,就是说,要达到CP
状态。
刚性事务:XA
协议(2PC
(AT)、JTA
、JTS
)、3PC
,但由于同步阻塞,处理效率低,不适合大型网站分布式场景。
AT
模式是增强的2PC
。
2.2 柔性事务
柔性事务指的是,不要求强一致性,而是要求最终一致性,允许有中间状态,也就是Base
理论,换句话说,就是AP
状态。
与刚性事务相比,柔性事务的特点为:有业务改造,最终一致性,实现补偿接口,实现资源锁定接口,高并发,适合长事务。
柔性事务分为:
- 补偿型
- 异步确保型
- 最大努力通知型。
柔型事务:TCC/FMT
、Saga
(状态机模式、Aop
模式)、本地事务消息、消息事务(半消息)
三、刚性事务的分类
3.1 XA协议
3.1.1 XA模型或者X/Open DTP模型
X/OPEN
是一个组织.X/Open
国际联盟有限公司是一个欧洲基金会,它的建立是为了向UNIX
环境提供标准。它主要的目标是促进对UNIX
语言、接口、网络和应用的开放式系统协议的制定。它还促进在不同的UNIX
环境之间的应用程序的互操作性,以及支持对电气电子工程师协会(IEEE
)对UNIX
的可移植操作系统接口(POSIX
)规范。
X/Open DTP(Distributed Transaction Process) 是一个分布式事务模型。这个模型主要使用了两段提交(2PC - Two-Phase-Commit
)来保证分布式事务的完整性。
在X/Open DTP(Distributed Transaction Process) 模型里面,有三个角色:
应用程序(Application
简称AP
): 也就是业务层。哪些操作属于一个事务,就是AP
定义的。
事务管理器(Transaction Manager
简称TM
): 接收AP
的事务请求,对全局事务进行管理,管理事务分支状态,协调RM
的处理,通知RM
哪些操作属于哪些全局事务以及事务分支等等。这个也是整个事务调度模型的核心部分。
资源管理器(Resource Manager
简称RM
):一般是数据库,也可以是其他的资源管理器,如消息队列(如JMS
数据源),文件系统等。
XA
把参与事务的角色分成AP
,RM
,TM
。
AP
即应用,也就是我们的业务服务。RM
指的是资源管理器,即DB
,MQ
等。TM
则是事务管理器。
AP
自己操作TM
,当需要事务时,AP
向TM
请求发起事务,TM
负责整个事务的提交,回滚等。
XA
规范主要定义了(全局)事务管理器(TM
)和(局部)资源管理器(RM
)之间的接口。XA
接口是双向的系统接口,在事务管理器(TM
)以及一个或多个资源管理器(RM
)之间形成通信桥梁。
XA
之所以需要引入事务管理器是因为,在分布式系统中,从理论上讲(参考Fischer
等的论文),两台机器理论上无法达到一致的状态,需要引入一个单点进行协调。事务管理器控制着全局事务,管理事务生命周期,并协调资源。资源管理器负责控制和管理实际资源(如数据库或JMS
队列)
3.1.2 XA规范
什么是XA
?用非常官方的话来说:
XA
规范是X/Open
组织定义的分布式事务处理(DTP
,Distributed Transaction Processing
)标准。XA
规范描述了全局的事务管理器与局部的资源管理器之间的接口。XA
规范的目的是允许的多个资源(如数据库,应用服务器,消息队列等)在同一事务中访问,这样可以使ACID
属性跨越应用程序而保持有效。XA
规范使用两阶段提交(2PC
,Two-Phase Commit
)协议来保证所有资源同时提交或回滚任何特定的事务。XA
规范在上世纪90
年代初就被提出。目前,几乎所有主流的数据库都对XA
规范提供了支持。
XA
规范(XA Specification
)是X/OPEN
提出的分布式事务处理规范。XA
则规范了TM
与RM
之间的通信接口,在TM
与多个RM
之间形成一个双向通信桥梁,从而在多个数据库资源下保证ACID
四个特性。目前知名的数据库,如Oracle
、DB2
、MySQL
等,都是实现了XA
接口的,都可以作为RM
。
XA
是数据库的分布式事务,强一致性,在整个过程中,数据一张锁住状态,即从prepare
到commit
、rollback
的整个过程中,TM
一直把持折数据库的锁,如果有其他人要修改数据库的该条数据,就必须等待锁的释放,存在长事务风险。
以下的函数使事务管理器可以对资源管理器进行的操作 :
1)xa_open
,xa_close
:建立和关闭与资源管理器的连接。
2)xa_start
,xa_end
:开始和结束一个本地事务。
3)xa_prepare
,xa_commit
,xa_rollback
:预提交、提交和回滚一个本地事务。
4)xa_recover
:回滚一个已进行预提交的事务。
5)ax_
开头的函数使资源管理器可以动态地在事务管理器中进行注册,并可以对XID
(Transaction Ids
)进行操作。
6)ax_reg,ax_unreg
;允许一个资源管理器在一个TMS
(Transaction Manager Server
)中动态注册或撤消注册。
XA各个阶段的处理流程
3.1.3 XA协议的实现
1、2PC/3PC协议
两阶段提交(2PC
)协议是XA
规范定义的数据一致性协议。
三阶段提交(3PC
)协议对2PC
协议的一种扩展。
2、JTA规范
作为java
平台上事务规范JTA
(Java Transaction API
)也定义了对XA
事务的支持,实际上,JTA
是基于XA
架构上建模的,在JTA
中,事务管理器抽象为javax.transaction.TransactionManager
接口,并通过底层事务服务(即JTS
)实现。像很多其他的java
规范一样,JTA
仅仅定义了接口,具体的实现则是由供应商(如J2EE
厂商)负责提供,目前JTA
的实现主要由以下几种:
1.J2EE
容器所提供的JTA
实现(JBoss
)
2.独立的JTA
实现:如JOTM
,Atomikos
。
这些实现可以应用在那些不使用J2EE
应用服务器的环境里用以提供分布事事务保证。如Tomcat
,Jetty
以及普通的java
应用。
JTA
规范下载地址:Java Transaction API
3、JTS规范
事务是编程中必不可少的一项内容,基于此,为了规范事务开发,Java
增加了关于事务的规范,即JTA
和JTS
JTA
定义了一套接口,其中约定了几种主要的角色:TransactionManager
、UserTransaction
、Transaction
、XAResource
,并定义了这些角色之间需要遵守的规范,如Transaction
的委托给TransactionManager
等。
JTS
也是一组规范,上面提到JTA
中需要角色之间的交互,那应该如何交互?JTS
就是约定了交互细节的规范。
总体上来说JTA
更多的是从框架的角度来约定程序角色的接口,而JTS
则是从具体实现的角度来约定程序角色之间的接口,两者各司其职。
3.1.4 XA的主要限制
- 必须要拿到所有数据源,而且数据源还要支持
XA
协议。目前MySQL
中只有InnoDB
存储引擎支持XA
协议。 - 性能比较差,要把所有涉及到的数据都要锁定,是强一致性的,会产生长事务。
3.2 2PC(标准XA模型)
2PC
即Two-Phase Commit
,二阶段提交。
3.2.1 详解:二个阶段
广泛应用在数据库领域,为了使得基于分布式架构的所有节点可以在进行事务处理时能够保持原子性和一致性。绝大部分关系型数据库,都是基于2PC
完成分布式的事务处理。
顾名思义,2PC
分为两个阶段处理,阶段一:提交事务请求、阶段二:执行事务提交;如果阶段一超时或者出现异常,2PC
的阶段二:中断事务
1、阶段一:提交事务请求
- 事务询问。协调者向所有参与者发送事务内容,询问是否可以执行提交操作,并开始等待各参与者进行响应;
- 执行事务。各参与者节点,执行事务操作,并将
Undo
和Redo
操作计入本机事务日志; - 各参与者向协调者反馈事务问询的响应。成功执行返回
Yes
,否则返回No
。
2、阶段二:执行事务提交
协调者在阶段二决定是否最终执行事务提交操作。这一阶段包含两种情形:
执行事务提交
所有参与者reply Yes
,那么执行事务提交。
- 发送提交请求。协调者向所有参与者发送
Commit
请求; - 事务提交。参与者收到
Commit
请求后,会正式执行事务提交操作 ,并在完成提交操作之后,释放在整个事务执行期间占用的资源; - 反馈事务提交结果。参与者在完成事务提交后,写协调者发送
Ack
消息确认; - 完成事务。协调者在收到所有参与者的
Ack
后,完成事务。
3、阶段二:中断事务
事情总会出现意外,当存在某一参与者向协调者发送No
响应,或者等待超时。协调者只要无法收到所有参与者的Yes
响应,就会中断事务。
- 发送回滚请求。协调者向所有参与者发送
Rollback
请求; - 回滚。参与者收到请求后,利用本机
Undo
信息,执行Rollback
操作。并在回滚结束后释放该事务所占用的系统资源; - 反馈回滚结果。参与者在完成回滚操作后,向协调者发送
Ack
消息; - 中断事务。协调者收到所有参与者的回滚
Ack
消息后,完成事务中断。
3.2.2 2pc解决的是分布式数据强一致性问题
顾名思义,两阶段提交在处理分布式事务时分为两个阶段:voting
(投票阶段,有的地方会叫做prepare
阶段)和commit
阶段。
2pc
中存在两个角色,事务协调者(seata、atomikos、lcn)和事务参与者,事务参与者通常是指应用的数据库。
3.2.3 2PC二阶段提交的特点
1、2PC方案比较适合单体应用
2PC
方案中,有一个事务管理器的角色,负责协调多个数据库(资源管理器)的事务,事务管理器先问问各个数据库你准备好了吗?如果每个数据库都回复ok
,那么就正式提交事务,在各个数据库上执行操作;如果任何其中一个数据库回答不ok
,那么就回滚事务。
2PC
方案比较适合单体应用里,跨多个库的分布式事务,而且因为严重依赖于数据库层面来搞定复杂的事务,效率很低,绝对不适合高并发的场景。
2PC
方案实际很少用,一般来说某个系统内部如果出现跨多个库的这么一个操作,是不合规的。我可以给大家介绍一下,现在微服务,一个大的系统分成几百个服务,几十个服务。一般来说,我们的规定和规范,是要求每个服务只能操作自己对应的一个数据库。
如果你要操作别的服务对应的库,不允许直连别的服务的库,违反微服务架构的规范,你随便交叉胡乱访问,几百个服务的话,全体乱套,这样的一套服务是没法管理的,没法治理的,可能会出现数据被别人改错,自己的库被别人写挂等情况。
如果你要操作别人的服务的库,你必须是通过调用别的服务的接口来实现,绝对不允许交叉访问别人的数据库。
2、2PC具有明显的优缺点
优点主要体现在实现原理简单;
缺点比较多:
2PC
的提交在执行过程中,所有参与事务操作的逻辑都处于阻塞状态,也就是说,各个参与者都在等待其他参与者响应,无法进行其他操作;- 协调者是个单点,一旦出现问题,其他参与者将无法释放事务资源,也无法完成事务操作;
- 数据不一致。当执行事务提交过程中,如果协调者向所有参与者发送
Commit
请求后,发生局部网络异常或者协调者在尚未发送完Commit
请求,即出现崩溃,最终导致只有部分参与者收到、执行请求。于是整个系统将会出现数据不一致的情形; - 保守。
2PC
没有完善的容错机制,当参与者出现故障时,协调者无法快速得知这一失败,只能严格依赖超时设置来决定是否进一步的执行提交还是中断事务。
实际上分布式事务是一件非常复杂的事情,两阶段提交只是通过增加了事务协调者(Coordinator)的角色来通过2
个阶段的处理流程来解决分布式系统中一个事务需要跨多个服务节点的数据一致性问题。但是从异常情况上考虑,这个流程也并不是那么的无懈可击。
假设如果在第二个阶段中Coordinator
在接收到Partcipant
的"Vote_Request"后挂掉了或者网络出现了异常,那么此时Partcipant
节点就会一直处于本地事务挂起的状态,从而长时间地占用资源。当然这种情况只会出现在极端情况下,然而作为一套健壮的软件系统而言,异常Case
的处理才是真正考验方案正确性的地方。
3、总结一下:XA-两阶段提交协议中会遇到的一些问题
- 1.性能问题
从流程上我们可以看得出,其最大缺点就在于它的执行过程中间,节点都处于阻塞状态。各个操作数据库的节点此时都占用着数据库资源,只有当所有节点准备完毕,事务协调者才会通知进行全局提交,参与者进行本地事务提交后才会释放资源。这样的过程会比较漫长,对性能影响比较大。
- 2.协调者单点故障问题
事务协调者是整个XA
模型的核心,一旦事务协调者节点挂掉,会导致参与者收不到提交或回滚的通知,从而导致参与者节点始终处于事务无法完成的中间状态。
- 3.丢失消息导致的数据不一致问题
在第二个阶段,如果发生局部网络问题,一部分事务参与者收到了提交消息,另一部分事务参与者没收到提交消息,那么就会导致节点间数据的不一致问题。
3.3 3PC
针对2PC
的缺点,研究者提出了3PC
,即Three-Phase Commit
。作为2PC
的改进版,3PC
将原有的两阶段过程,重新划分为CanCommit
、PreCommit
和doCommit
三个阶段。
3.3.1 详解:三个阶段
1、阶段一:CanCommit
- 事务询问。协调者向所有参与者发送包含事务内容的
canCommit
的请求,询问是否可以执行事务提交,并等待应答; - 各参与者反馈事务询问。正常情况下,如果参与者认为可以顺利执行事务,则返回
Yes
,否则返回No
。
2、阶段二:PreCommit
在本阶段,协调者会根据上一阶段的反馈情况来决定是否可以执行事务的PreCommit
操作。有以下两种可能:
执行事务预提交
- 发送预提交请求。协调者向所有节点发出
PreCommit
请求,并进入prepared
阶段; - 事务预提交。参与者收到
PreCommit
请求后,会执行事务操作,并将Undo
和Redo
日志写入本机事务日志; - 各参与者成功执行事务操作,同时将反馈以
Ack
响应形式发送给协调者,同事等待最终的Commit
或Abort
指令。
中断事务
加入任意一个参与者向协调者发送No
响应,或者等待超时,协调者在没有得到所有参与者响应时,即可以中断事务:
- 发送中断请求。协调者向所有参与者发送
Abort
请求; - 中断事务。无论是收到协调者的
Abort
请求,还是等待协调者请求过程中出现超时,参与者都会中断事务;
3、阶段三:doCommit
在这个阶段,会真正的进行事务提交,同样存在两种可能。
执行提交
- 发送提交请求。假如协调者收到了所有参与者的
Ack
响应,那么将从预提交转换到提交状态,并向所有参与者,发送doCommit
请求; - 事务提交。参与者收到
doCommit
请求后,会正式执行事务提交操作,并在完成提交操作后释放占用资源; - 反馈事务提交结果。参与者将在完成事务提交后,向协调者发送
Ack
消息; - 完成事务。协调者接收到所有参与者的
Ack
消息后,完成事务。
中断事务
在该阶段,假设正常状态的协调者接收到任一个参与者发送的No
响应,或在超时时间内,仍旧没收到反馈消息,就会中断事务:
- 发送中断请求。协调者向所有的参与者发送
abort
请求; - 事务回滚。参与者收到
abort
请求后,会利用阶段二中的Undo
消息执行事务回滚,并在完成回滚后释放占用资源; - 反馈事务回滚结果。参与者在完成回滚后向协调者发送
Ack
消息; - 中端事务。协调者接收到所有参与者反馈的
Ack
消息后,完成事务中断。
3.3.2 2PC和3PC的区别
3PC
有效降低了2PC
带来的参与者阻塞范围,并且能够在出现单点故障后继续达成一致;
但3PC
带来了新的问题,在参与者收到preCommit
消息后,如果网络出现分区,协调者和参与者无法进行后续的通信,这种情况下,参与者在等待超时后,依旧会执行事务提交,这样会导致数据的不一致。
1、2PC和3PC的区别
三阶段提交协议在协调者和参与者中都引入超时机制 ,并且把两阶段提交协议的第一个阶段拆分成了两步:询问,然后再锁资源,最后真正提交。三阶段提交的三个阶段分别为:can_commit
,pre_commit
,do_commit
。
在doCommit
阶段,如果参与者无法及时接收到来自协调者的doCommit
或者abort
请求时,会在等待超时之后,继续进行事务的提交。(其实这个应该是基于概率来决定的,当进入第三阶段时,说明参与者在第二阶段已经收到了PreCommit
请求,那么协调者产生PreCommit
请求的前提条件是他在第二阶段开始之前,收到所有参与者的CanCommit
响应都是Yes
。(一旦参与者收到了PreCommit
,意味他知道大家其实都同意修改了)所以,一句话概括就是,当进入第三阶段时, 由于网络超时等原因,虽然参与者没有收到commit
或者abort
响应,但是他有理由相信:成功提交的几率很大)
2、3PC主要解决的单点故障问题
相对于2PC
,3PC
主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit
。而不会一直持有事务资源并处于阻塞状态 。
但是这种机制也会导致数据一致性问题,因为,由于网络原因,协调者发送的abort
响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit
操作。这样就和其他接到abort
命令并执行回滚的参与者之间存在数据不一致的情况。
3、3PC相对于2PC而言到底优化了什么地方呢?
相比较2PC
而言,3PC
对于协调者(Coordinator
)和参与者(Partcipant
)都设置了超时时间,而2PC
只有协调者才拥有超时机制。这解决了一个什么问题呢?
这个优化点,主要是避免了参与者在长时间无法与协调者节点通讯(协调者挂掉了)的情况下,无法释放资源的问题,因为参与者自身拥有超时机制会在超时后,自动进行本地commit
从而进行释放资源。而这种机制也侧面降低了整个事务的阻塞时间和范围。
另外,通过CanCommit
、PreCommit
、doCommit
三个阶段的设计,相较于2PC
而言,多设置了一个缓冲阶段保证了在最后提交阶段之前各参与节点的状态是一致的。
以上就是3PC
相对于2PC
的一个提高(相对缓解了2PC
中的前两个问题),但是3PC
依然没有完全解决数据不一致的问题。假如在doCommit
过程,参与者A无法接收协调者的通信,那么参与者A会自动提交,但是提交失败了,其他参与者成功了,此时数据就会不一致。
3.4 XA规范的问题
但是XA
规范在1994
年就出现了,至今没有大规模流行起来,必然有他一定的缺陷:
- 数据锁定:数据在事务未结束前,为了保障一致性,根据数据隔离级别进行锁定。
- 协议阻塞:本地事务在全局事务没
commit
或callback
前都是阻塞等待的。 - 性能损耗高:主要体现在事务协调增加的RT成本,并发事务数据使用锁进行竞争阻塞。
XA
协议比较简单,而且一旦商业数据库实现了XA
协议,使用分布式事务的成本也比较低。但是,XA
也有致命的缺点,那就是性能不理想,特别是在交易下单链路,往往并发量很高,XA
无法满足高并发场景。XA
目前在商业数据库支持的比较理想,在MySQL
数据库中支持的不太理想,MySQL
的XA
实现,没有记录prepare
阶段日志,主备切换回导致主库与备库数据不一致。许多nosql
也没有支持XA
,这让XA
的应用场景变得非常狭隘。
其实也并非不用,例如在IBM
大型机上基于CICS
很多跨资源是基于XA
协议实现的分布式事务,事实上XA
也算分布式事务处理的规范了,但在为什么互联网中很少使用,究其原因有以下几个:
- 性能(阻塞性协议,增加响应时间、锁时间、死锁);
- 数据库支持完善度(
MySQL 5.7
之前都有缺陷); - 协调者依赖独立的
J2EE
中间件(早期重量级Weblogic
、Jboss
、后期轻量级Atomikos
、Narayana
和Bitronix
); - 运维复杂,
DBA
缺少这方面经验; - 并不是所有资源都支持
XA
协议;
准确讲XA
是一个规范、协议,它只是定义了一系列的接口,只是目前大多数实现XA
的都是数据库或者MQ
,所以提起XA
往往多指基于资源层的底层分布式事务解决方案。其实现在也有些数据分片框架或者中间件也支持XA
协议,毕竟它的兼容性、普遍性更好。
四、柔性事务的分类
在电商领域等互联网场景下,刚性事务在数据库性能和处理能力上都暴露出了瓶颈。
柔性事务有两个特性:基本可用和柔性状态。
- 基本可用是指分布式系统出现故障的时候允许损失一部分的可用性。
- 柔性状态是指允许系统存在中间状态,这个中间状态不会影响系统整体的可用性,比如数据库读写分离的主从同步延迟等。柔性事务的一致性指的是最终一致性。
柔性事务主要分为补偿型和通知型。补偿型事务都是同步的,通知型事务都是异步的。
补偿型事务:TCC
、Saga
;
通知型事务:MQ
事务消息、最大努力通知型。
4.1 通知型事务
通知型事务的主流实现是通过MQ
(消息队列)来通知其他事务参与者自己事务的执行状态,引入MQ
组件,有效的将事务参与者进行解耦,各参与者都可以异步执行,所以通知型事务又被称为异步事务。
通知型事务主要适用于需要异步更新数据,并且对数据的实时性要求较低的场景,主要包含:异步确保型事务和最大努力通知事务两种。
- 异步确保型事务:主要适用于内部系统的数据最终一致性保障,因为内部相对比较可控,如订单和购物车、收货与清算、支付与结算等等场景;
- 最大努力通知:主要用于外部系统,因为外部的网络环境更加复杂和不可信,所以只能尽最大努力去通知实现数据最终一致性,比如充值平台与运营商、支付对接等等跨网络系统级别对接;
4.1.1 异步确保型事务
指将一系列同步的事务操作修改为基于消息队列异步执行的操作,来避免分布式事务中同步阻塞带来的数据操作性能的下降。
1、MQ事务消息方案
基于MQ
的事务消息方案主要依靠MQ
的半消息机制来实现投递消息和参与者自身本地事务的一致性保障。半消息机制实现原理其实借鉴的2PC
的思路,是二阶段提交的广义拓展。
半消息:在原有队列消息执行后的逻辑,如果后面的本地逻辑出错,则不发送该消息,如果通过则告知
MQ
发送;
1、流程
- 事务发起方首先发送半消息到
MQ
; MQ
通知发送方消息发送成功;- 在发送半消息成功后执行本地事务;
- 根据本地事务执行结果返回
commit
或者是rollback
; - 如果消息是
rollback
,MQ
将丢弃该消息不投递;如果是commit
,MQ
将会消息发送给消息订阅方; - 订阅方根据消息执行本地事务;
- 订阅方执行本地事务成功后再从
MQ
中将该消息标记为已消费; - 如果执行本地事务过程中,执行端挂掉,或者超时,
MQ
服务器端将不停的询问producer
来获取事务状态; Consumer
端的消费成功机制有MQ
保证;
2、异步确保型事务使用示例
举个例子,假设存在业务规则:某笔订单成功后,为用户加一定的积分。在这条规则里,管理订单数据源的服务为事务发起方,管理积分数据源的服务为事务跟随者。从这个过程可以看到,基于消息队列实现的事务存在以下操作:
- 订单服务创建订单,提交本地事务
- 订单服务发布一条消息
- 积分服务收到消息后加积分
我们可以看到它的整体流程是比较简单的,同时业务开发工作量也不大:
- 编写订单服务里订单创建的逻辑
- 编写积分服务里增加积分的逻辑
可以看到该事务形态过程简单,性能消耗小,发起方与跟随方之间的流量峰谷可以使用队列填平,同时业务开发工作量也基本与单机事务没有差别,都不需要编写反向的业务逻辑过程。因此基于消息队列实现的事务是我们除了单机事务外最优先考虑使用的形态。
3、基于RocketMQ实现MQ异步确保型事务
有一些第三方的
MQ
是支持事务消息的,这些消息队列,支持半消息机制,比如RocketMQ
,ActiveMQ
。但是有一些常用的MQ
也不支持事务消息,比如RabbitMQ
和Kafka
都不支持。
以阿里的RocketMQ
中间件为例,其思路大致为:
producer
(本例中指A
系统)发送半消息到broker
,这个半消息不是说消息内容不完整,它包含完整的消息内容,在producer
端和普通消息的发送逻辑一致broker
存储半消息,半消息存储逻辑与普通消息一致,只是属性有所不同,topic
是固定的RMQ_SYS_TRANS_HALF_TOPIC
,queueId
也是固定为0
,这个tiopic
中的消息对消费者是不可见的,所以里面的消息永远不会被消费。这就保证了在半消息提交成功之前,消费者是消费不到这个半消息的broker
端半消息存储成功并返回后,A
系统执行本地事务,并根据本地事务的执行结果来决定半消息的提交状态为提交或者回滚A
系统发送结束半消息的请求,并带上提交状态(提交or
回滚)broker
端收到请求后,首先从RMQ_SYS_TRANS_HALF_TOPIC
的queue
中查出该消息,设置为完成状态。如果消息状态为提交,则把半消息从RMQ_SYS_TRANS_HALF_TOPIC
队列中复制到这个消息原始topic
的queue
中去(之后这条消息就能被正常消费了);如果消息状态为回滚,则什么也不做。producer
发送的半消息结束请求是oneway
的,也就是发送后就不管了,只靠这个是无法保证半消息一定被提交的,RocketMQ
提供了一个兜底方案,这个方案叫消息反查机制,Broker
启动时,会启动一个TransactionalMessageCheckService
任务,该任务会定时从半消息队列中读出所有超时未完成的半消息,针对每条未完成的消息,Broker
会给对应的Producer
发送一个消息反查请求,根据反查结果来决定这个半消息是需要提交还是回滚,或者后面继续来反查consumer
(本例中指B
系统)消费消息,执行本地数据变更(至于B
是否能消费成功,消费失败是否重试,这属于正常消息消费需要考虑的问题)
在RocketMQ
中,不论是producer
收到broker
存储半消息成功返回后执行本地事务,还是broker
向producer
反查消息状态,都是通过回调机制完成,producer
端的代码如下:
半消息发送时,会传入一个回调类TransactionListener
,使用时必须实现其中的两个方法,executeLocalTransaction
方法会在broker
返回半消息存储成功后执行,我们会在其中执行本地事务;checkLocalTransaction
方法会在broker
向producer
发起反查时执行,我们会在其中查询库表状态。两个方法的返回值都是消息状态,就是告诉broker
应该提交或者回滚半消息
2、本地消息表方案
有时候我们目前的MQ
组件并不支持事务消息,或者我们想尽量少的侵入业务方。这时我们需要另外一种方案“基于DB
本地消息表“。本地消息表最初由eBay
提出来解决分布式事务的问题。是目前业界使用的比较多的方案之一,它的核心思想就是将分布式事务拆分成本地事务进行处理。
1、本地消息表流程
发送消息方:
- 需要有一个消息表,记录着消息状态相关信息。
- 业务数据和消息表在同一个数据库,要保证它俩在同一个本地事务。直接利用本地事务,将业务数据和事务消息直接写入数据库。
- 在本地事务中处理完业务数据和写消息表操作后,通过写消息到
MQ
消息队列。使用专门的投递工作线程进行事务消息投递到MQ
,根据投递ACK
去删除事务消息表记录 - 消息会发到消息消费方,如果发送失败,即进行重试。
消息消费方:
- 处理消息队列中的消息,完成自己的业务逻辑。
- 如果本地事务处理成功,则表明已经处理成功了。
- 如果本地事务处理失败,那么就会重试执行。
- 如果是业务层面的失败,给消息生产方发送一个业务补偿消息,通知进行回滚等操作。
生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。
2、本地消息表优缺点:
优点:
- 本地消息表建设成本比较低,实现了可靠消息的传递确保了分布式事务的最终一致性。
- 无需提供回查方法,进一步减少的业务的侵入。
- 在某些场景下,还可以进一步利用注解等形式进行解耦,有可能实现无业务代码侵入式的实现。
缺点:
- 本地消息表与业务耦合在一起,难于做成通用性,不可独立伸缩。
- 本地消息表是基于数据库来做的,而数据库是要读写磁盘
IO
的,因此在高并发下是有性能瓶颈的
3、MQ事务消息 VS 本地消息表
1、二者的共性:
- 事务消息都依赖
MQ
进行事务通知,所以都是异步的。 - 事务消息在投递方都是存在重复投递的可能,需要有配套的机制去降低重复投递率,实现更友好的消息投递去重。
- 事务消息的消费方,因为投递重复的无法避免,因此需要进行消费去重设计或者服务幂等设计。
2、二者的区别:
MQ
事务消息:
- 需要
MQ
支持半消息机制或者类似特性,在重复投递上具有比较好的去重处理; - 具有比较大的业务侵入性,需要业务方进行改造,提供对应的本地操作成功的回查功能;
DB
本地消息表:
- 使用了数据库来存储事务消息,降低了对
MQ
的要求,但是增加了存储成本; - 事务消息使用了异步投递,增大了消息重复投递的可能性;
4.1.2 最大努力通知
最大努力通知方案的目标,就是发起通知方通过一定的机制,最大努力将业务处理结果通知到接收方。
最大努力通知型的最终一致性:
本质是通过引入定期校验机制实现最终一致性,对业务的侵入性较低,适合于对最终一致性敏感度比较低、业务链路较短的场景。
最大努力通知事务主要用于外部系统,因为外部的网络环境更加复杂和不可信,所以只能尽最大努力去通知实现数据最终一致性,比如充值平台与运营商、支付对接、商户通知等等跨平台、跨企业的系统间业务交互场景 ;
而异步确保型事务主要适用于内部系统的数据最终一致性保障,因为内部相对比较可控,比如订单和购物车、收货与清算、支付与结算等等场景。
普通消息是无法解决本地事务执行和消息发送的一致性问题的。因为消息发送是一个网络通信的过程,发送消息的过程就有可能出现发送失败、或者超时的情况。超时有可能发送成功了,有可能发送失败了,消息的发送方是无法确定的,所以此时消息发送方无论是提交事务还是回滚事务,都有可能不一致性出现。
所以,通知型事务的难度在于:投递消息和参与者本地事务的一致性保障。
因为核心要点一致,都是为了保证消息的一致性投递,所以,最大努力通知事务在投递流程上跟异步确保型是一样的,因此也有两个分支:
- 基于
MQ
自身的事务消息方案 - 基于
DB
的本地事务消息表方案
1、MQ事务消息方案
要实现最大努力通知,可以采用MQ
的ACK
机制。最大努力通知事务在投递之前,跟异步确保型流程都差不多,关键在于投递后的处理。因为异步确保型在于内部的事务处理,所以MQ
和系统是直连并且无需严格的权限、安全等方面的思路设计。最大努力通知事务在于第三方系统的对接,所以最大努力通知事务有几个特性:
- 业务主动方在完成业务处理后,向业务被动方(第三方系统)发送通知消息,允许存在消息丢失。
- 业务主动方提供递增多挡位时间间隔(5min、10min、30min、1h、24h),用于失败重试调用业务被动方的接口;在通知N次之后就不再通知,报警+记日志+人工介入。
- 业务被动方提供幂等的服务接口,防止通知重复消费。
- 业务主动方需要有定期校验机制,对业务数据进行兜底;防止业务被动方无法履行责任时进行业务回滚,确保数据最终一致性。
- 业务活动的主动方,在完成业务处理之后,向业务活动的被动方发送消息,允许消息丢失。
- 主动方可以设置时间阶梯型通知规则,在通知失败后按规则重复通知,直到通知N次后不再通知。
- 主动方提供校对查询接口给被动方按需校对查询,用于恢复丢失的业务消息。
- 业务活动的被动方如果正常接收了数据,就正常返回响应,并结束事务。
- 如果被动方没有正常接收,根据定时策略,向业务活动主动方查询,恢复丢失的业务消息。
特点
- 用到的服务模式:可查询操作、幂等操作;
- 被动方的处理结果不影响主动方的处理结果;
- 适用于对业务最终一致性的时间敏感度低的系统;
- 适合跨企业的系统间的操作,或者企业内部比较独立的系统间的操作,比如银行通知、商户通知等;
2、本地消息表方案
要实现最大努力通知,可以采用定期检查本地消息表的机制。
发送消息方:
- 需要有一个消息表,记录着消息状态相关信息。
- 业务数据和消息表在同一个数据库,要保证它俩在同一个本地事务。直接利用本地事务,将业务数据和事务消息直接写入数据库。
- 在本地事务中处理完业务数据和写消息表操作后,通过写消息到
MQ
消息队列。使用专门的投递工作线程进行事务消息投递到MQ
,根据投递ACK
去删除事务消息表记录 - 消息会发到消息消费方,如果发送失败,即进行重试。
- 生产方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。
4.1.3 最大努力通知事务 VS 异步确保型事务
最大努力通知事务在我认知中,其实是基于异步确保型事务发展而来适用于外部对接的一种业务实现。他们主要有的是业务差别,如下:
• 从参与者来说:最大努力通知事务适用于跨平台、跨企业的系统间业务交互;异步确保型事务更适用于同网络体系的内部服务交付。
• 从消息层面说:最大努力通知事务需要主动推送并提供多档次时间的重试机制来保证数据的通知;而异步确保型事务只需要消息消费者主动去消费。
• 从数据层面说:最大努力通知事务还需额外的定期校验机制对数据进行兜底,保证数据的最终一致性;而异步确保型事务只需保证消息的可靠投递即可,自身无需对数据进行兜底处理。
4.1.4 通知型事务的问题
通知型事务,是无法解决本地事务执行和消息发送的一致性问题的。因为消息发送是一个网络通信的过程,发送消息的过程就有可能出现发送失败、或者超时的情况。超时有可能发送成功了,有可能发送失败了,消息的发送方是无法确定的,所以此时消息发送方无论是提交事务还是回滚事务,都有可能不一致性出现。
1、消息发送一致性
消息中间件在分布式系统中的核心作用就是异步通讯、应用解耦和并发缓冲(也叫作流量削峰)。在分布式环境下,需要通过网络进行通讯,就引入了数据传输的不确定性,也就是CAP
理论中的分区容错性。
消息发送一致性是指产生消息的业务动作与消息发送动作一致,也就是说如果业务操作成功,那么由这个业务操作所产生的消息一定要发送出去,否则就丢失。
常规MQ消息处理流程和特点
常规的MQ
队列处理流程无法实现消息的一致性。所以,需要借助半消息、本地消息表,保障一致性。
2、消息重复发送问题和业务接口幂等性设计
对于未确认的消息,采用按规则重新投递的方式进行处理。
对于以上流程,消息重复发送会导致业务处理接口出现重复调用的问题。消息消费过程中消息重复发送的主要原因就是消费者成功接收处理完消息后,消息中间件没有及时更新投递状态导致的。如果允许消息重复发送,那么消费方应该实现业务接口的幂等性设计。
4.2 补偿型事务
但是基于消息实现的事务并不能解决所有的业务场景,例如以下场景:某笔订单完成时,同时扣掉用户的现金。这里事务发起方是管理订单库的服务,但对整个事务是否提交并不能只由订单服务决定,因为还要确保用户有足够的钱,才能完成这笔交易,而这个信息在管理现金的服务里。这里我们可以引入基于补偿实现的事务,其流程如下:
- 创建订单数据,但暂不提交本地事务
- 订单服务发送远程调用到现金服务,以扣除对应的金额
- 上述步骤成功后提交订单库的事务
以上这个是正常成功的流程,异常流程需要回滚的话,将额外发送远程调用到现金服务以加上之前扣掉的金额。以上流程比基于消息队列实现的事务的流程要复杂,同时开发的工作量也更多:
- 编写订单服务里创建订单的逻辑
- 编写现金服务里扣钱的逻辑
- 编写现金服务里补偿返还的逻辑
可以看到,该事务流程相对于基于消息实现的分布式事务更为复杂,需要额外开发相关的业务回滚方法,也失去了服务间流量削峰填谷的功能。但其仅仅只比基于消息的事务复杂多一点,若不能使用基于消息队列的最终一致性事务,那么可以优先考虑使用基于补偿的事务形态。
什么是补偿模式?
补偿模式使用一个额外的协调服务来协调各个需要保证一致性的业务服务,协调服务按顺序调用各个业务微服务,如果某个业务服务调用异常(包括业务异常和技术异常)就取消之前所有已经调用成功的业务服务。
补偿模式大致有TCC
和Saga
两种细分的方案。
4.2.1 TCC事务模型
1、什么是TCC事务模型
TCC
(Try-Confirm-Cancel
)的概念来源于Pat Helland
发表的一篇名为“Life beyond Distributed Transactions:an Apostate’s Opinion”的论文。
TCC分布式事务模型包括三部分:
1.主业务服务:主业务服务为整个业务活动的发起方,服务的编排者,负责发起并完成整个业务活动。
2.从业务服务:从业务服务是整个业务活动的参与方,负责提供TCC
业务操作,实现初步操作(Try
)、确认操作(Confirm
)、取消操作(Cancel
)三个接口,供主业务服务调用。
3.业务活动管理器 :业务活动管理器管理控制整个业务活动,包括记录维护TCC
全局事务的事务状态和每个从业务服务的子事务状态,并在业务活动提交时调用所有从业务服务的Confirm
操作,在业务活动取消时调用所有从业务服务的Cancel
操作。
TCC
提出了一种新的事务模型,基于业务层面的事务定义,锁粒度完全由业务自己控制,目的是解决复杂业务中,跨表跨库等大颗粒度资源锁定的问题。
TCC
把事务运行过程分成Try
、Confirm/Cancel
两个阶段,每个阶段的逻辑由业务代码控制,避免了长事务,可以获取更高的性能。
2、TCC的工作流程
TCC
(Try-Confirm-Cancel
)分布式事务模型相对于XA
等传统模型,其特征在于它不依赖资源管理器(RM
)对分布式事务的支持,而是通过对业务逻辑的分解来实现分布式事务 。
TCC
模型认为对于业务系统中一个特定的业务逻辑,其对外提供服务时,必须接受一些不确定性,即对业务逻辑初步操作的调用仅是一个临时性操作,调用它的主业务服务保留了后续的取消权。如果主业务服务认为全局事务应该回滚,它会要求取消之前的临时性操作,这就对应从业务服务的取消操作。而当主业务服务认为全局事务应该提交时,它会放弃之前临时性操作的取消权,这对应从业务服务的确认操作。每一个初步操作,最终都会被确认或取消。因此,针对一个具体的业务服务,TCC
分布式事务模型需要业务系统提供三段业务逻辑:
- 初步操作
Try
:完成所有业务检查,预留必须的业务资源。 - 确认操作
Confirm
:真正执行的业务逻辑,不作任何业务检查,只使用Try
阶段预留的业务资源。因此,只要Try
操作成功,Confirm
必须能成功。另外,Confirm
操作需满足幂等性,保证一笔分布式事务有且只能成功一次。 - 取消操作
Cancel
:释放Try
阶段预留的业务资源。同样的,Cancel
操作也需要满足幂等性。
TCC分布式事务模型包括三部分:
Try
阶段:调用Try
接口,尝试执行业务,完成所有业务检查,预留业务资源。Confirm/Cancel
阶段:两者是互斥的,只能进入其中一个,并且都满足幂等性,允许失败重试。Confirm
操作:对业务系统做确认提交,确认执行业务操作,不做其他业务检查,只使用Try
阶段预留的业务资源。Cancel
操作:在业务执行错误,需要回滚的状态下执行业务取消,释放预留资源。
Try
阶段失败可以Cancel
,如果Confirm
和Cancel
阶段失败了怎么办?
TCC
中会添加事务日志,如果Confirm
或者Cancel
阶段出错,则会进行重试,所以这两个阶段需要支持幂等;如果重试失败,则需要人工介入进行恢复和处理等。
3、TCC事务案例
然而基于补偿的事务形态也并非能实现所有的需求,如以下场景:某笔订单完成时,同时扣掉用户的现金,但交易未完成,也未被取消时,不能让客户看到钱变少了。这时我们可以引入TCC
,其流程如下:
- 订单服务创建订单
- 订单服务发送远程调用到现金服务,冻结客户的现金
- 提交订单服务数据
- 订单服务发送远程调用到现金服务,扣除客户冻结的现金
以上是正常完成的流程,若为异常流程,则需要发送远程调用请求到现金服务,撤销冻结的金额。以上流程比基于补偿实现的事务的流程要复杂,同时开发的工作量也更多:
- 订单服务编写创建订单的逻辑
- 现金服务编写冻结现金的逻辑
- 现金服务编写扣除现金的逻辑
- 现金服务编写解冻现金的逻辑
TCC
实际上是最为复杂的一种情况,其能处理所有的业务场景,但无论出于性能上的考虑,还是开发复杂度上的考虑,都应该尽量避免该类事务。
4、TCC事务模型的要求
- 可查询操作:服务操作具有全局唯一的标识,操作唯一的确定的时间。
- 幂等操作:重复调用多次产生的业务结果与调用一次产生的结果相同。一是通过业务操作实现幂等性,二是系统缓存所有请求与处理的结果,最后是检测到重复请求之后,自动返回之前的处理结果。
TCC
操作:Try
阶段:尝试执行业务,完成所有业务的检查,实现一致性;预留必须的业务资源,实现准隔离性。Confirm
阶段:真正的去执行业务,不做任何检查,仅适用Try
阶段预留的业务资源,Confirm
操作还要满足幂等性。Cancel
阶段:取消执行业务,释放Try
阶段预留的业务资源,Cancel
操作要满足幂等性。
- 可补偿操作:
Do
阶段:真正的执行业务处理,业务处理结果外部可见。Compensate
阶段:抵消或者部分撤销正向业务操作的业务结果,补偿操作满足幂等性。
约束:补偿操作在业务上可行,由于业务执行结果未隔离或者补偿不完整带来的风险与成本可控。实际上,TCC
的Confirm
和Cancel
操作可以看做是补偿操作。
5、TCC事务模型 VS DTP事务模型
比较一下TCC
事务模型和DTP
事务模型,如下所示:
这两张图看起来差别较大,实际上很多地方是类似的!
1、TCC
模型中的主业务服务相当于DTP
模型中的AP
,TCC
模型中的从业务服务相当于DTP
模型中的RM
- 在
DTP
模型中,应用AP
操作多个资源管理器RM上的资源;而在TCC
模型中,是主业务服务操作多个从业务服务上的资源。例如航班预定案例中,美团App
就是主业务服务,而川航和东航就是从业务服务,主业务服务需要使用从业务服务上的机票资源。不同的是DTP
模型中的资源提供者是类似于MySQL
这种关系型数据库,而TCC
模型中资源的提供者是其他业务服务。
2、TCC
模型中,从业务服务提供的try
、confirm
、cancel
接口相当于DTP
模型中RM
提供的prepare
、commit
、rollback
接口
XA
协议中规定了DTP
模型中定RM需要提供prepare
、commit
、rollback
接口给TM
调用,以实现两阶段提交。- 而在
TCC
模型中,从业务服务相当于RM
,提供了类似的try
、confirm
、cancel
接口。
3、事务管理器
DTP
模型和TCC
模型中都有一个事务管理器。不同的是:- 在
DTP
模型中,阶段1
的(prepare
)和阶段2
的(commit
、rollback
),都是由TM
进行调用的。 - 在
TCC
模型中,阶段1
的try
接口是主业务服务调用(绿色箭头),阶段2
的(confirm
、cancel
接口)是事务管理器TM
调用(红色箭头)。这就是TCC
分布式事务模型的二阶段异步化功能,从业务服务的第一阶段执行成功,主业务服务就可以提交完成,然后再由事务管理器框架异步的执行各从业务服务的第二阶段。这里牺牲了一定的隔离性和一致性的,但是提高了长事务的可用性。
6、TCC与2PC对比
TCC
与2PC
(两阶段提交)协议的区别:TCC
位于业务服务层而不是资源层,TCC
没有单独准备阶段,Try
操作兼备资源操作与准备的能力,TCC
中Try
操作可以灵活的选择业务资源,锁定粒度。TCC
的开发成本比2PC
高。实际上TCC
也属于两阶段操作,但是TCC
不等同于2PC
操作。
TCC其实本质和2PC是差不多的:
T
就是Try
,两个C
分别是Confirm
和`Cancel。Try
就是尝试,请求链路中每个参与者依次执行Try
逻辑,如果都成功,就再执行Confirm
逻辑,如果有失败,就执行Cancel
逻辑。
TCC
与XA
两阶段提交有着异曲同工之妙,下图列出了二者之间的对比
- 在阶段1:
- 在
XA
中,各个RM
准备提交各自的事务分支,事实上就是准备提交资源的更新操作(insert
、delete
、update
等); - 而在
TCC
中,是主业务活动请求(try
)各个从业务服务预留资源。
- 在阶段2:
XA
根据第一阶段每个RM
是否都prepare
成功,判断是要提交还是回滚。如果都prepare
成功,那么就commit
每个事务分支,反之则rollback
每个事务分支。TCC
中,如果在第一阶段所有业务资源都预留成功,那么confirm
各个从业务服务,否则取消(cancel
)所有从业务服务的资源预留请求。
TCC和2PC不同的是:
XA
是资源层面的分布式事务,强一致性,在两阶段提交的整个过程中,一直会持有资源的锁。基于数据库锁实现,需要数据库支持XA
协议,由于在执行事务的全程都需要对相关数据加锁,一般高并发性能会比较差TCC
是业务层面的分布式事务,最终一致性,不会一直持有资源的锁,性能较好。但是对微服务的侵入性强,微服务的每个事务都必须实现try
、confirm
、cancel
等3
个方法,开发成本高,今后维护改造的成本也高为了达到事务的一致性要求,try
、confirm
、cancel
接口必须实现幂等性操作由于事务管理器要记录事务日志,必定会损耗一定的性能,并使得整个TCC
事务时间拉长
TCC
它会弱化每个步骤中对于资源的锁定,以达到一个能承受高并发的目的(基于最终一致性)。
具体的说明如下:
XA
是资源层面的分布式事务,强一致性,在两阶段提交的整个过程中,一直会持有资源的锁。
XA
事务中的两阶段提交内部过程是对开发者屏蔽的,开发者从代码层面是感知不到这个过程的。而事务管理器在两阶段提交过程中,从prepare
到commit/rollback
过程中,资源实际上一直都是被加锁的。如果有其他人需要更新这两条记录,那么就必须等待锁释放。
TCC
是业务层面的分布式事务,最终一致性,不会一直持有资源的锁。 TCC
中的两阶段提交并没有对开发者完全屏蔽,也就是说从代码层面,开发者是可以感受到两阶段提交的存在。try
、confirm/cancel
在执行过程中,一般都会开启各自的本地事务,来保证方法内部业务逻辑的ACID
特性。其中:
1、try
过程的本地事务,是保证资源预留的业务逻辑的正确性。
2、confirm/cancel
执行的本地事务逻辑确认/取消预留资源,以保证最终一致性,也就是所谓的补偿型事务(Compensation-Based Transactions
)。由于是多个独立的本地事务,因此不会对资源一直加锁。
另外,这里提到confirm/cancel
执行的本地事务是补偿性事务:
补偿是一个独立的支持ACID
特性的本地事务,用于在逻辑上取消服务提供者上一个ACID
事务造成的影响,对于一个长事务(long-running transaction
),与其实现一个巨大的分布式ACID
事务,不如使用基于补偿性的方案,把每一次服务调用当做一个较短的本地ACID
事务来处理,执行完就立即提交
7、TCC的使用场景
TCC
是可以解决部分场景下的分布式事务的,但是,它的一个问题在于,需要每个参与者都分别实现Try
,Confirm
和Cancel
接口及逻辑,这对于业务的侵入性是巨大的。
TCC
方案严重依赖回滚和补偿代码,最终的结果是:回滚代码逻辑复杂,业务代码很难维护。所以,TCC
方案的使用场景较少,但是也有使用的场景。
比如说跟钱打交道的,支付、交易相关的场景,大家会用TCC
方案,严格保证分布式事务要么全部成功,要么全部自动回滚,严格保证资金的正确性,保证在资金上不会出现问题。
4.2.2 SAGA长事务模型
SAGA
可以看做一个异步的、利用队列实现的补偿事务。
1、Saga相关概念
1987
年普林斯顿大学的Hector Garcia-Molina
和Kenneth Salem
发表了一篇Paper Sagas
,讲述的是如何处理long lived transaction
(长活事务)。Saga
是一个长活事务可被分解成可以交错运行的子事务集合。其中每个子事务都是一个保持数据库一致性的真实事务。
论文地址:sagas
Saga
模型是把一个分布式事务拆分为多个本地事务,每个本地事务都有相应的执行模块和补偿模块(对应TCC
中的Confirm
和Cancel
),当Saga
事务中任意一个本地事务出错时,可以通过调用相关的补偿方法恢复之前的事务,达到事务最终一致性。
这样的SAGA
事务模型,是牺牲了一定的隔离性和一致性的,但是提高了long-running
事务的可用性。
Saga模型由三部分组成 :
- LLT(Long Live Transaction):由一个个本地事务组成的事务链。
- 本地事务:事务链由一个个子事务(本地事务)组成,
LLT = T1+T2+T3+...+Ti
。 - 补偿:每个本地事务
Ti
有对应的补偿Ci
。
Saga的执行顺序有两种:
- T1, T2, T3, ..., Tn
- T1, T2, ..., Tj, Cj,..., C2, C1,其中0 < j < n
2、Saga两种恢复策略
- 向后恢复(
Backward Recovery
):撤销掉之前所有成功子事务。如果任意本地子事务失败,则补偿已完成的事务。如异常情况的执行顺序T1,T2,T3,..Ti,Ci,...C3,C2,C1
。
- 向前恢复(
Forward Recovery
):即重试失败的事务,适用于必须要成功的场景,该情况下不需要Ci
。执行顺序:T1,T2,...,Tj(失败),Tj(重试),...,Ti
。
显然,向前恢复没有必要提供补偿事务,如果你的业务中,子事务(最终)总会成功,或补偿事务难以定义或不可能,向前恢复更符合你的需求。
理论上补偿事务永不失败,然而,在分布式世界中,服务器可能会宕机,网络可能会失败,甚至数据中心也可能会停电。在这种情况下我们能做些什么?最后的手段是提供回退措施,比如人工干预。
3、Saga的使用条件
Saga
看起来很有希望满足我们的需求。所有长活事务都可以这样做吗?这里有一些限制:
Saga
只允许两个层次的嵌套,顶级的Saga
和简单子事务- 在外层,全原子性不能得到满足。也就是说,
sagas
可能会看到其他sagas
的部分结果 - 每个子事务应该是独立的原子行为
- 在我们的业务场景下,各个业务环境(如:航班预订、租车、酒店预订和付款)是自然独立的行为,而且每个事务都可以用对应服务的数据库保证原子操作。
补偿也有需考虑的事项:
- 补偿事务从语义角度撤消了事务
Ti
的行为,但未必能将数据库返回到执行Ti
时的状态。(例如,如果事务触发导弹发射,则可能无法撤消此操作)
但这对我们的业务来说不是问题。其实难以撤消的行为也有可能被补偿。例如,发送电邮的事务可以通过发送解释问题的另一封电邮来补偿。
4、对于ACID的保证:
Saga
对于ACID
的保证和TCC
一样:
- 原子性(
Atomicity
):正常情况下保证。 - 一致性(
Consistency
),在某个时间点,会出现A
库和B
库的数据违反一致性要求的情况,但是最终是一致的。 - 隔离性(
Isolation
),在某个时间点,A
事务能够读到B
事务部分提交的结果。 - 持久性(
Durability
),和本地事务一样,只要commit
则数据被持久。
Saga
不提供ACID
保证,因为原子性和隔离性不能得到满足。原论文描述如下:
full atomicity is not provided. That is, sagas may view the partial results of other sagas
通过saga log
,saga
可以保证一致性和持久性。
5、SAGA模型的解决方案
SAGA
模型的核心思想是,通过某种方案,将分布式事务转化为本地事务,从而降低问题的复杂性。
比如以DB
和MQ
的场景为例,业务逻辑如下:
- 向
DB
中插入一条数据。 - 向
MQ
中发送一条消息。
由于上述逻辑中,对应了两种存储端,即DB
和MQ
,所以,简单的通过本地事务是无法解决的。那么,依照SAGA
模型,可以有两种解决方案。
方案一:半消息模式
RocketMQ
新版本中,就支持了这种模式。
首先,什么是半消息。简单来说,就是在消息上加了一个状态。当发送者第一次将消息放入MQ
后,该消息为待确认状态。该状态下,该消息是不能被消费者消费的。发送者必须二次和MQ
进行交互,将消息从待确认状态变更为确认状态后,消息才能被消费者消费。待确认状态的消息,就称之为半消息。
半消息的完整事务逻辑如下:
- 向
MQ
发送半消息。 - 向
DB
插入数据。 - 向
MQ
发送确认消息。
通过半消息的形式,将DB
的操作夹在了两个MQ
操作的中间。假设,第2
步失败了,那么,MQ
中的消息就会一直是半消息状态,也就不会被消费者消费。
那么,半消息就一直存在于MQ
中吗?或者是说如果第3
步失败了呢?
为了解决上面的问题,MQ
引入了一个扫描的机制。即MQ
会每隔一段时间,对所有的半消息进行扫描,并就扫描到的存在时间过长的半消息,向发送者进行询问,询问如果得到确认回复,则将消息改为确认状态,如得到失败回复,则将消息删除。
如上,半消息机制的一个问题是:要求业务方提供查询消息状态接口,对业务方依然有较大的侵入性。
方案二:本地消息表
在DB
中,新增一个消息表,用于存放消息。如下:
- 在
DB
业务表中插入数据。 - 在
DB
消息表中插入数据。 - 异步将消息表中的消息发送到
MQ
,收到ack
后,删除消息表中的消息。
如上,通过上述逻辑,将一个分布式的事务,拆分成两大步。第1
和第2
,构成了一个本地的事务,从而解决了分布式事务的问题。
这种解决方案,不需要业务端提供消息查询接口,只需要稍微修改业务逻辑,侵入性是最小的。
6、SAGA的案例
SAGA
适用于无需马上返回业务发起方最终状态的场景,例如:你的请求已提交,请稍后查询或留意通知之类。
将上述补偿事务的场景用SAGA
改写,其流程如下:
- 订单服务创建最终状态未知的订单记录,并提交事务
- 现金服务扣除所需的金额,并提交事务
- 订单服务更新订单状态为成功,并提交事务
以上为成功的流程,若现金服务扣除金额失败,那么,最后一步订单服务将会更新订单状态为失败。
其业务编码工作量比补偿事务多一点,包括以下内容:
- 订单服务创建初始订单的逻辑
- 订单服务确认订单成功的逻辑
- 订单服务确认订单失败的逻辑
- 现金服务扣除现金的逻辑
- 现金服务补偿返回现金的逻辑
但其相对于补偿事务形态有性能上的优势,所有的本地子事务执行过程中,都无需等待其调用的子事务执行,减少了加锁的时间,这在事务流程较多较长的业务中性能优势更为明显。同时,其利用队列进行进行通讯,具有削峰填谷的作用。
因此该形式适用于不需要同步返回发起方执行最终结果、可以进行补偿、对性能要求较高、不介意额外编码的业务场景。
但当然SAGA
也可以进行稍微改造,变成与TCC
类似、可以进行资源预留的形态
7、Saga和TCC对比
Saga
相比TCC
的缺点是缺少预留动作,导致补偿动作的实现比较麻烦:Ti
就是commit
,比如一个业务是发送邮件,在TCC
模式下,先保存草稿(Try
)再发送(Confirm
),撤销的话直接删除草稿(Cancel)
就行了。而Saga
则就直接发送邮件了(Ti
),如果要撤销则得再发送一份邮件说明撤销(Ci
),实现起来有一些麻烦。
如果把上面的发邮件的例子换成:A
服务在完成Ti
后立即发送Event
到ESB
(企业服务总线,可以认为是一个消息中间件),下游服务监听到这个Event
做自己的一些工作然后再发送Event
到ESB
,如果A
服务执行补偿动作Ci
,那么整个补偿动作的层级就很深。
不过没有预留动作也可以认为是优点:
- 有些业务很简单,套用
TCC
需要修改原来的业务逻辑,而Saga
只需要添加一个补偿动作就行了。 TCC
最少通信次数为2n
,而Saga
为n
(n=sub-transaction
的数量)。- 有些第三方服务没有
Try
接口,TCC
模式实现起来就比较tricky
了,而Saga
则很简单。 - 没有预留动作就意味着不必担心资源释放的问题,异常处理起来也更简单。
Saga对比TCC
Saga
和TCC
都是补偿型事务,他们的区别为:
劣势:
- 无法保证隔离性;
优势:
- 一阶段提交本地事务,无锁,高性能;
- 事件驱动模式,参与者可异步执行,高吞吐;
Saga
对业务侵入较小,只需要提供一个逆向操作的Cancel
即可;而TCC
需要对业务进行全局性的流程改造;
五、分布式事务框架
5.1 Seata
Seata
(曾用名Fescar,开源版本GTS)是阿里巴巴和蚂蚁金服联合打造的分布式事务框架。其AT
事务的目标是让开发者像使用本地事务一样使用分布式事务。Seata
支持XA
、TCC
、Saga
模式,但支持的主要方式是AT
模式。Seata AT
模式是增强型2PC
模式。
AT
模式:两阶段提交协议的演变,没有一直锁表
- 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源
- 二阶段:提交异步化,非常快速地完成。或回滚通过一阶段的回滚日志进行反向补偿
具体请参考分布式事务Seata
5.2 LCN
TX-LCN 5.0
以后由于框架兼容了LCN(2PC)、TCC、TXC
三种事务模式,为了区分LCN
模式,特此将LCN
分布式事务改名为TX-LCN
分布式事务框架。
TX-LCN
定位于一款事务协调性框架,框架其本身并不生产事务,而是本地事务的协调者,从而达到事务一致性的效果。
TX-LCN
主要有两个模块,Tx-Client
(TC
),Tx-Manager
(TM
).
TM
(Tx-Manager
):是独立的服务,是分布式事务的控制方,协调分布式事务的提交,回滚TC
(Tx-Client
):由业务系统集成,事务发起方、参与方都由TxClient
端来控制
5.3 ShardingSphere
ShardingSphere
是一套开源的分布式数据库解决方案组成的生态圈,它由JDBC
、Proxy
和Sidecar
(规划中)这3
款既能够独立部署,又支持混合部署配合使用的产品组成。 它们均提供标准化的数据水平扩展、分布式事务和分布式治理等功能,可适用于如Java
同构、异构语言、云原生等各种多样化的应用场景。
ShardingSphere 5.x
版本开始致力于可插拔架构,项目的功能组件能够灵活的以可插拔的方式进行扩展。目前,数据分片、读写分离、数据加密、影子库压测等功能,以及MySQL
、PostgreSQL
、SQLServer
、Oracle
等SQL
与协议的支持,均通过插件的方式织入项目。 开发者能够像使用积木一样定制属于自己的独特系统。ShardingSphere
目前已提供数十个SPI
作为系统的扩展点,仍在不断增加中。
ShardingSphere
已于2020年4月16日成为Apache
软件基金会的顶级项目。
ShardingSphere
通过整合常用的几个事务开源实现,如Atomkkos
、Narayana
,为本地事务、两阶段事务和柔性事务提供统一的分布式事务接口,并弥补当前方案的不足,提供一站式的分布式事务解决方案是ShardingSphere
的设计目标。
使用实例:https://gitee.com/mmcLine/spring-cloud-transaction
5.4 Atomikos
Atomikos
,官方文档
其旗下最著名的产品就是事务管理器。产品分两个版本:
TransactionEssentials
:开源的免费产品ExtremeTransactions
:商业版,需要收费
这两个产品的关系如下图所示:
可以看到,在开源版本中支持JTA/XA
、JDBC
、JMS
的事务。
atomikos也支持与spring事务整合。spring
事务管理器的顶级抽象是PlatformTransactionManager
接口,其提供了个重要的实现类:
- DataSourceTransactionManager:用于实现本地事务
- JTATransactionManager :用于实现分布式事务
显然,这里配置的是JTATransactionManager
。
单体项目多数据源可以使用JTA+ Atomikos
。
5.5 Hmily
Hmily
一款金融级的分布式事务解决方案,支持Dubbo
、Spring Cloud
、Motan
,GRPC
,BRCP
等RPC
框架进行分布式事务。
框架特性
- 高可靠性 :支持分布式场景下,事务异常回滚,超时异常恢复,防止事务悬挂。
- 易用性 :提供零侵入性式的
Spring-Boot
,Spring-Namespace
快速与业务系统集成。 - 高性能 :去中心化设计,与业务系统完全融合,天然支持集群部署。
- 可观测性 :
Metrics
多项指标性能监控,以及admin
管理后台UI展示。 - 多种
RPC
: 支持Dubbo
,SpringCloud
,Montan
,brpc
,tars
等知名RPC框架。 - 日志存储 : 支持
mysql
,oracle
,mongodb
,redis
,zookeeper
等方式。 - 复杂场景 : 支持
RPC
嵌套调用事务
5.6 EasyTransaction
EasyTransaction
,Github。
EasyTransaction
(简称ET
)的目标也是构建出一个全面分布式事务解决方案,到目前为止其包含TCC
、Saga
事务、可靠消息、补偿等多种形态。可一站式解决分布式SOA
(包括微服务等)的事务问题。不过该项目目前已停止维护。
5.7 RocketMQ
消息队列RocketMQ
版提供类似XA
或Open XA
的分布式事务功能,通过消息队列RocketMQ
版事务消息,能达到分布式事务的最终一致。
具体请参考:消息队列RocketMQ应用
5.8 其他的分布式事务开源框架
tcc-transaction、ByteTCC、spring-cloud-rest-tcc
六、总结
总体的方案对比
属性 | 2PC | TCC | Saga | 异步确保型事务 | 尽最大努力通知 |
---|---|---|---|---|---|
事务一致性 | 强 | 弱 | 弱 | 弱 | 弱 |
复杂性 | 中 | 高 | 中 | 低 | 低 |
业务侵入性 | 小 | 大 | 小 | 中 | 中 |
使用局限性 | 大 | 大 | 中 | 小 | 中 |
性能 | 低 | 中 | 高 | 高 | 高 |
维护成本 | 低 | 高 | 中 | 低 | 中 |