分布式事务之CP架构、AP架构解决方案

  • 1. 什么是CP架构和AP架构?
  • 2. CP架构解决方案
    • 2.1. DTP和XA
    • 2.2. 二阶段提交(Two-Phase Commit,2PC)
      • 2.2.1. 协议的产生
      • 2.2.2. 什么是二阶段提交协议?
      • 2.2.3. 二阶段提交协议的内容
      • 2.2.4. 二阶段提交协议的缺陷
      • 2.2.5. 2PC使用场景
  • 3. AP架构解决方案
    • 3.1. TCC(补偿型)
    • 3.2. MQ事务消息方案(消息通知型)
    • 3.3. 本地消息表方案(消息通知型)
      • 3.3.1. 简化版本
      • 3.3.2. 解耦合版本
    • 3.4. RabbitMQ的消息确认(消息通知型)
    • 3.5. 消息事务的优缺点
    • 3.6. AT模式

1. 什么是CP架构和AP架构?

在分布式事务之CAP定理一文中,我们了解到一个分布式系统不可能同时满足数据一致性(consistency)、服务可用性(availability)、分区容错性(partition-tolerance)。

现实情况下,我们面对的是一个不可靠的网络、有一定概率宕机的设备,这两个因素都会导致Partition,因而分布式系统实现中 P 是一个必须项,而不是可选项。

对于分布式系统工程实践,CAP理论更合适的描述是:在满足分区容错的前提下,没有算法能同时满足数据一致性和服务可用性。

因此,我们需要在C和A之间进行取舍:

  • CP架构(刚性事务):如果要满足数据的强一致性,就必须在一个服务数据库锁定的同时,对分布式服务下的其他服务数据资源同时锁定。等待全部服务处理完业务,才可以释放资源。此时如果有其他请求想要操作被锁定的资源就会被阻塞,这样就是满足了CP。达到了强一致性和弱可用性。
  • AP架构(柔性事务):如果要满足服务的的强可用性,每个服务就可以各自独立执行本地事务,而无需相互锁定其他服务的资源。在各个服务的事务尚未完全处理完毕时,如果去访问数据库,可能会遇到各个节点数据不一致的情况。然后我们还需要一些措施,使得经过一段时间后,各个节点的数据最终达到一致性。这样就是满足了AP。达到了弱一致性(最终一致性)和强可用性。

2. CP架构解决方案

2.1. DTP和XA

1994 年,X/Open 组织(即现在的 Open Group )定义了分布式事务处理的DTP 模型。

该模型包括这样几个角色:

  • AP(Application Program): 也就是应用程序,可以理解为DTP的程序,也就是我们的微服务;
  • TM(Transaction Manager): 事务管理器,负责协调和管理事务,提供给AP应用程序编程接口以及管理资源管理器。
  • RM(Resource Manager): 资源管理器(这里可以是一个DBMS,或者消息服务器管理系统)应用程序通过资源管理器对资源进行控制,资源必须实现XA定义的接口;
  • CRM(Communication Resource Manager):通信资源管理器,是TM和RM间的通信中间件;

在该模型中,一个分布式事务(全局事务)可以被拆分成许多个本地事务,运行在不同的AP和RM上。每个本地事务的ACID很好实现,但是全局事务必须保证其中包含的每一个本地事务都能同时成功,若有一个本地事务失败,则所有其它事务都必须回滚。但问题是,本地事务处理过程中,并不知道其它事务的运行状态。因此,就需要通过CRM来通知各个本地事务,同步事务执行的状态。

因此,各个本地事务的通信必须有统一的标准,否则不同数据库间就无法通信。XA就是 X/Open DTP中通信中间件与TM间联系的接口规范,定义了用于通知事务开始、提交、终止、回滚等接口,各个数据库厂商都必须实现这些接口。

2.2. 二阶段提交(Two-Phase Commit,2PC)

2.2.1. 协议的产生

要想控制分布式事务,仅仅靠厂商实现XA规范是不够的,所以引入了两阶段提交协议。

2.2.2. 什么是二阶段提交协议?

指事务的提交分为两个阶段:准备阶段和执行阶段。

两阶段提交用来协调参与一个更新中的多个服务器的活动,以防止分布式系统部分失败时产生数据的不一致性。例如,如果一个更新操作要求位于三个不同结点上的记录被改变,且其中只要有一个结点失败,另外两个结点必须检测到这个失败并取消它们所做的改变。

2.2.3. 二阶段提交协议的内容

二阶段提交协议将全局事务拆分为两个阶段来执行:

  • 阶段一:准备阶段,各个本地事务完成本地事务的准备工作。
  • 阶段二:执行阶段,各个本地事务根据上一阶段执行结果,进行提交或回滚。

这个过程中需要一个协调者(coordinator),还有事务的参与者(voter)。

  1. 正常情况

投票阶段:协调组询问各个事务参与者,是否可以执行事务。每个事务参与者执行事务,写入redo和undo日志,然后反馈事务执行成功的信息(agree)

提交阶段:协调组发现每个参与者都可以执行事务(agree),于是向各个事务参与者发出commit指令,各个事务参与者提交事务。

  1. 异常情况

投票阶段:协调组询问各个事务参与者,是否可以执行事务。每个事务参与者执行事务,写入redo和undo日志,然后反馈事务执行结果,但只要有一个参与者返回的是Disagree,则说明执行失败。

提交阶段:协调组发现有一个或多个参与者返回的是Disagree,认为执行失败。于是向各个事务参与者发出abort指令,各个事务参与者回滚事务。

2.2.4. 二阶段提交协议的缺陷

  1. 单点故障问题

    2PC的缺点在于不能处理fail-stop形式的节点failure. 比如下图这种情况。

    假设coordinator和voter3都在Commit这个阶段crash了, 而voter1和voter2没有收到commit消息. 这时候voter1和voter2就陷入了一个困境。因为他们并不能判断现在是两个场景中的哪一种:

    1. 上轮全票通过然后voter3第一个收到了commit的消息并在commit操作之后crash了;
    2. 上轮voter3反对所以干脆没有通过。
  2. 阻塞问题

    • 在准备阶段、提交阶段,每个事务参与者都会锁定本地资源,并等待其它事务的执行结果,阻塞时间较长,资源锁定时间太久,因此执行的效率就比较低了。

    • 可能发生死锁。

  3. 面对二阶段提交的上述缺点,后来又演变出了三阶段提交,但是依然没有完全解决阻塞和资源锁定的问题,而且引入了一些新的问题,因此实际使用的场景较少。

2.2.5. 2PC使用场景

对事务有强一致性要求,对事务执行效率不敏感,并且不希望有太多代码侵入。

3. AP架构解决方案

3.1. TCC(补偿型)

  1. TCC模式可以解决2PC中的资源锁定和阻塞问题,减少资源锁定时间。

  2. 它本质是一种补偿的思路。事务运行过程包括三个方法:

    • Try:资源的检测和预留;
    • Confirm:执行的业务操作提交;要求 Try 成功 Confirm 一定要能成功;
    • Cancel:预留资源释放。

    执行分两个阶段:

    • 准备阶段(try):资源的检测和预留;
    • 执行阶段(confirm/cancel):根据上一步结果,判断下面的执行方法。如果上一步中所有事务参与者都成功,则这里执行confirm。反之,执行cancel;
  3. 粗看似乎与两阶段提交没什么区别,但其实差别很大:

    • try、confirm、cancel都是独立的事务,不受其它参与者的影响,不会阻塞等待它人
    • try、confirm、cancel由程序员在业务层编写,锁粒度有代码控制
  4. 优势

    TCC执行的每一个阶段都会提交本地事务并释放锁,并不需要等待其它事务的执行结果。而如果其它事务执行失败,最后不是回滚,而是执行补偿操作。这样就避免了资源的长期锁定和阻塞等待,执行效率比较高,属于性能比较好的分布式事务方式。

  5. 缺点

    • 代码侵入:需要人为编写代码实现try、confirm、cancel,代码侵入较多
    • 开发成本高:一个业务需要拆分成3个步骤,分别编写业务实现,业务编写比较复杂
    • 安全性考虑:cancel动作如果执行失败,资源就无法释放,需要引入重试机制,而重试可能导致重复执行,还要考虑重试时的幂等问题
  6. 使用场景

    • 对事务有一定的一致性要求(最终一致)
    • 对性能要求较高
    • 开发人员具备较高的编码能力和幂等处理经验

3.2. MQ事务消息方案(消息通知型)

  1. 其基本的设计思想是将远程分布式事务拆分成一系列的本地事务。
  2. 一般分为事务的发起者A和事务的其它参与者B:
    • 事务发起者A执行本地事务;
    • 事务发起者A通过MQ将需要执行的事务信息发送给事务参与者B;
    • 事务参与者B接收到消息后执行本地事务;
  3. 几个注意事项:
    • 事务发起者A必须确保本地事务成功后,消息一定发送成功;
    • MQ必须保证消息正确投递和持久化保存;
    • 事务参与者B必须确保消息最终一定能消费,如果失败需要多次重试;
    • 事务B执行失败,会重试,但不会导致事务A回滚;

3.3. 本地消息表方案(消息通知型)

为了避免MQ事务消息方案中消息发送失败或丢失,我们可以把消息持久化到数据库中。实现时有简化版本和解耦合版本两种方式。

3.3.1. 简化版本

  1. 事务发起者:

    1. 开启本地事务;
    2. 执行事务相关业务;
    3. 发送消息到MQ;
    4. 把消息持久化到数据库,标记为已发送;
    5. 提交本地事务;
  2. 事务接收者:

    1. 接收消息;
    2. 开启本地事务;
    3. 处理事务相关业务;
    4. 修改数据库消息状态为已消费;
    5. 提交本地事务;
  3. 额外的定时任务

    1. 定时扫描表中超时未消费消息,重新发送;
  4. 优点:

    • 与tcc相比,实现方式较为简单,开发成本低;

    缺点:

    • 数据一致性完全依赖于消息服务,因此消息服务必须是可靠的;
    • 需要处理被动业务方的幂等问题;
    • 被动业务失败不会导致主动业务的回滚,而是重试被动的业务;
    • 事务业务与消息发送业务耦合、业务数据与消息表要在一起;

3.3.2. 解耦合版本

  1. 为了解决上述问题,我们会引入一个独立的消息服务,来完成对消息的持久化、发送、确认、失败重试等一系列行为,大概的模型如下:

  2. 一次消息发送的时序图:

  3. 事务发起者A的基本执行步骤:

    1. 开启本地事务
    2. 通知消息服务,准备发送消息(消息服务将消息持久化,标记为准备发送)
    3. 执行本地业务,
      • 执行失败则终止,通知消息服务,取消发送(消息服务修改订单状态)
      • 执行成功则继续,通知消息服务,确认发送(消息服务发送消息、修改订单状态)
    4. 提交本地事务
  4. 消息服务本身提供下面的接口:

    1. 准备发送:把消息持久化到数据库,并标记状态为准备发送
    2. 取消发送:把数据库消息状态修改为取消
    3. 确认发送:把数据库消息状态修改为确认发送。尝试发送消息,成功后修改状态为已发送
    4. 确认消费:消费者已经接收并处理消息,把数据库消息状态修改为已消费
    5. 定时任务:定时扫描数据库中状态为确认发送的消息,然后询问对应的事务发起者,事务业务执行是否成功,结果:
      • 业务执行成功:尝试发送消息,成功后修改状态为已发送
      • 业务执行失败:把数据库消息状态修改为取消
  5. 事务参与者B的基本步骤:

    1. 接收消息
    2. 开启本地事务
    3. 执行业务
    4. 通知消息服务,消息已经接收和处理
    5. 提交事务
  6. 优点:

    • 解除了事务业务与消息相关业务的耦合

    缺点:

    • 实现起来比较复杂

3.4. RabbitMQ的消息确认(消息通知型)

RabbitMQ确保消息不丢失的思路比较奇特,并没有使用传统的本地表,而是利用了消息的确认机制:

  1. 生产者确认机制:确保消息从生产者到达MQ不会有问题
    1. 消息生产者发送消息到RabbitMQ时,可以设置一个异步的监听器,监听来自MQ的ACK;
    2. MQ接收到消息后,会返回一个回执给生产者:
      • 消息到达交换机后路由失败,会返回失败ACK;
      • 消息路由成功,持久化失败,会返回失败ACK;
      • 消息路由成功,持久化成功,会返回成功ACK;
    3. 生产者提前编写好不同回执的处理方式
      • 失败回执:等待一定时间后重新发送;
      • 成功回执:记录日志等行为;
  2. 消费者确认机制:确保消息能够被消费者正确消费
    1. 消费者需要在监听队列的时候指定手动ACK模式;
    2. RabbitMQ把消息投递给消费者后,会等待消费者ACK,接收到ACK后才删除消息,如果没有接收到ACK消息会一直保留在服务端,如果消费者断开连接或异常后,消息会投递给其它消费者;
    3. 消费者处理完消息,提交事务后,手动ACK。如果执行过程中抛出异常,则不会ACK,业务处理失败,等待下一条消息;

经过上面的两种确认机制,可以确保从消息生产者到消费者的消息安全,再结合生产者和消费者两端的本地事务,即可保证一个分布式事务的最终一致性。

3.5. 消息事务的优缺点

  1. 优点:

    • 业务相对简单,不需要编写三个阶段业务;

    • 是多个本地事务的结合,因此资源锁定周期短,性能好;

  2. 缺点:

    • 代码侵入;

    • 依赖于MQ的可靠性;

    • 消息发起者可以回滚,但是消息参与者无法引起事务回滚;

    • 事务时效性差,取决于MQ消息发送是否及时,还有消息参与者的执行情况;

针对事务无法回滚的问题,有人提出说可以再事务参与者执行失败后,再次利用MQ通知消息服务,然后由消息服务通知其他参与者回滚。那么,恭喜你,你利用MQ和自定义的消息服务再次实现了2PC 模型,又造了一个大轮子;

3.6. AT模式

  1. 2019年 1 月份,Seata 开源了 AT 模式。AT 模式是一种无侵入的分布式事务解决方案。可以看做是对TCC或者二阶段提交模型的一种优化,解决了TCC模式中的代码侵入、编码复杂等问题。

  2. 在 AT 模式下,用户只需关注自己的“业务 SQL”,用户的 “业务 SQL” 作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作。可以参考Seata的官方文档。

  3. 基本原理:

    1. 流程图:

    2. 有没有感觉跟TCC的执行很像,都是分两个阶段:

      • 一阶段:执行本地事务,并返回执行结果
      • 二阶段:根据一阶段的结果,判断二阶段做法:提交或回滚

      但AT模式底层做的事情可完全不同,而且第二阶段根本不需要我们编写,全部有Seata自己实现了。也就是说:我们写的代码与本地事务时代码一样,无需手动处理分布式事务。

    3. 在一阶段,Seata 会拦截业务SQL,首先解析 SQL 语义,找到业务SQL要更新的业务数据,在业务数据被更新前,将其保存成before image,然后执行业务SQL更新业务数据,在业务数据更新之后,再将其保存成after image,最后获取全局行锁,提交事务。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。

      这里的before imageafter image类似于数据库的undo和redo日志,但其实是用数据库模拟的。

    4. 二阶段如果是提交的话,因为业务SQL在一阶段已经提交至数据库, 所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。

    5. 二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的业务 SQL,还原业务数据。回滚方式便是用before image还原业务数据;但在还原前要首先要校验脏写,对比数据库当前业务数据和 after image,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。

    6. 不过因为有全局锁机制,所以可以降低出现脏写的概率。

      AT 模式的一阶段、二阶段提交和回滚均由 Seata 框架自动生成,用户只需编写业务SQL,便能轻松接入分布式事务,AT 模式是一种对业务无任何侵入的分布式事务解决方案。

  4. 详细架构和流程

    1. Seata中的几个基本概念:

      • TC(Transaction Coordinator) – 事务协调者

        维护全局和分支事务的状态,驱动全局事务提交或回滚(TM之间的协调者)。

      • TM(Transaction Manager) – 事务管理器

        定义全局事务的范围:开始全局事务、提交或回滚全局事务。

      • RM(Resource Manager) – 资源管理器

        管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

    2. 架构图

      1. TM:业务模块中全局事务的开启者
        • 向TC开启一个全局事务
        • 调用其它微服务
      2. RM:业务模块执行者中,包含RM部分,负责向TC汇报事务执行状态
        • 执行本地事务
        • 向TC注册分支事务,并提交本地事务执行结果
      3. TM:结束对微服务的调用,通知TC,全局事务执行完毕,事务一阶段结束
      4. TC:汇总各个分支事务执行结果,决定分布式事务是提交还是回滚;
      5. TC 通知所有 RM 提交/回滚 资源,事务二阶段结束。
    3. 一阶段:

      1. TM开启全局事务,并向TC声明全局事务,包括全局事务XID信息;
      2. TM所在服务调用其它微服务;
      3. 微服务,主要由RM来执行:
        1. 查询before_image
        2. 执行本地事务;
        3. 查询after_image
        4. 生成undo_log并写入数据库;
        5. 向TC注册分支事务,告知事务执行结果;
        6. 获取全局锁(阻止其它全局事务并发修改当前数据);
        7. 释放本地锁(不影响其它业务对数据的操作);
      4. 待所有业务执行完毕,事务发起者(TM)会尝试向TC提交全局事务;
    4. 二阶段:

      1. TC统计分支事务执行情况,根据结果判断下一步行为:
        • 分支都成功:通知分支事务,提交事务;
        • 有分支执行失败:通知执行成功的分支事务,回滚数据;
      2. 分支事务的RM:
        • 提交事务:直接清空before_imageafter_image信息,释放全局锁;
        • 回滚事务:
          • 校验after_image,判断是否有脏写;
          • 如果没有脏写,回滚数据到before_image,清除before_imageafter_image
          • 如果有脏写,请求人工介入;