0%

分布式事务解决方案

为什么需要分布式事务

相对于 本地事务(单个数据源上进行数据访问和更新) 而言,分布式事务是跨越多个数据源进行数据的访问和更新,即,事务的发起者、参与者、数据资源服务器以及事务管理器分别位于分布式系统的不同节点上。
分布式事务的目的是保障分布式环境下的数据一致性。

CAP

说到一致性,我们都知道,分布式系统有一个著名的CAP定理,即:一致性、可用性和分区容忍性无法同时满足,最多只能实现两点。

Consistency

一致性 - 所有节点在同一时间的数据完全一致。
按照CAP定理的取舍,有三类一致性模型:

  • 强一致性
    更新完成后,任意时刻读取到的所有节点中的数据都是一致的。一般采用加锁同步的方式实现,对性能影响较大,会牺牲可用性。
  • 弱一致性
    更新完成后,系统不承诺可以立即读到最新写入的值,也不承诺具体多久之后可以读到。
  • 最终一致性
    弱一致性的一种形式,更新完成后,系统不承诺可以立即返回最新写入的值,但是保证最终会返回上一次更新操作的值。
    注:CAP的C特指强一致性,弱一致性和最终一致性是为了弥补AP之下的数据一致性。

Availability

可用性 - 即,服务可用,能正常的接收请求并及时给予响应,不出现操作失败或访问超时等影响体验的情况。
注:这里的可用性不是通常所说的高可用,而是指服务器虽然在线,但却不能对外提供写入服务。

Partition Tolerance

分区容忍性 - 在遇到部分节点或网络分区故障时,仍能对外提供满足一致性或可用性的服务。
网络分区是指独立的机器无法在期望的时间内完成数据交换,这不仅仅指网络断开,还可能有其他情况产生网络分区,比如宕机、网络延时等。

BASE

BASE由eBay架构师提出,是对CAP中一致性和可用性权衡的结果(对AP的扩展),核心是:无法做到强一致性,按需采用适当的方式来达到最终一致性

  • BA - Basically Available,基本可用
    指分布式系统在出现故障时,保证核心可用,允许损失部分可用性。eg.
    性能上降级 - 为了避免高并发导致的系统资源耗尽不可用,可以延长部分请求响应(排队 或 分流降量,整体延长);
    功能上降级 - 电商平台促销时,为了保证购物系统的稳定性,部分消费者可能会被引导到一个降级的页面。
  • S - Soft state,软状态
    相对 硬状态,指允许系统中的数据存在中间状态,并认为该中间状态不会影响系统整体可用性,即允许系统不同节点的数据副本之间进行同步的过程存在时延。
    eg. 使用支付宝的时候,会出现“支付中”、“数据同步中”等状态,这时候就叫做软状态,但最终会显示支付成功。
  • E - Eventually consistent,最终一致性
    最终一致性强调的是系统中的数据副本,在经过一段时间的同步后,最终能达到一致的状态。
    eg. 订单的“支付中”状态,最终会变为“支付成功”或“支付失败”,使订单状态与实际交易结果达成一致,但需要一定时间的延迟、等待。

常见分布式事务解决方案

在实际分布式场景中,不同业务单元和组件对数据一致性的要求是不同的,因此在具体分布式架构设计过程中,往往会把 ACID特性 和 BASE理论 结合考虑
从类型上分 刚性事务柔性事务

  1. 刚性事务
    通常无业务改造,强一致性,原生支持回滚 / 隔离性,低并发,适合短事务,eg. XA规范(2PC、JTA、JTS)、3PC,满足ACID特性,属于CP策略。
  2. 柔性事务
    有业务改造,最终一致性,实现补偿接口,实现资源锁定接口,高并发,适合长事务,eg. TCC / FMT、Saga、基于消息的分布式事务,基于BASE理论,属于AP策略。

XA 规范

XA,全称 eXtended Architecture,是 X/Open 组织制定的分布式事务规范,目的是保证分布式事务的ACID特性。
X/Open DTP(Distributed Transaction Process),一种分布式事务处理模型:
alt

  • RM(Resource Manager):资源管理器,提供数据资源的操作、管理接口,保证数据的一致性和完整性。eg. DBMS(数据库管理系统)、FS(文件系统)、MQ(消息中间件)等。
  • TM(Transaction Manager):事务管理器,协调跨库事务关联的所有 RM 的行为。
  • AP(Application Program):应用程序,按照业务规则调用 RM 接口来完成对业务模型数据的变更,当数据的变更涉及多个 RM 且要保证事务时,AP 就会通过 TM 来定义事务的边界,TM 负责协调参与事务的各个 RM 一同完成一个全局事务。
  • CRM(Communication Resource Manager):通信资源管理器,主要用来进行跨服务的事务的传播。

XA定义了DTP中RM和TM的接口规范,实现了DTP环境中的两阶段提交。
XA 规范中分布式事务是构建在 RM 本地事务(此时本地事务被看作分支事务)的基础上,TM 负责协调这些分支事务要么都成功提交、要么都回滚。
XA 规范把分布式事务处理过程划分为两个阶段,所以又叫两阶段提交协议(Two Phrase Commit)

2PC

alt
其中,ax_* 由TM提供给RM调用,实现RM的动态注册;xa_* 由RM提供给TM调用,实现2PC中的事务提交/回滚。

过程简述

  • AP向TM发起全局事务请求,TM响应并生成全局事务标识(XID)。
  • 全局事务涉及的RM通过 ax_reg 注册到TM,事务结束后通过 ax_unreg 进行注销。
    此为动态注册;若为静态注册,可以在全局事务开始之前进行 ax_reg,但实际用的更多的是动态注册(据说能让TM更专注于调用全局事务相关的RM,有一定的性能优势)。
  • TM调用 xa_open 与RM建立连接:初始化RM,供AP使用。
  • TM通过 xa_start 和 xa_end 来标识分支事务的开始和结束,并将XID与RM进行关联/取消关联,记录事务开始日志,而具体的分支事务操作由AP直接调用RM进行。
  • TM通过 xa_prepare 通知每个RM进行预提交。
    实际上是询问每个RM是否可以执行提交准备操作,RM收到指令后评估自身状态,尝试执行本地事务的预备操作,eg. 资源加锁、执行操作、记录日志等,但并不提交事务,等待TM后续指令。
  • TM通过 xa_complete 获取每个RM的预提交结果,记录事务准备完成日志。
  • TM根据每个RM的预提交结果发起事务的提交或回滚。
    若每个RM均成功,则记录事务 commit 日志,并调用 xa_commit 通知RM进行事务提交;RM收到指令后,提交事务、释放资源,并响应“提交完成”。
    否有RM返回失败或超时未应答,则记录事务 abort 日志,并调用 xa_rollback 通知RM进行事务回滚;RM收到指令后,回滚事务、释放资源,并响应“回滚完成”。
    待TM收到所有RM的响应,记录事务结束日志。
  • TM通过 xa_close 关闭与RM的连接。

2PC的核心是通过提交分阶段和记日志的方式,记录下事务提交所处的阶段状态,并通过日志对分支事务进行提交或回滚的控制。

优化措施

针对部分场景,XA 规范还定义如下优化措施:

  • 如果 TM 发现整个事务只涉及一个RM,就会将整个过程退化为一阶段提交(跳过预备阶段,直接执行提交/回滚)。
  • 如果 RM 收到 AP 的数据操作是只读操作,可以在阶段 1 就将本地事务完成并告知 TM 不再参与阶段 2。
    会有脏读的风险,比如此RM在返回数据给AP之前就释放了锁并被其他事务改变了数据。

故障恢复处理

由于 TM 与 RM 频繁交互,而这期间,宕机和网络超时都有可能发生,针对这些异常场景的故障恢复处理,非常考验遵循 XA 规范的具体实现:

  • TM 在阶段 1 中询问 RM 前宕机,恢复后无需做任何操作。
  • TM 在阶段 1 中询问 RM 后宕机,可能只有部分 RM 收到了阶段 1 的请求,因此需要向 RM 发起回滚指令。
  • TM 在阶段 1 中询问 RM 完毕,但是在获取结果并记录准备完成日志时宕机,因不清楚宕机前的事务协商结果,因此恢复后需要向 RM 发起回滚指令。
  • TM 在阶段 1 中记录完毕事务准备完成日志后宕机,恢复后可以根据日志发起提交或回滚的指令。
  • TM 在阶段 2 中记录 commit/abort 日志前宕机,恢复后可以根据日志发起提交或回滚的指令。
  • TM 在阶段 2 中记录事务结束日志前宕机,恢复后可以根据日志发起提交或回滚的指令。
  • TM 在阶段 2 中记录事务结束日志后宕机,恢复后无需做任何操作。
  • 阶段 1 中,RM 有超时情况时,TM 按失败处理,给所有 RM 发送回滚指令。
  • 阶段 2 中,RM 有超时情况时,TM 需要对超时的 RM 持续重复发送指令。

注意:RM 的提交和回滚操作需支持幂等。

缺点

XA存在如下问题:

  • 单点问题
    TM 是单点,存在单点故障风险,若 TM 在阶段1之后挂掉,会导致参与的 RM 收不到阶段 2 的请求而长久持有资源的锁。
  • 性能问题
    RM中的资源一直处于同步阻塞,直到收到提交/回滚指令才会释放,这非常影响性能。
    尤其在微服务架构下,若参与的 RM 过多,阻塞时间更长,系统的吞吐能力会变得很差。
  • 数据不一致
    阶段 2 中,若 TM 发出事务提交(或回滚)通知,但由于网络问题或自身宕机,该通知仅被一部分RM收到并执行了操作,那么就会导致数据不一致。

JTA

JTA(Java Transaction API),是符合X/Open DTP的一个编程模型,事务管理和资源管理器支架也用了XA协议。

3PC

针对 XA 两阶段提交中的问题,三阶段提交在2PC的基础上增加了 canCommit 阶段,并且引入了 RM 超时机制(RM长久没有收到TM的 commit 请求,会自动进行本地事务 commit)这样有效解决了TM单点故障的问题。

过程简述

alt
2PC 的预备阶段中,在 preCommit 之前插入 canCommit 阶段,大致过程如下:

  • TM 向 RM 发送 canCommit 请求,RM 如果可以提交就返回 yes 并进入预备状态,否则返回 no。
  • TM 根据 RM 返回的 canCommit 结果进行协调,若全部yes,则进行基于事务的 preCommit 操作,若存在no,则发出abort请求,RM接收并中断事务。
  • TM 发出的 preCommit 请求和响应结果判断及之后的 commit 操作与 2PC 一致。

完成 preCommit 后,无论是 TM 宕机或网络出现问题,都会导致 RM 无法接收到 TM 发出的 commit 请求或 abort 请求。
此时,RM 都会在等待超时之后,继续执行事务提交。
这样可以避免 RM 在准备之后,由于 TM 宕机而导致 RM 较长时间阻塞资源,无法操作的问题。

为什么 RM 等待超时后直接提交事务,而不是回滚呢?
因为 TM 发出了 preCommit 请求,说明 canCommit 阶段,所有 RM 返回的都是 yes,即,RM 可以提交。
所以,默认的直接提交是在 RM 已经向 TM 表明自身具备提交条件的前提下进行的

缺点

数据不一致问题依然存在:当 RM 收到 preCommit 请求后等待 commite 指令时,此时若 TM 的协调结果是 abort,并且由于自身宕机或网络波动而造成部分 RM 无法收到,会导致这部分 RM 在等待超时后继续提交事务,造成数据不一致。
所以,性能问题和数据不一致仍然没有根本解决,实际应用很少。

TCC

TCC(Try-Confirm-Cancel的简称)是一种补偿型事务,实现最终一致性,核心思想是:通过对资源的预留(提供中间态,eg. 账户状态、冻结金额等),尽早释放对资源的加锁,如果事务可以提交,则完成对预留资源的确认,如果事务要回滚,则释放预留的资源
TCC 也是两阶段提交,但不同于XA的两阶段提交,它是在服务层面上实现的,要求每个服务都实现 Try/Confirm/Cancel 三个接口,资源锁定由服务自己控制。

过程简述

  • Try 阶段
    尝试执行业务,完成所有业务检查(一致性),预留必要的业务资源(准隔离性)。
  • Confirm/Cancel 阶段
    如果 Try 阶段所有业务资源都预留成功,则执行 Confirm 操作,否则执行 Cancel操作。
    • Confirm:不再做业务检查,只使用 Try 阶段预留的业务资源执行业务操作,若失败则重试。
    • Cancel:取消执行业务操作,释放 Try 阶段预留的业务资源,若失败则重试。

TCC事务模型:
alt
主服务需记录全局事务和各个分支事务的状态和信息;子服务需记录分支事务的状态。
执行过程中,任意环节都可能发生异常情况(eg. 宕机、网络中断等),此时需根据全局事务日志和分支事务日志,继续进行未完成的分支事务提交或回滚,使全局事务内所有参与者达到最终一致的状态,实现事务的原子性。
值得注意的是,为了解决网络波动引起的异常情况,在实现上要遵循三个策略:

  • 允许空回滚
    若 Try 阶段发生异常,可能部分参与者没有收到 Try 请求从而触发整个事务的 Cancel 操作;Try 失败或者没有执行 Try 操作的参与方收到 Cancel 请求时,要进行空回滚操作。
  • 保持幂等性
    若 Confirm/Cancel 阶段发生异常(比如超时或网络波动而导致事务管理器没有收到子服务的响应、子服务自身异常导致 Confirm/Cancel 操作失败),则事务管理器会重新调用参与者的 Confirm/Cancel 方法,因此,Confirm/Cancel 操作必须支持幂等性。
  • 防止资源悬挂
    网络异常可能造成 Try 和 Confirm/Cancel 无法严格按顺序执行,比如,出现 Try 请求比 Cancel 请求更晚到达的情况,Cancel 会执行空回滚来确保事务的正确性,但此时 Try 方法也不可以再被执行。

举个栗子

用个简单的订单支付作为示例来说明下 - 假设小明账户余额有120元,已下单10本故事书,共计100元,正准备支付,那么在支付完订单后,会有 更新订单状态扣减账户余额减少图书库存增加账户积分 这样几个步骤。
若这几个步骤分别隶属于 _订单服务支付服务库存服务会员服务_,则对应的 Try/Confirm/Cancel 逻辑如下:

订单服务 支付服务 库存服务 会员服务
Try 订单状态设置为“支付中” 冻结账户中的100元,但不实际扣减账户余额 冻结库存中的10本书,但不实际减少库存 预增加积分,但不实际加到账户积分里
Confirm 订单状态更新为“已完成” 扣减余额并清除冻结记录 减少库存并清除冻结记录 增加账户积分并清除预增加记录
Cancel 订单状态更新为“已取消” 清除冻结记录 清除冻结记录 清除预增加记录

注:其他并发事务在用到 账户余额 / 库存 时需注意剔除 冻结金额 / 冻结数量。

优缺点

相比 XA 的 2PC,TCC 可以在服务层面实现更灵活的资源锁粒度,降低阻塞,避免了长事务引起的低性能风险。
不足主要体现在对业务服务的侵入性强,实现成本较高,且事务管理器要记录完整全局事务日志,以便故障恢复,也会损耗一定的性能。

Saga

Saga,寓意“一连串的事件”,也是一种补偿型事务,核心思想是:将长事务拆分为多个本地短事务,由 Saga 事务协调器协调,若全部正常则事务完成,否则一个步骤失败,则根据相反顺序调用补偿事务进行恢复
不同于 TCC,Saga 没有 Try 阶段,每个步骤都会将其更改持久化提交,所以,必须编写补偿事务来显式撤销之前步骤所做的更改。
从概念上讲,每个步骤 Ti 都有一个相应的补偿事务 Ci,它可以撤销 Ti 的影响;Saga 必须以相反的顺序执行每个 Ci。
eg. 以 Create Order Saga 为例(源自《微服务架构设计模式》一书),每个步骤的补偿事务如下:

步骤 服务 事务 补偿事务
1 Order Service createOrder() rejectOrder()
2 Consumer Service verifyConsumerDetails() -
3 Kitchen Service createTicket() rejectTicket()
4 Accounting Service authorizeCreditCard() -
5 Kitchen Service approveTicket() -
6 Order Service approveOrder() -
  • Order Service - 创建一个处于APPROVAL_PENDING状态的Order。
  • Consumer Service - 验证当前订单中的消费者可以下单。
  • Kitchen Service - 验证订单内容,并创建一个后厨工单Ticket,状态为CREATE_PENDING。
  • Accounting Service - 对消费者提供的信用卡做授权操作。
  • Kitchen Service - 把后厨工单Ticket的状态改为AWAITING_ACCEPTANCE。
  • Order Service - 把Order的状态改为APPROVED。

并非所有步骤都需要补偿事务:只读步骤,eg. verifyConsumerDetails() 不需要补偿事务;也不需要考虑为诸如 authorizeCreditCard() 之类的步骤设计补偿事务,因为这些步骤之后的操作总是会成功:

  • 1 ~ 3 步骤称为可补偿性事务,因为它们后面跟着的步骤可能失败。
  • 4 步骤称为关键性事务,因为它后面跟着不可能失败的步骤(即,后面跟着的都是 可重复性事务)。
  • 5 ~ 6 步骤称为可重复性事务,因为它们总是会成功(幂等性,可失败重试直至成功,所以直观地认为此类事务总是成功)。

假设下单过程中“消费者信用卡授权失败”,则 Create Order Saga 的事务执行步骤如下:

  1. Order Service:创建一个处于APPROVAL_PENDING状态的Order。
  2. Consumer Service:验证消费者是否可以下单。
  3. Kitchen Service:验证订单内容,并创建一个后厨工单Ticket,状态为CREATE_PENDING。
  4. Accounting Service:授权消费者的信用卡,但失败了。
  5. Kitchen Service:将后厨工单Ticket的状态更改为CREATE_REJECTED。
  6. Order Service:将Order的状态更改为REJECTED。

其中,5 和 6 步骤分别是用于取消由 Kitchen Service 和 Order Service 进行的更新的补偿事务。

协调模式

Saga 事务协调器按实现逻辑划分为:协同式 和 编排式。

  • 协同式 - 把 Saga 的决策和执行顺序逻辑分布在 Saga 的每一个参与方中,它们通过交换事件的方式来进行沟通。
  • 编排式 - 把 Saga 的决策和执行顺序逻辑集中在一个 Saga 编排器类中,Saga 编排器发出命令式消息给各个 Saga 参与方,指示这些参与方服务完成具体操作(本地事务),即,集中控制器告诉 Saga 参与方要执行的操作。

协同式 Saga

参与方订阅彼此的事件并做出相应的响应。

可靠的事件通信
  1. 首先,确保参与方将更新本地数据库和发布事件作为数据库事务的一部分,数据库更新和事件发布必须是原子的。
    解决方案 - 事务性消息。
  2. 其次,确保参与方必须能够将接收到的每个事件映射到自己的数据上。
    eg. 当 Order Service 收到 CreditCardAuthorized 事件时,它必须能够查找相应的 Order。
    解决方案 - 让参与方发布包含相关性ID的事件,该相关性ID使其他参与方能够执行数据的操作。eg. Create Order Saga 的参与方可以使用 orderId 作为从一个参与方传递到下一个参与方的相关性ID。
好处和弊端
  • 好处:简单、松耦合。
  • 弊端:难理解(逻辑分散,很难理解整体工作流程)、服务之间的循环依赖关系(eg. Order Service -> Accounting Service -> Order Service,虽然不一定是个问题,但循环依赖被认为是一种不好的设计风格)、紧耦合的风险(每个参与方都需要订阅所有影响它们的事件)。

协同式可以很好的用于简单的 Saga,但对于复杂场景,通常更倾向于使用编排式来应对。

编排式 Saga

定一个编排器类:告诉 Saga 参与方该做什么事情;编排器使用 命令/异步响应方式 与参与方服务通信。
注:每个步骤都包括一个更新数据库和发布消息的服务,所以服务必须使用事务性消息,以便自动更新数据库并发布消息。
目前,业界有两类实现方式,一种是基于业务逻辑层Proxy设计(基于AOP实现),比如华为的 ServiceComb;另一种是状态机实现的机制,比如阿里的 Seata 的 Saga 模式。

状态机模式

一个 Saga 可能有很多场景,例如 Create Order Saga 有4个场景,除了一切正常外,由于 Consumer Service、Kitchen Service 或 Accounting Service 的失败,Saga 可能会失败。因此,将Saga建模为状态机非常有用,因为它描述了所有可能的场景。
状态机由一组状态和一组由事件触发的状态之间的转换组成:

  • 每个转换都可以有一个动作,对 Saga 来说动作就是对某个参与方的调用。
  • 状态之间的转换由 Saga 参与方执行的本地事务完成触发。
  • 当前状态和本地事务的特定结果决定了状态转换以及执行的动作。
  • 对状态机也有有效的测试策略。

因此,使用状态机模型可以更轻松地设计、实现和测试Saga。
eg. Create Order Saga 的状态机模型:初始操作是将 Verify Consumer 命令发送到 Consumer Service;
Consumer Service 的响应会触发下一次状态转换(如果消费者被成功验证,则 Saga 会创建 Ticket 并转换为 Creating Ticket 状态;若失败,则拒绝 Order 并转换为Order Rejected 状态)。
状态机的状态转换由 Saga 参与方的响应驱动。

AOP模式

Apache ServiceComb Saga 基于 Spring 注解和 AOP 切面。
有兴趣可以自己找相关资料深入了解。
https://gitee.com/servicecomb/ServiceComb-Saga
http://vlambda.com/wz_7iJr8jOihAi.html

好处和弊端
  • 好处
    • 更简单的依赖关系:不会引入循环依赖关系,只有编排器依赖参与方。
    • 较少的耦合:每个服务实现供编排器调用的API,不需要知道参与方发布的事件。
    • 改善关注点隔离,简化业务逻辑:协调逻辑本地化在编排器中;领域对象更简单,并且不需要了解它们参与的 Saga。
  • 弊端:在编排器中存在过多业务逻辑的风险。

注意事项

和 TCC 一样,Saga 也是依靠业务改造来实现,所以在设计上需遵循三个策略:

  • 允许空补偿:网络异常导致事务的参与方只收到了补偿操作指令,因为没有执行过正常操作,因此要进行空补偿。
  • 保持幂等性:事务的正向操作和补偿操作都可能被重复触发(失败重试),因此要保证操作的幂等性。
  • 防止资源悬挂:原因是网络异常导致事务的正向操作指令晚于补偿操作指令到达,则要丢弃本次正常操作,否则会出现资源悬挂问题。

优缺点

Saga 可以实现最终一致性,但不保证事务的隔离,本地事务提交后变更就对其他事务可见了;若全局事务期间,其他事务更改了已经提交的数据,可能会导致补偿操作失败,业务设计上需要考虑这种场景并进行规避。
Saga 不会对资源长时间加锁,不影响性能,系统吞吐量高。
相对 TCC,Saga 对业务入侵低,只需要提供补偿事务即可;而 TCC 需要对业务进行全局性的流程改造。
Saga 非常适合于长事务的场景,所以很适合微服务架构的场景。

基于消息的分布式事务

基于消息的分布式事务核心思想是:通过消息队列来通知参与方的事务执行状态,主要难点在于如何解决本地事务与消息发送的一致性。

本地消息表

该方案最初由eBay提出,核心思想是:事务的发起方维护一个本地消息表,业务执行和本地消息表的执行处在同一个本地事务中

过程简述

alt

  • 本地事务执行中,记录一条“待发送”状态的消息,事务成功,则持久化写入本地消息表。
  • 启动一个定时任务定时扫描本地消息表中状态为“待发送”的记录,并写入 MQ(发布消息)。
  • 子服务订阅相关主题消息,触发执行自身本地事务,若成功,则发布结果消息供主服务消费,否则不响应,待主服务再次发布处理消息,实现重试。
  • 主服务根据子服务响应的结果消息更新本地消息表的数据状态,若没有获取到结果消息,则主服务依旧会发布处理消息,直至闭环成功。

优缺点

可达到最终一致,且基于MQ可以将服务有效解耦,实现异步调用。
但,主服务需维护一张本地消息表,对业务实现有一定的侵入性。

可靠消息最终一致

普通的消息发送无法保障与本地事务执行的一致性,eg. 本地事务已提交,但消息发送可能失败或超时;消息已投递并被消费,但本地事务可能回滚。
要解决这个问题,需要引入事务消息(又称半消息),事务消息和普通消息的区别在于:事务消息发送成功后,处于“待发送”状态,此时不能被订阅者消费,等到事务消息被指定为“可消费”后,订阅者才能监听到此消息。

过程简述

alt

  • MQ收到事务消息后将其持久化存储并置为“待发送”,成功后给发送者响应一个ACK消息。
  • 主服务如果没有收到ACK消息,则取消事务;如果收到了ACK消息,则执行本地事务,完成后给MQ发一个结果通知消息,通知本地事务执行情况。
  • MQ收到通知消息,根据本地事务执行情况进行处理:若成功,则将事务消息改为“可消费”并推送给订阅者(推送失败,则重试);若失败,则删除该事务消息。
  • 子服务收到消息后,执行本地事务,若成功,则给MQ响应一个ACK消息;若失败,则不响应,MQ会持续推送消息,使得子服务事务重试。

本地事务执行完毕后,发给 MQ 的通知消息有可能丢失。所以支持事务消息的 MQ 需要有一个定时扫描逻辑,扫描出仍是“待发送”的消息,并向消息的发送方发起询问,以获取这条事务消息的最终状态并根据结果更新事务消息的状态。
因此事务的发起方需要给 MQ 提供一个事务消息状态查询接口。

优缺点

相比本地消息表,此方案把参与事务的消息放在了MQ,业务入侵低,服务间耦合度小。
但需要多次网络请求来进行消息交换,出异常的风险较高,也要求业务系统提供事务状态回查接口。
与本地消息表一样,实现上不考虑事务的回滚,对于失败情况要一直重试,适用于对最终一致性敏感度较低的业务场景,eg. 系统间的调用,适用场景有限。
注:“系统间的调用”有别于“服务间的调用”,前者偏不同场景的数据传递,而后者偏完整场景,不便割裂。

最大努力通知

相比前两种,“最大努力通知”不要求消息可靠,核心思想是:给参与方的消息在多次重试后不再发送,允许消息丢失(不可靠消息);后续,参与方按需根据主导方提供的查询接口获取丢失的消息数据

过程简述

[原样摘抄网上的一个例子] 假设小明通过联通的网上营业厅为手机充话费,充值方式选择支付宝支付。整个操作的流程如下:

  • 小明选择充值金额 50 元,用支付宝进行支付。
  • 联通网上营业厅创建一个充值订单,状态为“支付中”,并跳转到支付宝的支付页面(此时进入了支付宝的系统中)。
  • 支付宝验明确认小明的支付后,从小明的账户中扣除 50 元,并向联通的账户中增加 50 元。执行完毕后向 MQ 系统发送一条消息,消息的内容标识支付是否成功,消息发送允许失败。
  • 如果消息发送成功,那么支付宝的通知服务会订阅到该消息,并调用联通的接口通知本次支付的结果。如果此时联通的服务挂掉了,导致通知失败了,则会按照 5min、10min、30min、1h、…、24h 等递增的时间间隔,间隔性重复调用联通的接口,直到调用成功或者达到预订的时间窗口上限后,则不再通知。这就是尽最大努力通知的含义。
  • 如果联通服务恢复正常,收到了支付宝的通知,如果支付成功,则给账户充值;如果支付失败,则取消充值。执行完毕后给支付宝通知服务确认响应,确认响应允许失败,支付宝系统会继续重试。所以联通的充值接口需要保持幂等性。
  • 如果联通服务故障时间很久,恢复正常后,已超出支付宝通知服务的时间窗口,则联通扫描“支付中”的订单,主动向支付宝发起请求,核验订单的支付结果。

优缺点

最大努力通知型是最简单的一种柔性事务,适用于最终一致性时间敏感度低的业务,且参与方处理结果不影响主导方。
典型的使用场景,eg. 话费充值通知、银行通知、商户通知等。

小结

不同的分布式事务解决方案适用于不同的业务场景,在损失一定性能的情况下可以保证强一致性,但整体来说,由于分布式环境的复杂性,实际应用中更偏向于选择柔性事务,以保证业务隔离不冲突的情况下达成最终一致性。
当下,国内比较知名的开源分布式事务中间件有阿里的 Seata 和 Apache ServiceComb。
Seata 早期叫 GTS(Global Transaction Service),于2019年开源,改名 Seata,目前支持 TCC 模式、Saga 模式,Seata 对 TCC 模式的支持提供了一种对业务入侵度为 0 的解决方案 - AT(Automatic Transaction)模式
ServiceComb 是一个开源微服务解决方案,最初由华为开发,于2017年11月捐给 Apache,ServiceComb Saga 是其中的一个子项,提供了分布式事务最终一致性解决方案,与 Spring 框架完美融合,简单易上手。

几个问题

ACID中的一致性和CAP中的一致性有什么区别?

ACID的一致性 又称 内部一致性,可以理解为:事务前后数据的完整性;从应用层面上讲,就是从一个一致的状态转换到另一个一致的状态。
eg. AB转账,A扣钱的同时,B必须收到。
CAP的一致性 又称 外部一致性,可以理解为:分布式系统中,访问所有节点的数据副本都是一样的,即,读写数据的一致性
eg. 有A、B、C三个节点,A中写入数据hello world,写完后立即读BC,均能获取到已写数据hello world

AP or CP,为什么?

换个角度,Why not CA or CAP?这其实是两个问题:

  • CA without P,可以吗?
    若不要求P,C和A是可以同时满足的。但放弃P也就意味着放弃了系统的扩展性,这违背了分布式系统设计的初衷。
    分布式之下网络异常不可避免,所以不保障分区容忍性,除非节点间网络不会发生异常(这是不可能的),否则只能单机部署,这样就不是分布式系统了。
  • 为什么CAP不能同时满足?
    在满足P的前提下,若要满足A,则在数据写入某个节点时必须立即返回成功,至于数据是否有成功同步给其他节点,就无法保障,即,无法保障C;
    若要满足C,则在数据写入某个节点时,同步等待写入所有节点(暂停对外提供数据写入服务,直至所有节点同步完成),若有节点写入失败(比如网络异常或节点故障),则整体写入失败,这样虽然保障了C,但容易造成响应超时或数据一直写不成功的情况,牺牲了A。
    大部分情况下,为了保障分布式和服务可用,会牺牲强一致性,选择AP。

什么是事务性消息?

通过消息的异步事务,保证本地事务和消息发送同时执行成功或失败,达到数据的最终一致性。

  • 用关系型数据库表做消息队列
    可靠地发布消息的直接方法是应用事务性发件箱模式,此模式使用数据库表作为临时消息队列,即,作为增删改业务对象的数据库事务的一部分,服务通过将消息插入到临时表来发送消息。
    事务性发件箱模式:通过将事件或消息保存在数据库的OUTBOX表中,将其作为数据库事务的一部分发布。
    将消息从数据库移动到消息代理并对外发送,有两种不同的方法:
    • 通过轮询模式发布事件:轮询获取OUTBOX表中未发布的消息。
    • 使用事务日志拖尾模式发布事件
      拖尾数据库的事务日志文件:事务日志挖掘器可以读取事务日志,把每条跟消息有关的记录发送给消息代理。
      此方法也适用于NoSQL,即,无法生成临时表,直接查询业务实体的情况。
      已有一些现成的开源项目可用,eg. Debezium、LinkedIn Databus、DynamoDB streams、Eventuate Tram…
  • 消息中间件实现事务消息机制
    RocketMQ 和 Kafka 都有对应的事务消息机制,有兴趣可以自己找资料深入了解。
    https://promotion.aliyun.com/ntms/act/mqshiwu1.html
    https://www.confluent.io/blog/transactions-apache-kafka/

参考资料