分布式事务及实现方案

0 阅读25分钟

前言

文章主要参考的是分布式系统 - 分布式事务及实现方案,之所以二次编辑,是为了增加部分自己理解的内容。

什么是事务

参考文章事务

什么是分布式事务

分布式事务是指在分布式系统中需要保证原子性、隔离性、一致性和持久性的一个或多个数据库操作。而且这些操作可能是位于不同的服务中。

从分布式的理论的角度看

  • 分布式理论的CP -> 刚性事务

    • 遵循ACID,对数据要求强一致性
  • 分布式理论的AP+BASE -> 柔性事务

    • 遵循BASE,允许一定时间内不同节点的数据不一致,但要求最终一致。

分布式事务体系

graph TD
    %% --- 样式定义 (去掉分号,防止解析错误) ---
    classDef rootFill fill:#800000,stroke:#333,stroke-width:2px,color:#fff
    classDef orangeFill fill:#E65100,stroke:#333,stroke-width:1px,color:#fff
    classDef greenFill fill:#43A047,stroke:#333,stroke-width:1px,color:#fff
    classDef blueFill fill:#01579B,stroke:#333,stroke-width:1px,color:#fff
    classDef purpleFill fill:#6200EA,stroke:#333,stroke-width:1px,color:#fff
    classDef yellowFill fill:#FBC02D,stroke:#333,stroke-width:1px,color:#333

    %% --- 主树结构 ---
    Root["分布式事务"]:::rootFill

    %% 第一层
    Tech["技术方案"]:::orangeFill
    Mid["中间件"]:::orangeFill
    Root --> Tech
    Root --> Mid

    %% --- 技术方案分支 ---
    XA["基于XA协议<br/>(DB层)"]:::greenFill
    Comp["基于补偿<br/>(业务层)"]:::blueFill
    Final["基于最终一致性"]:::blueFill

    Tech --> XA
    Tech --> Comp
    Tech --> Final

    %% XA 细节
    TwoPC["2PC"]:::greenFill
    ThreePC["3PC"]:::greenFill
    JTA["JTA,JTS"]:::greenFill

    XA --> TwoPC
    XA -- "Java规范和实现" --> JTA
    TwoPC -- "优化" --> ThreePC

    %% 补偿 细节
    TCC_Biz["TCC"]:::blueFill
    Saga_Biz["Saga"]:::blueFill
    Comp --> TCC_Biz
    Comp --> Saga_Biz

    %% 最终一致性 细节
    MsgTable["消息表"]:::blueFill
    MsgQueue["消息队列"]:::blueFill
    BestEffort["最大努力通知"]:::blueFill
    Final --> MsgTable
    Final --> MsgQueue
    Final --> BestEffort

    %% --- 中间件分支 ---
    Atomiks["Atomiks"]:::greenFill
    Seata["Seata"]:::orangeFill
    TXLCN["TX-LCN"]:::orangeFill
    Other["..."]:::orangeFill

    Mid --> Atomiks
    Mid --> Seata
    Mid --> TXLCN
    Mid --> Other

    %% 中间件子节点
    Atom_XA["XA"]:::greenFill
    Atomiks --> Atom_XA

    Seata_XA["XA"]:::greenFill
    Seata_AT["AT"]:::greenFill
    Seata_TCC["TCC"]:::blueFill
    Seata_Saga["Saga"]:::blueFill
    Seata --> Seata_XA
    Seata --> Seata_AT
    Seata --> Seata_TCC
    Seata --> Seata_Saga

    LCN_LCN["LCN"]:::yellowFill
    LCN_TCC["TCC"]:::blueFill
    LCN_TXC["TXC"]:::yellowFill
    TXLCN --> LCN_LCN
    TXLCN --> LCN_TCC
    TXLCN --> LCN_TXC

    %% --- 底部理论部分 ---
    %% 使用子图将底部理论框起来,并保证逻辑清晰

    subgraph Theory["从分布式理论理解"]
        direction LR
        TheoryRoot["从分布式理论理解"]:::purpleFill
        
        Rigid["CP + ACID => 刚性事务"]:::greenFill
        Soft["AP + BASE => 柔性事务"]:::blueFill
        
        ApplyDB["适用数据库层"]:::greenFill
        ApplyBiz["适用业务层"]:::blueFill

        TheoryRoot --> Rigid --> ApplyDB
        TheoryRoot --> Soft --> ApplyBiz
    end

刚性事务: 分布式理论的CP,遵循ACID,对数据要求强一致性。

  • XA协议 是一个基于数据库层面的分布式事务协议,它定义了两个角色如何配合工作:

    • 事务管理器 (Transaction Manager, TM) :即“协调者/指挥官”。它负责统筹全局,指挥各个数据库是该提交还是回滚。
    • 本地资源管理器 (Resource Manager, RM) :即“参与者/干活的”。通常就是指数据库本身(如 MySQL, Oracle)。

    二阶段提交 (2PC) :是基于 XA 接口的具体执行流程

    1. 第一阶段(准备/投票阶段) :协调者问所有参与者:“你们都能执行成功吗?”参与者执行操作但不提交,锁住资源,然后告诉协调者:“我可以”或“我不行”。
    2. 第二阶段(提交/执行阶段)
      • 如果大家都说“我可以”,协调者下令:“全体提交 (Commit)!”

      • 只要有一个说“我不行”,协调者下令:“全体回滚 (Rollback)!”

    1. 如果 prepare 返回 YES,但 commit 阶段硬件出问题了怎么办?

        一旦参与者在 prepare 阶段返回了 YES,它就失去了对自己命运的掌控权。它必须无条件服从协调者(Coordinator)的最终决定。它承诺了:“我已经准备好了,只要你一声令下,我随时可以提交,也随时可以回滚。”

        如果是短暂宕机(如重启) :数据库重启后,会检查事务日志(WAL)。它会发现有一条处于 Prepared 状态但没有 Commit 标记的记录(即**“不决状态” In-Doubt Transaction**)。此时,它必须主动去联系协调者:“喂,刚才那个 XID 的事务,大家最后到底是提交了还是回滚了?”

    • 如果协调者说“提交”,它就补做提交。
    • 如果协调者说“回滚”,它就补做回滚。

        如果是协调者也挂了:这就是最坏的情况。参与者联系不上协调者,不知道其他兄弟节点是成功还是失败了。为了保证数据一致性(不能别人回滚了我却提交了),它只能阻塞等待,一直持有锁,直到协调者恢复。这通常需要人工介入(DBA 手动强行 Commit 或 Rollback)。

    1. p.prepare()p.commit() 中间一直在锁着资源吗? 一直锁着。 这正是 2PC 性能差的根本原因。

    三阶段提交 (2PC) :是基于 XA 接口的具体执行流程

    1. 第一阶段(询问阶段):协调者问:“大家身体健康吗?网络通吗?能接单吗?”。参与者答:Yes/No。
    2. 第二阶段(预提交阶段):如果第一阶段大家都说 Yes。协调者说:“好,现在大家开始锁资源、写日志(执行事务逻辑),但别提交!如果我后面不理你们了,你们就自己看着办。”
    3. 第三阶段(提交阶段)
      • 如果大家都说“我可以”,协调者下令:“全体提交 (Commit)!”

      • 只要有一个说“我不行”,协调者下令:“全体回滚 (Rollback)!”

    1. 如果第一阶段大家都说 No,但是协调者挂了,怎么办?

    此时参与者将无法正常收到消息,但在分布式系统中, “没有收到消息” 有两种可能:

    • 协调者挂了(3PC 赌的是这个)。
    • 协调者没挂,只是网络断了,而且协调者其实刚刚决定了要 Abort。 一旦协调者挂了,在等待一段时间后,参与者会默认提交对应的事务。导致数据一致性问题。

柔性事务:分布式理论的AP,遵循BASE,允许一定时间内不同节点的数据不一致,但要求最终一致。

  • TCC: TCC(Try-Confirm-Cancel)又被称补偿事务,TCC与2PC的思想很相似,事务处理流程也很相似,但2PC是应用于在DB层面,TCC则可以理解为在应用层面的2PC,是需要我们编写业务逻辑来实现。

    在 2PC (XA) 中,只需要告诉数据库 Prepare(准备)和 Commit(提交)。如果在 Prepare 阶段失败了,数据库引擎会自动利用 Undo Log 把数据回滚回去。不需要写代码去恢复数据。

    “应用层面”的意思是:事务的控制权掌握在代码 (Business Logic) 手里。数据库把普通的本地事务处理完了,这个过程中程序员需要利用好本地事务。

  • SAGA:Saga是由一系列的本地事务构成。每一个本地事务在更新完数据库之后,会发布一条消息或者一个事件来触发Saga中的下一个本地事务的执行。如果一个本地事务因为某些业务规则无法满足而失败,Saga会执行在这个失败的事务之前成功提交的所有事务的补偿操作。Saga的实现有很多种方式,其中最流行的两种方式是:基于事件的方式和基于命令的方式。
  • 最终一致性
    • 消息表:本地消息表的方案最初是由 eBay 提出,核心思路是将分布式事务拆分成本地事务进行处理。
    • 消息队列:基于 MQ 的分布式事务方案其实是对本地消息表的封装,将本地消息表基于 MQ 内部,其他方面的协议基本与本地消息表一致。
    • 最大努力通知:最大努力通知也称为定期校对,是对MQ事务方案的进一步优化。它在事务主动方增加了消息校对的接口,如果事务被动方没有接收到消息,此时可以调用事务主动方提供的消息校对的接口主动获取。

分布式事务方案之刚性事务

说到刚性事务,首先要讲的是XA协议。

XA协议是一个基于 数据库 的分布式事务协议,其分为两部分:事务管理器(Transaction Manager)本地资源管理器(Resource Manager) 。事务管理器作为一个全局的调度者,负责对各个本地资源管理器统一号令提交或者回滚。二阶提交协议(2PC)三阶提交协议(3PC)就是根据此协议衍生出来而来。主流的诸如Oracle、MySQL等数据库均已实现了XA接口。

两段提交(2PC)

引入一个作为协调者(coordinator)的组件来统一掌控所有参与者(participant)的操作结果,并最终指示这些节点是否要把操作结果进行真正的提交。

  • 第一阶段: 准备阶段;

    • 协调者向所有参与者发送 REQUEST-TO-PREPARE
    • 当参与者收到REQUEST-TO-PREPARE 消息后, 它向协调者发送消息PREPARED或者NO,表示事务是否准备好;如果发送的是NO,那么事务要回滚;
  • 第二阶段: 提交阶段。

    • 协调者收集所有参与者的返回消息, 如果所有的参与者都回复的是PREPARED, 那么协调者向所有参与者发送COMMIT 消息;否则,协调者向所有回复PREPARED的参与者发送ABORT消息;
    • 参与者如果回复了PREPARED消息并且收到协调者发来的COMMIT消息,或者它收到ABORT消息,它将执行提交或回滚,并向协调者发送DONE消息以确认。

image.png

两段提交(2PC)的缺点

二阶段提交看似能够提供原子性的操作,但它存在着严重的缺陷:

  • 网络抖动导致的数据不一致:第二阶段中协调者向参与者发送commit命令之后,一旦此时发生网络抖动,导致一部分参与者接收到了commit请求并执行,可其他未接到commit请求的参与者无法执行事务提交。进而导致整个分布式系统出现了数据不一致。
  • 超时导致的同步阻塞问题:2PC中的所有的参与者节点都为事务阻塞型,当某一个参与者节点出现通信超时,其余参与者都会被动阻塞占用资源不能释放。
  • 单点故障的风险:由于严重的依赖协调者,一旦协调者发生故障,而此时参与者还都处于锁定资源的状态,无法完成事务commit操作。虽然协调者出现故障后,会重新选举一个协调者,可无法解决因前一个协调者宕机导致的参与者处于阻塞状态的问题。

三段提交(3PC)

三段提交(3PC)是对两段提交(2PC)的一种升级优化,3PC在2PC的第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前,各参与者节点的状态都一致。同时在协调者和参与者中都引入超时机制,当参与者各种原因未收到协调者的commit请求后,会对本地事务进行commit,不会一直阻塞等待,解决了2PC的单点故障问题,但3PC还是没能从根本上解决数据一致性的问题。

3PC的三个阶段分别是CanCommit、PreCommit、DoCommit

  • CanCommit:协调者向所有参与者发送CanCommit命令,询问是否可以执行事务提交操作。如果全部响应YES则进入下一个阶段。
  • PreCommit:协调者向所有参与者发送PreCommit命令,询问是否可以进行事务的预提交操作,参与者接收到PreCommit请求后,如参与者成功的执行了事务操作,则返回Yes响应,进入最终commit阶段。一旦参与者中有向协调者发送了No响应,或因网络造成超时,协调者没有接到参与者的响应,协调者向所有参与者发送abort请求,参与者接受abort命令执行事务的中断。
  • DoCommit:在前两个阶段中所有参与者的响应反馈均是YES后,协调者向参与者发送DoCommit命令正式提交事务,如协调者没有接收到参与者发送的ACK响应,会向所有参与者发送abort请求命令,执行事务的中断。

image.png

3PC存在的问题

3PC工作在同步网络模型上,它假设消息传输时间是有上界的,只存在机器失败而不存在消息失败。这个假设太强,现实的情形是,机器失败是无法完美地检测出来的,消息传输可能因为网络拥堵花费很多时间。同时, 说阻塞是相对, 存在协调者和参与者同时失败的情形下, 3PC事务依然会阻塞。实际上,很少会有系统实现3PC,多数现实的系统会通过复制状态机解决2PC阻塞的问题。比如,如果失败模型不是失败-停止, 而是消息失败(消息延迟或网络分区),那样3PC会产生不一致的情形。

3PC小结

3PC并没有完美解决2PC的阻塞,也引入了新的问题(不一致问题),所以3PC很少会被真正的使用

分布式事务方案之柔性事务

柔性事务:分布式理论的AP,遵循BASE,允许一定时间内不同节点的数据不一致,但要求最终一致。

补偿事务 (TCC)

TCC(Try-Confirm-Cancel)又被称补偿事务,TCC与2PC的思想很相似,事务处理流程也很相似,但2PC是应用于在DB层面,TCC则可以理解为在应用层面的2PC,是需要我们编写业务逻辑来实现

TCC它的核心思想是:"针对每个操作都要注册一个与其对应的确认(Try)和补偿(Cancel)"。

还拿下单扣库存解释下它的三个操作:

  • Try阶段:下单时通过Try操作去扣除库存预留资源。
  • Confirm阶段:确认执行业务操作,在只预留的资源基础上,发起购买请求。
  • Cancel阶段:只要涉及到的相关业务中,有一个业务方预留资源未成功,则取消所有业务资源的预留请求。

image.png

TCC的缺点

  1. 空回滚

    当一个分支事务所在的服务发生宕机或者网络异常导致调用失败,并未执行try方法,当恢复后事务执行回滚操作就会调用此分支事务的cancel方法,如果cancel方法不能处理此种情况就会出现空回滚。

    是否出现空回滚,我们需要需要判断是否执行了try方法,如果执行了就没有空回滚。解决方法就是当主业务发起事务时,生成一个全局事务记录,并生成一个全局唯一ID,贯穿整个事务,再创建一张分支事务记录表,用于记录分支事务,try执行时将全局事务ID和分支事务ID存入分支事务表中,表示执行了try阶段,当cancel执行时,先判断表中是否有该全局事务ID的数据,如果有则回滚,否则不做任何操作。比如seata的AT模式中就有分支事务表。

  2. 幂等问题

    由于服务宕机或者网络问题,方法的调用可能出现超时,为了保证事务正常执行我们往往会加入重试的机制,因此就需要保证confirm和cancel阶段操作的幂等性。

    我们可以在分支事务记录表中增加事务执行状态,每次执行confirm和cancel方法时都查询该事务的执行状态,以此判断事务的幂等性。

  3. 悬挂问题

    TCC中,在调用try之前会先注册分支事务,注册分支事务之后,调用出现超时,此时try请求还未到达对应的服务,因为调用超时了,所以会执行cancel调用,此时cancel已经执行完了,然而这个时候try请求到达了,这个时候执行了try之后就没有后续的操作了,就会导致资源挂起,无法释放。

    执行try方法时我们可以判断confirm或者cancel方法是否执行,如果执行了那么就不执行try阶段。同样借助分支事务表中事务的执行状态。如果已经执行了confirm或者cancel那么try就执行。

Saga事务

Saga是由一系列的本地事务构成。每一个本地事务在更新完数据库之后,会发布一条消息或者一个事件来触发Saga中的下一个本地事务的执行。如果一个本地事务因为某些业务规则无法满足而失败,Saga会执行在这个失败的事务之前成功提交的所有事务的补偿操作。

Saga的实现有很多种方式,其中最流行的两种方式是:

  • 基于事件的方式。这种方式没有协调中心,整个模式的工作方式就像舞蹈一样,各个舞蹈演员按照预先编排的动作和走位各自表演,最终形成一只舞蹈。处于当前Saga下的各个服务,会产生某类事件,或者监听其它服务产生的事件并决定是否需要针对监听到的事件做出响应。
  • 基于命令的方式。这种方式的工作形式就像一只乐队,由一个指挥家(协调中心)来协调大家的工作。协调中心来告诉Saga的参与方应该执行哪一个本地事务。

我们继续以订单流程为例,说明一下该模式。

假设一个完整的订单流程包含了如下几个服务:

  1. Order Service:订单服务
  2. Payment Service:支付服务
  3. Stock Service:库存服务
  4. Delivery Service:物流服务

image.png

基于事件的方式

在基于事件的方式中,第一个服务执行完本地事务之后,会产生一个事件。其它服务会监听这个事件,触发该服务本地事务的执行,并产生新的事件。

采用基于事件的saga模式的订单处理流程如下:

image.png

  • 订单服务创建一笔新订单,将订单状态设置为"待处理",产生事件ORDER_CREATED_EVENT。
  • 支付服务监听ORDER_CREATED_EVENT,完成扣款并产生事件BILLED_ORDER_EVENT。
  • 库存服务监听BILLED_ORDER_EVENT,完成库存扣减和备货,产生事件ORDER_PREPARED_EVENT。
  • 物流服务监听ORDER_PREPARED_EVENT,完成商品配送,产生事件ORDER_DELIVERED_EVENT。
  • 订单服务监听ORDER_DELIVERED_EVENT,将订单状态更新为"完成"。

在这个流程中,订单服务很可能还会监听BILLED_ORDER_EVENT,ORDER_PREPARED_EVENT来完成订单状态的实时更新。将订单状态分别更新为"已经支付"和"已经出库"等状态来及时反映订单的最新状态。

该模式下分布式事务的回滚

为了在异常情况下回滚整个分布式事务,我们需要为相关服务提供补偿操作接口。

假设库存服务由于库存不足没能正确完成备货,我们可以按照下面的流程来回滚整个Saga事务:

image.png

  1. 库存服务产生事件PRODUCT_OUT_OF_STOCK_EVENT。
  2. 订单服务和支付服务都会监听该事件并做出响应:
    1. 支付服务完成退款。
    2. 订单服务将订单状态设置为"失败"。

基于事件方式的优缺点

优点:简单且容易理解。各参与方相互之间无直接沟通,完全解耦。这种方式比较适合整个分布式事务只有2-4个步骤的情形。

缺点:这种方式如果涉及比较多的业务参与方,则比较容易失控。各业务参与方可随意监听对方的消息,以至于最后没人知道到底有哪些系统在监听哪些消息。更悲催的是,这个模式还可能产生环形监听,也就是两个业务方相互监听对方所产生的事件。

接下来,我们将介绍如何使用命令的方式来克服上面提到的缺点。

基于命令的方式

在基于命令的方式中,我们会定义一个新的服务,这个服务扮演的角色就和一支交响乐乐队的指挥一样,告诉各个业务参与方,在什么时候做什么事情。我们管这个新服务叫做协调中心。协调中心通过命令/回复的方式来和Saga中其它服务进行交互。

我们继续以之前的订单流程来举例。下图中的Order Saga Orchestrator就是新引入的协调中心。

image.png

  • 订单服务创建一笔新订单,将订单状态设置为"待处理",然后让Order Saga Orchestrator(OSO)开启创建订单事务。
  • OSO发送一个"支付命令"给支付服务,支付服务完成扣款并回复"支付完成"消息。
  • OSO发送一个"备货命令"给库存服务,库存服务完成库存扣减和备货,并回复"出库"消息。
  • OSO发送一个"配送命令"给物流服务,物流服务完成配送,并回复"配送完成"消息。
  • OSO向订单服务发送"订单结束命令"给订单服务,订单服务将订单状态设置为"完成"。
  • OSO清楚一个订单处理Saga的具体流程,并在出现异常时向相关服务发送补偿命令来回滚整个分布式事务。

实现协调中心的一个比较好的方式是使用状态机(Sate Machine)

该模式下分布式事务的回滚

该模式下的回滚流程如下:

image.png

  1. 库存服务回复OSO一个"库存不足"消息。
  2. OSO意识到该分布式事务失败了,触发回滚流程:
  3. OSO发送"退款命令"给支付服务,支付服务完成退款并回复"退款成功"消息。
  4. OSO向订单服务发送"将订单状态改为失败命令",订单服务将订单状态更新为"失败"。

基于命令方式的优缺点

优点:

  1. 避免了业务方之间的环形依赖。
  2. 将分布式事务的管理交由协调中心管理,协调中心对整个逻辑非常清楚。
  3. 减少了业务参与方的复杂度。这些业务参与方不再需要监听不同的消息,只是需要响应命令并回复消息。
  4. 测试更容易(分布式事务逻辑存在于协调中心,而不是分散在各业务方)。
  5. 回滚也更容易。

缺点:

  1. 一个可能的缺点就是需要维护协调中心,而这个协调中心并不属于任何业务方。
Saga模式建议

1,给每一个分布式事务创建一个唯一的Tx id。这个唯一的Tx id可以用来在各个业务参与方沟通时精确定位哪一笔分布式事务。 2,对于基于命令的方式,在命令中携带回复地址。这种方式可以让服务同时响应多个协调中心请求。 3,幂等性。幂等性能够增加系统的容错性,让各个业务参与方服务提供幂等性操作,能够在遇到异常情况下进行重试。 4,尽量在命令或者消息中携带下游处理需要的业务数据,避免下游处理时需要调用消息产生方接口获取更多数据。减少系统之间的相互依赖。

本地消息表

本地消息表核心思路是将分布式事务拆分成本地事务进行处理。

角色:

  • 事务主动方
  • 事务被动方

通过在事务主动发起方额外新建事务消息表,事务发起方处理业务和记录事务消息在本地事务中完成,轮询事务消息表的数据发送事务消息,事务被动方基于消息中间件消费事务消息表中的事务。

这样可以避免以下两种情况导致的数据不一致性:

image.png

场景描述

  • 上游服务:订单服务(Order Service),负责修改订单状态。
  • 下游服务:积分服务(Points Service),负责给用户增加积分。
  • 中间件:消息队列(MQ,如 RabbitMQ/RocketMQ)。
  • 目标:保证只要订单支付变成了“成功”,用户最终一定能收到积分,且不会多发。

数据库包括:订单表,本地消息表,积分表。

执行流程详解:

  1. 第一阶段:上游业务操作(利用本地事务的原子性)

    当用户支付成功时,订单服务执行以下逻辑,重点是将这两步包裹在同一个数据库事务(Transaction)中

    -- 开启事务
    BEGIN;
    
    -- 1. 更新订单状态为已支付
    UPDATE t_order SET status = 'PAID' WHERE id = 101;
    
    -- 2. 插入一条待发送的消息到本地消息表
    -- 注意:此时消息并没有发给 MQ,只是存在数据库里
    INSERT INTO t_local_message (msg_id, content, status) 
    VALUES ('uuid-123', '{"userId":888, "points":10}', 0);
    
    -- 提交事务
    COMMIT;
    
    • 如果 Commit 成功:订单变了,消息也肯定在表里了。
    • 如果 Commit 失败/回滚:订单没变,消息表里也没数据。MQ 也不会收到消息。这就保证了强一致性
  2. 第二阶段:异步发送消息(依靠轮询/定时任务)

    订单服务中有一个独立的后台线程(定时任务) ,专门处理 t_local_message 表:

    1. 查询:定时(比如每 5 秒)扫描 t_local_message 表中状态为 0 (待发送) 的记录。
    2. 发送:将这些记录的内容发送到消息队列(MQ)。
    3. 更新状态
      • 如果发送成功(MQ 返回 Ack),更新本地表状态为 1 (已发送)
      • 如果发送失败,更新 retry_count + 1,等待下次扫描重试。
  3. 第三阶段:下游消费(依靠幂等性)

积分服务监听 MQ:

  1. 接收消息:收到 {"userId":888, "points":10}
  2. 幂等检查非常重要!
    • 检查该 msg_id (uuid-123) 是否已经处理过。
    • 如果处理过,直接返回成功(丢弃消息)。
    • 如果没处理过,往下走。
  3. 执行业务:给用户 888 增加 10 积分。
  4. 确认消息:向 MQ 发送 ACK,表示消费成功。

异常情况分析:

  • 情况 A:第一阶段数据库挂了
    • 结果:事务回滚,订单没成功,消息也没写入。用户重试支付即可,数据一致。
  • 情况 B:消息发送给 MQ 时网络断了
    • 结果:第一阶段已提交,订单已成功。后台定时任务会发现这条消息还是 0 (待发送),它会不断重试发送,直到 MQ 恢复。保证了消息最终一定能发出去
  • 情况 C:下游积分服务挂了
    • 结果:MQ 会保留消息。等积分服务重启后,继续消费。
  • 情况 D:消息重复发送了(定时任务刚发完,更新数据库失败,下次又发了一次)
    • 结果:下游积分服务必须实现幂等性,通过唯一 msg_id 判断,发现处理过就不再加积分。

MQ事务方案

基于 MQ 的分布式事务方案其实是对本地消息表的封装,将本地消息表基于 MQ 内部,其他方面的协议基本与本地消息表一致。

MQ事务方案整体流程和本地消息表的流程很相似,如下图:

image.png

从上图可以看出和本地消息表方案唯一不同就是将本地消息表存在了MQ内部,而不是业务数据库中。

那么MQ内部的处理尤为重要,下面主要基于 RocketMQ 4.3 之后的版本介绍 MQ 的分布式事务方案。

在本地消息表方案中,保证事务主动方发写业务表数据和写消息表数据的一致性是基于数据库事务,RocketMQ 的事务消息相对于普通 MQ提供了 2PC 的提交接口,方案如下:

正常情况:事务主动方发消息

image.png

这种情况下,事务主动方服务正常,没有发生故障,发消息流程如下:

  • 发送方向 MQ 服务端(MQ Server)发送 half 消息。
  • MQ Server 将消息持久化成功之后,向发送方 ack 确认消息已经发送成功。
  • 发送方开始执行本地事务逻辑。
  • 发送方根据本地事务执行结果向 MQ Server 提交二次确认(commit 或是 rollback)。
  • MQ Server 收到 commit 状态则将半消息标记为可投递,订阅方最终将收到该消息;MQ Server 收到 rollback 状态则删除半消息,订阅方将不会接受该消息。

异常情况:事务主动方消息恢复

image.png

在断网或者应用重启等异常情况下,上图中提交的二次确认超时未到达 MQ Server,此时处理逻辑如下:

  • MQ Server 对该消息发起消息回查。
  • 发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
  • 发送方根据检查得到的本地事务的最终状态再次提交二次确认。
  • MQ Server基于 commit/rollback 对消息进行投递或者删除。

优点

相比本地消息表方案,MQ 事务方案优点是:

  • 消息数据独立存储 ,降低业务系统与消息系统之间的耦合。
  • 吞吐量大于使用本地消息表方案。

缺点

  • 一次消息发送需要两次网络请求(half 消息 + commit/rollback 消息) 。
  • 业务处理服务需要实现消息状态回查接口。

最大努力通知

最大努力通知也称为定期校对,是对MQ事务方案的进一步优化。它在事务主动方增加了消息校对的接口,如果事务被动方没有接收到消息,此时可以调用事务主动方提供的消息校对的接口主动获取。

最大努力通知的整体流程如下图:

image.png

在可靠消息事务中,事务主动方需要将消息发送出去,并且消息接收方成功接收,这种可靠性发送是由事务主动方保证的.

但是最大努力通知,事务主动方尽最大努力(重试,轮询....)将事务发送给事务接收方,但是仍然存在消息接收不到,此时需要事务被动方主动调用事务主动方的消息校对接口查询业务消息并消费,这种通知的可靠性是由事务被动方保证的。

最大努力通知适用于业务通知类型,例如微信交易的结果,就是通过最大努力通知方式通知各个商户,既有回调通知,也有交易查询接口。