[Java微服务架构]7_事务处理

101 阅读31分钟

欢迎来到啾啾的博客🐱。
这是一个致力于构建完善的Java程序员知识体系的博客📚,记录学习的点滴,分享工作的思考、实用的技巧,偶尔也分享一些杂谈💬。
欢迎评论交流,感谢您的阅读😄。

目录

  • 引言
  • 1.事务特性
    • ACID
    • 内部一致性(Internal Consistency)
    • 外部一致性(External Consistency)
  • 2.本地事务(Local Transaction)
    • ARIES理论
    • 实现原子性和持久性
      • 提交日志 Commit Logging
    • 实现隔离性
  • 3.全局事务(Global Transaction)
    • 2PC
    • 3PC
  • 4.共享事务(Share Transaction)
    • 共享数据源连接
  • 5.分布式事务(Distributed Transaction)
    • CAP理论与ACID的矛盾(难以达成的强一致性)
    • 最终一致性
      • BASE模型
        • BASE模型
        • BASE模型实现方式
      • 可靠事件队列
      • TCC模式
      • Sage模式
        • 事务拆分与补偿策设计
        • 实现方式
          • 编排式 Saga(Choreography)
          • 指挥式 Saga(Orchestration)
        • Sage总结
  • 总结
    • 2PC/3PC与TCC、Sage
      • 关联
      • 适用场景
  • 资料引用

引言

事务贯穿于各类信息系统之中,是数据库、分布式共享数据等技术不可或缺的组成部分。 只要系统涉及信息的存储、处理或数据的管理,事务机制就扮演着关键角色,通过其特性确保数据操作的可靠性和一致性。

事务是:“通过ACID特性确保数据安全与一致性的数据操作的逻辑单元”

在微服务架构中,需要保证数据一致性的场景很多,包括但不限于数据库、事务内存、缓存、消息队列以及分布式存储等。事务要怎么保证这些场景的数据一致性呢?
这个问题可以拆分为以下问题,即:
”单服务单数据源“场景下,事务如何在单一服务和单一数据源的场景下实现一致性?
“单服务多数据源”场景下,事务如何协调一致性?
“多服务单数据源”场景下,事务如何确保一致性?
“多服务多数据源”(分布式事务)场景下,事务如何应对多服务、多数据源的复杂一致性挑战?

1.事务特性

ACID

原子性Atomicity 一致性Consistency 隔离性Isolation 持久性Durability。 一般来说,其中原子性 A、隔离性I、持久性D都是为了保证数据的一致性 C。即满足A、I、D,以实现数据的一致性I。

A、I、D for C

具体关于四个特性的描述,在MySQL事务开篇有提及,这里不再赘述。

内部一致性(Internal Consistency)

内部一致性指的是在分布式系统中的一个事务执行过程中,系统内部的数据状态保持逻辑上的正确性和完整性。

即,事务要么成功,要么失败。关注事务本身。

分布式系统中,内部一致性通常通过事务管理器、两阶段提交(2PC)或者日志回滚等机制来实现。

外部一致性(External Consistency)

当一个服务使用到多个不同的数据源,甚至多个不同服务同时涉及多个不同的数据源时,问题就变得困难了许多。此时,并发执行甚至是先后执行的多个事务,在时间线上的顺序并不由任何一个数据源来决定,这种涉及多个数据源的事务间一致性被称为“外部一致性”。

即,多个数据源在外部看来的一致性。全局视角。

外部一致性通常比内部一致性更难实现,因为它需要协调多个节点之间的数据同步,可能涉及 Paxos、Raft 这样的共识算法,或者使用强一致性协议。

2.本地事务(Local Transaction)

本地事务是适用于单个服务使用的单数据源场景的事务解决方案。

ARIES理论

ARIES是现代数据库的基础理论。 (Algorithms for Recovery and Isolation Exploiting Semantic,ARIES),直接翻译过来是“基于语义的恢复与隔离算法”。 其着重解决了原子性A和持久性D在算法层面上的实现。

还是算法,所以说程序就是=数据结构+算法

实现原子性和持久性

原子性和持久性在事务里是密切相关的两个属性:原子性保证了事务的多个操作要么都生效要么都不生效,不会存在中间状态;持久性保证了一旦事务生效,就不会再因为任何原因而导致其修改的内容被撤销或丢失。

实现原子性和持久性需要将数据“写入磁盘”,最大困难是“写入磁盘”这个操作并不是原子 的,不仅有“写入”与“未写入”状态,还客观存在着“正在写”的中间状态。

为了避免“未提交事务,写入后崩溃(事务未完成,部分数据写入磁盘)”、“已提交事务,写入前崩溃(事务已完成,未写入磁盘)”,需要进行数据恢复操作,即“恢复崩溃”。 有一种能“恢复崩溃”的事务实现方式为——“提交日志 (Commit Logging)”。

提交日志 Commit Logging

提交日志的核心思想是:在事务真正修改数据之前,先把事务的所有操作记录到一个持久化的日志中。这个日志通常存储在磁盘上(或高可用存储中),确保即使系统崩溃,事务的状态也能通过日志恢复。 主要作用是:

  1. 保证原子性:通过记录操作意图,确保事务要么全部完成,要么全部回滚。
  2. 提供持久性:一旦日志被写入并标记为“已提交”,即使系统故障,数据也能恢复。
  3. 支持一致性:日志记录了事务的完整状态,便于协调多个节点。 工作流程 :
  • 1.日志记录阶段:
    • 事务开始时,系统将事务的所有操作(如“更新 A=100”“插入 B=200”)写入日志。
    • 这些操作以“意图”(intent)的形式记录,而不是直接修改数据库。
  • 2.提交阶段:
    • 当事务的所有操作都记录到日志后,系统写入一个“提交记录”(commit record),表示事务已成功。
    • 提交记录写入后,事务被认为“持久化”,即使后续崩溃也能恢复。
  • 3.应用阶段:
    • 日志提交后,系统根据日志内容实际更新数据库(称为“重做”,redo)。
  • 4.恢复阶段(如果崩溃):
    • 系统重启后,读取日志:
      • 如果有提交记录,就重做操作。
      • 如果没有提交记录,就回滚(撤销未完成的操作,称为“undo”)。

以MySQL为例简单描述,其InnoDB使用一种叫WAL (Write-Ahead Logging,预写日志)的方式提交日志:
1.执行修改数据操作,先生成Undo Log记录数据修改前状态
2.将“更改记录”写入Redo Log
3.按事务顺序将DDL和DML操作记录到Binlog
4.事务打上commit标签,正式结束。

可以看出,核心是Redo Log与Undo Log,提交日志流程借助这两者进行数据崩溃恢复。 MySQL实际数据写入流程没这么简单,这里只是提取了提交日志部分。

实现隔离性

提起隔离,很容易想到使用锁的方式进行并发隔离。 以数据库为例,有3种锁——写锁、读锁、范围锁,其中写锁排他,读锁共享,范围锁是典型的加范围(范围内不能写入)。 也可以按照范围划分为行锁、表锁、间隙锁(Gap Lock)与Next-Key Lock.

还是以MySQL为例,8.0版本默认隔离级别为RR,可重复读。 实现机制中锁为:读取加共享锁(部分情况),写操作加排他锁,范围操作可能使用 Next-Key Lock(InnoDB 默认使用 Next-Key Lock,它是行锁和间隙锁的组合)。还有MVCC。

对于隔离性来说,隔离级别越高,隔离程度越高,并发能力越低,但脏读、幻读、不可重复度现象越少。

具体隔离级别与MVCC可以看这篇MySQL事务

3.全局事务(Global Transaction)

全局事务主要讨论单个服务使用多个数据源的多数据源事务问题。

2PC

在多个数据源的场景中,可以将多个事务的提交划分为两个阶段,即2PC。 1.准备阶段 又叫作投票阶段,在这一阶段,协调者询问事务的所有参与者是否准备好提交,参与者如果已经准备好提交则回复Prepared,否则回复Non-Prepared。 对于数据库来说,准备操作是在Undo Log中记录全部事务提交操作所要做的内容,它与本地事务中真正提交的区别只是暂不写入最后一条Commit Record而已。 即上面一MySQL为例进行到第四步前。

2.提交阶段 又叫作执行阶段,协调者如果在上一阶段收到所有事务参与者回复的Prepared消息,则先自己在本地持久化事务状态为Commit,然后向所有参与者发送Commit指令,让所有参与者立即执行提交操作。 任意一个参与者回复了Non-Prepared消息,或任意一个参与者超时未回复时,协调者将在自己完成事务状态为Abort持久化后,向所有参与者发送Abort指令,让参与者立即执行回滚操作。 对于数据库来说,这个阶段的提交操作应是很轻量的,仅仅是持久化一条Commit Record而已,通常能够快速完成,只有收到Abort指令时,才需要根据回滚日志清理已提交的数据,这可能是相对重负载操作。

2PC能够成功保证一致性还需要一些其他前提条件: 必须假设网络在提交阶段的短时间内是可靠的,即提交阶段不会丢失消息。同时也假设网络通信在全过程都不会出现误差,即可以丢失消息,但不会传递错误的消息。

image.png

3PC

不难看出2PC存在协调者等待参与者会有超时问题(也称单点问题),且所有参与者相当于被绑定为一个统一调度的整体,期间要经过两次远程服务调用,三次数据持久化(准备阶段写重做日志,协调者做状态持久化,提交阶段在日志写入提交记录),整个过程将持续到参与者集群中最慢的那一个处理操作结束为止,这决定了两段式提交的性能通常都较差。 且2PC需要可靠的网络,当网络不稳定时2PC失效。

FLP不可能原理:如果宕机最后不能恢复,那就不存在任何一种分布式协议可以正确地达成一致性结果。

三段式提交把原本的两段式提交的准备阶段再细分为两个阶段,分别称为CanCommit、PreCommit,把提交阶段改称为DoCommit阶段。其中,新增的CanCommit是一个询问阶段,即协调者让每个参与的数据库根据自身状态,评估该事务是否有可能顺利完成。

将准备阶段一分为二的理由是这个阶段是重负载的操作,一旦协调者发出开始准备的消息,每个参与者都将马上开始写重做日志,它们所涉及的数据资源即被锁住,如果此时某一个参与者宣告无法完成提交,相当于大家都做了一轮无用功。

所以,增加一轮询问阶段,如果都得到了正面的响应,那事务能够成功提交的把握就比较大了,这也意味着因某个参与者提交时发生崩溃而导致大家全部回滚的风险相对变小。因此,在事务需要回滚的场景中,三段式提交的性能通常要比两段式提交好很多,但在事务能够正常提交的场景中,两者的性能都很差,甚至三段式因为多了一次询问,还要稍微更差一些。

同样也是由于事务失败回滚概率变小,在三段式提交中,如果在PreCommit阶段之后发生了协调者宕机,即参与者没有等到DoCommit的消息的话,默认的操作策略将是提交事务而不是回滚事务或者持续等待,这就相当于避免了协调者单点问题的风险。

从以上过程可以看出,三段式提交对单点问题(协调者等待参与者的超时问题)和回滚时的性能问题有所改善,但是对一致性风险问题并未有任何改进,甚至是略有增加的。譬如,进入PreCommit阶段之后,协调者发出的指令不是Ack而是Abort,而此时因网络问题,有部分参与者直至超时都未能收到协调者的Abort指令的话,这些参与者将会错误地提交事务,这就产生了不同参与者之间数据不一致的问题。

image.png

4.共享事务(Share Transaction)

与全局事务相反,共享事务是指多个服务共用同一个数据源。 很显然,多个服务使用一个数据源会有竞争问题,但是因为数据源的事务特性,其实和本地事务也一样。 共享事务的问题主要是“事务边界模糊”与“隔离级别不足问题”,复杂度高于本地事务

  • 事务边界模糊 即“事务的开始和结束由各个服务独立控制,难以保证事务边界的清晰性。如果某个服务未正确提交或回滚,可能影响其他服务的数据一致性”。
  • 隔离级别不足 业务逻辑的复杂性可能超出隔离级别的控制范围,即需要服务应用协调控制来保证数据一致性。

这个“隔离级别不足”问题记一下,分布式事务也存在这个问题。为什么不足呢?因为资源也是分布式的,有多数据源需要进行数据隔离,ACID是本地性质的,做不了多数据源的。

本篇主要还是讨论事务,不拓展单点访问性能瓶颈、资源消耗问题。

共享数据源连接

一种理论可行的方案是让各个服务共享数据库连接,这样可以共享事务,将共享事务转化成全局事务。

“一些中间件服务器就是使用共享数据源连接的方式,如WebSphere。” “共享的前提是所有使用数据源的服务都在同一个进程内”

看到这里可能就有疑惑了,如果所有服务都在同一个进程,那不就是个单服务是一样的了么? 没错,所以这种方案需要一个“中间服务层”,使用数据源的多个服务通过“中间服务层”来连接数据源。 如下图所示:

image.png 中间服务可以是一个服务用于处理数据库连接,现实中有类似ProxySQL、MaxScale这样用于多个数据库实例做负载均衡的数据库代理,其代理单个数据源时比较接近这种模式。 还有一种变种方式是使用消息队列当做中间服务。

不过因为通常情况下,一个服务集群里数据库才是压力最大而又最不容易伸缩拓展的“重灾区”。 实际很少有这样的方案,毕竟多服务至少是拆分了微服务的情况,都拆分微服务了目的肯定是低耦合易拓展,这种模式……

5.分布式事务(Distributed Transaction)

分布式事务(Distributed Transaction)特指多个服务同时访问多个数据源的事务处理机制。

CAP理论与ACID的矛盾(难以达成的强一致性)

CAP理论描述了在一个分布式系统中,涉及共享数据问题时, CAP三个特性只能满足其中两个。 在事务这个篇章,肯定是需要满足一致性 Consistency的,一致性按照过程又分为强一致性与弱一致性。

  • 强一致性 强一致性要求分布式系统中所有节点在任何时刻看到的数据都是完全一致的。即,一旦某个数据被更新,所有后续的读取操作(无论从哪个节点读取)都能立即反映最新的值。
  • 弱一致性(最终一致性) 允许数据源集群节点的数据状态有短暂的不一致。在更新后的短暂时间内,不同节点可能看到的数据不一致,但随着时间推移(一些同步机制),数据会收敛到一致。

了解定义后,我们很容易明白前面同样是多数据源的“全局事务”追求的是强一致性,ACID的C本身追求的也是强一致性。 然而,在分布式多服务多数据源的情况下,追求强一致性会有一些根本性困难。

  • 1.网络不可靠 在全局事务中我们有了解到,无论是2PC还是3PC,都无法解决网络不可靠的问题,只能说3PC多了一轮询问,事务成功的可能性更大,且解决了超时问题,但是一致性在不可靠网络情况下还是很难保证。 在分布式多服务系统中,网络会更不可靠,追求强一致性C就会牺牲可用性A。
  • 2.分布式系统故障是常态 多服务多数据源意味着很多的参与方,任何一个节点故障都可能导致事务失败。 故障重试、超时等待都会有性能损耗,违背了分布式追求高新能的目标。而且事务在分布式系统中涉及资源范围大,故障时因追求强一致性使用的锁持有时间也会增加,又违背了分布式追求高并发的目标。

其他困难还有协调成本指数增长导致的可扩展性受限等……

全局事务追求强一致性(C),但在多服务、多数据源的分布式系统中,P 是常态,因此强一致性只能通过牺牲 A(可用性)实现。而许多业务场景(如电商、社交网络)无法接受服务不可用,这些场景多选择最终一致性。

最终一致性

强一致性难以实现就实现最终一致性嘛,不过继续了解最终一致性之前,我们需要先留了解BASE模型,BASE模型系统化描述了如何在分布式系统中实现最终一致性。

BASE模型

BASE模型是作为ACID模型的对立面或补充,提供了一种在分布式系统中实现一致性的替代思路。 “BASE”是基本可用性(Basically Available)、柔性事务(Soft State)和最终一致性(Eventually Consistent)的缩写。 BASE模型是对最终一致性思想的系统化总结和应用,其核心思想是“放弃实时强一致性,转而追求高可用性和系统的柔韧性,最终通过时间和补偿机制达到一致状态”。

模型的概念与理解如下⬇️:

BASE模型
  1. Basically Available(基本可用性)

    • 含义:系统大部分时间保持可用。即使出现故障或也能尽量提供服务,而不是完全不可用。
    • 实现:熔断+服务降级或流量控制,或者牺牲部分一致性以保证响应,比如服务降级中使用旧数据缓存。
    • 示例:电商系统在高峰期可能暂时显示“库存稍后更新”,但仍接受订单,而不是拒绝服务。 具体的可用性可以看“异常处理”这篇,都是保证可用性,基本可用性实现基本是柔和的服务降级。
  2. Soft State(柔性状态)

    • 含义:系统状态可以在一段时间内处于不一致或“柔性”的状态,不要求所有节点实时同步。状态的变化是渐进的,可能需要外部干预(如同步机制)来调整。
    • 实现:数据副本间允许短暂差异,依赖异步更新或补偿机制逐步修正。
    • 示例:社交媒体点赞数在不同服务器上可能短时间内不同,用户看到的数据是“软状态”,最终会同步。 这部分主要是理解柔性的概念,即不一致也没关系,主要是在设计系统、分析业务的时候,要能抓住应用场景中的柔性的部分,当然,不要忘记目标,即设计补偿机制达成最终一致。 总结:理解柔性并针对柔性点设计+补偿机制以最终一致。
  3. Eventually Consistent(最终一致性)

    • 含义:如果没有新的更新操作,系统会在一段时间后达到一致状态,所有节点最终看到相同的数据。
    • 实现:通过后台同步(如日志重放、消息队列)或冲突解决(如版本号、时间戳)实现一致性。
    • 示例:DNS 更新后,全球服务器逐步同步,最终所有解析结果一致。 主要是了解最终一致性的实现方式,即“一段时间后达成一致”这个一段时间做了什么,如下⬇️
BASE模型实现方式
  1. 异步复制

    • 主节点完成写操作后立即返回,异步将更新传播到其他节点。
    • 示例:Cassandra 的写操作,客户端写入成功后,副本逐步同步。

    这里的传播方式有“状态转移”与“操作转移”,具体可以看这篇"分布式数据共享方式"。 即:异步传播+等待

  2. 补偿机制(Saga 模式)

    • 将大事务拆分为多个小事务,每个小事务直接提交。若某个小事务失败,则通过执行补偿操作回滚所有已提交的小事务,以实现最终一致性。
    • 示例:订单处理中,扣款成功但发货失败,则执行退款补偿,恢复初始状态。这种方式不同于传统事务的同步回滚(如 2PC),而是基于应用层的异步补偿。

    即分阶段完成事务,不用同2PC或者3PC一样整体事务都完成才算,而是可以分阶段完成,且不同阶段都有阶段状态感知,感知后进行失败补偿方案。 需要应用场景设计耦合低才方便做拆分与补偿。

  3. 事件驱动

    • 通过消息队列(如 Kafka)发布更新事件,其他节点订阅并应用变更。
    • 示例:库存减少后发布事件,通知其他服务更新缓存。

    数据异步通信传播的一种方式,但更适合分布式系统多服务多通信,因为有队列可以缓冲请求数据,且可以重试等。

  4. 冲突解决

    • 使用版本号、向量时钟或业务规则(如“最后写入胜出”)处理数据冲突。
    • 示例:DynamoDB 使用版本号确保最终一致。

    最终要达成的一致性,可以通过版本号来辨别是否完成操作。

可靠事件队列

可靠事件队列是以消息队列为中间件,将分布式事务投递进队列,利用消息队列的确保消费的机制进行进行事务提交。体现了BASE模型的“事件驱动”,同时,也体现了Sage模型的事务拆分+补偿。 设计上来说,也是很好的异步解耦防阻塞的事务方式,时序图如下⬇️:

image.png

核心是“事件队列”+确认机制(Ack Mechanism),即常见的消息队列,消息队列确保消费后才移除的特性。

TCC模式

在共享事务一节中,我们有了解到共享事务多服务单数据源存在“事务隔离级别不足,需要服务应用协调控制来保证数据的一致性”问题。 分布式事务也有这样的隔离级别不足问题,在了解过BASE模型的实现后,很显然,其并没有处理这个问题,还是需要服务引用协调来控制,即需要应用层模拟隔离性。

TCC分布式事务机制就是为了解决分布式事务“隔离级别不足问题”而诞生的。 TCC是另一种常见的分布式事务机制,它是“Try-Confirm/Cancel”3个单词的缩写。 如名字所示,分为3个阶段⬇️

  • Try:尝试执行阶段,完成所有业务可执行性的检查,并且预留好全部需要用到的业务资源(保障隔离性)。Try阶段可能会重复执行,因此本阶段执行的操作需要具备幂等性。 即检查并预留资源,预留即用锁的方式提前锁定资源。 例如:冻结库存、预扣账户余额

  • Confirm:确认执行阶段,不进行任何业务检查,直接使用Try阶段准备的资源来完成业务处理。Confirm阶段也可能会重复执行,也需要具备幂等性。 即如果Try阶段成功,则执行真正的提交操作,完成资源变更。 例如:确认库存减少、确认扣款

  • Cancel:取消执行阶段,释放Try阶段预留的业务资源。Cancel阶段也可能会重复执行,也需要具备幂等性。 即Try失败,进行回滚,释放预留资源。 例如:解冻库存、退回余额。

以转账场景为例

image.png

总结一下TCC Try+Confirm/Cancel就是通过预留资源补偿机制避免分布式事务的阻塞问题(较2PC与3PC)。不依赖数据库的隔离性,将隔离性实现提前到应用层,解决分布式事务隔离级别不够问题。 这样前面全局事务的阻塞问题+共享事务隔离级别不够问题就都解决啦。 还有网络问题,TCC的3个阶段都要求支持幂等=支持重试,可以一定程度上解决网络不可靠问题。

TCC总的来说,比2PC灵活且不用等待,是非阻塞状态。 且提供了分布式事务的“网络不可靠”、“隔离级别需要服务应用协调”、“协调者需要阻塞等待参与者结果”这3个问题的解决方案,性能更高,也和BASE模型一样体现了事务拆分,更灵活。

缺点是都需要为事务中的每个服务实现Try、Confirm、Cancel 这3个接口,且接口要求幂等+重试逻辑,设计和测试难度较高,逻辑更复杂,因为一般分布式事务方案都是融合的,比如TCC+可靠时间队列(异步处理)+补偿机制(其他操作)+基本可用(服务降级)。

Sage模式

TCC模式很好,能解决多数问题。但是其在部分场景应用有一个缺陷,即在TCC的Try阶段,核心的机制“资源预留”——预留不了怎么办?例如:银行业一些数据资源。 此时,TCC适用性收到限制,有没有一个事务机制或模式可以和TCC一样解决分布式事务的大多数问题呢? 有的,那就是Sage模式。 Saga 是一种分布式事务模式,用于处理长生命周期事务(Long-Lived Transactions)。在分布式系统中,Saga 将一个全局事务拆分为一系列本地事务,每个本地事务由独立的服务执行,并通过补偿机制(Compensation)保证最终一致性。 在BASE模型中有提过Sage模式,这里算是战术展开仔细了解。

Sage核心思想就是 事务拆分+顺序执行+补偿机制。

  • 事务拆分:将一个大事务分解为多个小的本地事务,每个事务直接提交(无预留阶段)。 Sage拆分后的事务和2PC还有TCC都不一样,不阻塞(都拆了等啥)也不预留资源,而是直接提交。 大事务拆分成小事务后可以不会长时间锁定数据库的资源。

  • 顺序执行:本地事务按顺序(或并行)执行,前一个事务成功后触发下一个。 这么做的理由是方便失败时进行补偿。事务顺序执行,补偿与事务意义对应顺序恢复。

  • 补偿机制:如果某个本地事务失败,已成功提交的本地事务会通过对应的补偿操作抵消其影响,补偿的触发可以由中央协调者指挥(指挥式Sage)或通过事件分布式协作(编排式Sage)完成。 这里补偿动作的进行与Sage模型的不同实现方式有关,补偿动作的本质是反向操作抵消影响。

事务拆分与补偿策设计

Sage事务拆分方式如下⬇️: 将大事务拆分成若干个小事务,将整个分布式事务 T 分解为 n 个子事务,命名为 T1, T2, …, Ti, …, Tn。每个子事务都应该是或能被视为原子行为。如果分布式事务能够正常提交,其对数据的影响(即最终一致性)应与按顺序连续成功提交所有子事务 T1, T2, …, Tn 的效果等价。 为每一个子事务设计对应的补偿动作,命名为C1,C2,…,Ci,…,Cn。 Ti与Ci必须具备幂等性、满足交换律(无论先执行T1还是C1,效果是一样的),补偿动作C不许能提交成功,即补偿必须成功。

在拆分事务和建立事务补偿机制时,必须确保每个子事务与其对应的补偿操作之间存在明确的联系,以保证失败时能够正确抵消已提交事务的影响。 Saga 模式中失败补偿机制的本质是通过执行反向操作回滚已完成的事务,但其具体实现可能因场景而异,显得有些灵活。 例如,可以结合失败重试(尝试执行补偿操作至最大重试次数)确保补偿成功,或在特定情况下通过服务降级作为辅助手段以维持系统可用性,但这些并非补偿机制的定义本身

比如,有两种事务恢复策略⬇️,其中正向恢复在定义上不算做补偿机制:

  • 正向恢复(Forward Recovery):如果子事务 Ti 提交失败,则持续重试 Ti 直到成功(最大努力交付)。这种策略不涉及补偿操作,适用于事务最终必须成功的场景,例如“银行扣款后必须发货”。正向恢复的执行模式为:T1, T2, ..., Ti(失败), Ti(重试), ..., Ti+1, ..., Tn。由于不依赖反向操作,正向恢复在定义上不属于补偿机制。
  • 反向恢复(Backward Recovery):如果子事务 Ti 提交失败,则执行对应的补偿操作 Ci 来抵消之前已提交事务的影响,并依次回滚之前的子事务(C(i-1), ..., C1),直至所有补偿操作成功(最大努力交付)。反向恢复的执行模式为:T1, T2, ..., Ti(失败), Ci(补偿), ..., C2, C1。这种策略是 Saga 模式中补偿机制的典型实现,Saga 模式通常采用反向恢复。

这里不得不提一下Sage和TCC不一样的地方。 TCC事务失败不全部回滚,那一Try阶段失败,则执行对应的Cancel即可。 而Sage补偿机制全部回归和2PC类似,全部回滚至初始状态,不同的是2PC是同步阻塞,而Sage是异步补偿,后面实现方式会将。

实现方式

Sage有两种实现方式,编排式与指挥式。机制还是事务拆分+顺序执行+补偿机制,不过事务执行方式、补偿方式不同。

编排式 Saga(Choreography)

编排式 Saga 是一种去中心化的实现方式,没有中央协调者。服务之间通过事件驱动机制协作,每个服务在完成本地事务后发布事件,触发下一个服务执行。失败时通过事件触发补偿操作。 每个服务监听和发布事件,按顺序执行本地事务。若某个服务失败,发布失败事件,之前的服务监听到后执行补偿。 优点:服务松耦合,无单点故障。(适合高并发、去中心化系统) 缺点:分布式协作复杂,状态追踪困难。

image.png

指挥式 Saga(Orchestration)

指挥式 Saga 使用一个中央协调者(Orchestrator)来指挥所有服务的执行。协调者按顺序调用每个服务的本地事务,失败时调用补偿操作。 协调者依次调用服务执行本地事务。若某个事务失败,协调者按逆序调用补偿操作。 优点:逻辑清晰,易于管理和调试。(适合业务逻辑复杂、需集中控制的场景) 缺点:协调者可能成为单点瓶颈。

image.png

Sage总结

Sage较之前的2PC与TCC,因为事务回滚机制的同步与事务调度方式不同,新能上有所差异。 2PC实现比较简单,但有着网络不可靠、事务隔离级别不足问题。 TCC解决了2PC问题且是非阻塞状态,且可以有更柔性的实现,如融合服务降级+补偿机制,但是需要服务应用“预留资源”,在部分场景中使用不了。 Sage不用预留资源,且也有补偿机制,且补偿机制较2PC全部回滚更灵活,只需要回滚已经提交的,且补偿机制可以按顺序部分回滚。

总结

总的来说,在设计分布式事务时不同方案有不同的优缺点。

2PC/3PC与TCC、Sage

关联

以解决事务问题角度来说,2PC/3PC与TCC、Sage关联如下⬇️: 2PC:实现简单,保证强一致性,但网络不可靠(阻塞)、隔离性不足(跨服务弱),性能低。 3PC:改进 2PC 的容错性,减少阻塞,但仍同步、复杂,实际应用少。 TCC:解决 2PC 的阻塞问题,非阻塞且柔性(可融合降级、事件队列),但需预留资源,部分场景受限。 Saga:无需预留,补偿机制灵活(反向回滚或正向重试),比 2PC 更适应分布式环境,但一致性窗口长,开发复杂。

适用场景

2PC 和 3PC 是传统强一致性方案,简单但阻塞,适合小规模场景。 TCC 和 Saga 是柔性事务方案,非阻塞且高性能,适应分布式环境。 TCC 依赖资源预留,解决隔离和网络问题。 Saga 无需预留,通过补偿灵活回滚,兼顾复杂流程。 它们在一致性、性能和适用性上形成递进关系,TCC 和 Saga 更贴近现代分布式系统需求。

特性2PC3PCTCCSaga
一致性模型强一致性强一致性最终一致性最终一致性
事务调度同步,协调者驱动同步,协调者驱动异步,应用层驱动异步,事件/协调者驱动
失败恢复同步回滚未提交同步回滚 + 容错异步 Cancel 回滚异步补偿回滚/重试
网络不可靠阻塞,易失败减少阻塞,仍脆弱重试 + 幂等重试 + 幂等
事务隔离数据库锁,跨服务弱数据库锁,跨服务弱预留资源,应用层协调无预留,应用层协调
性能低,同步等待低,多次通信高,非阻塞高,异步处理
开发复杂性简单,数据库支持中等,较少使用高,三接口实现高,补偿设计
适用性小规模强一致性理论改进 2PC可预留资源场景复杂流程、无预留场景

资料引用

《凤凰架构:构建可靠的大型分布式系统》