分布式事务

83 阅读17分钟

1. 什么是事务

下面是文心一言的标准答案:

数据库事务(Database Transaction)是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务的目的是确保数据库的完整性。

1.1. 事务的ACID特性

事务的主要特性(ACID)包括:

  1. 原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么全部不执行。
  2. 一致性(Consistency):事务应确保数据库从一个一致性状态转变为另一个一致性状态。
  3. 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务。
  4. 持久性(Durability):一旦事务提交,则其结果就是永久性的,即使系统发生故障也不应该对其结果有任何影响。 通过使用事务,可以确保数据库的完整性和可靠性,避免因单个操作的失败而导致的数据不一致或其他问题。同时,事务还可以提高数据库的性能,因为多个操作可以在一个事务中一起执行,减少了与数据库的交互次数。 在实际应用中,如银行转账、订单处理等场景,都需要使用事务来确保数据的完整性和准确性。

2. 分布式事务

2.1. 什么情况下会遇上分布式事务场景

当事务的影响范围局限在一个关系型数据库范围内时,很多时候上述四项性质是能够水到渠成地得到实现的,但是倘若事务涉及修改的对象是跨数据库甚至跨服务跨存储组件时,这个问题就开始变得复杂也就出现了分布式事务所要解决的场景。

举例:

假设我们在维护一个电商后台系统,每当在处理一笔来自用户创建订单的请求时,需要执行两步操作:

• 从账户系统中,扣减用户的账户余额

• 从库存系统中,扣减商品的剩余库存

从业务流程上来说,这个流程需要保证具备事务的原子性,即两个操作需要能够一气呵成地完成执行,要么同时成功,要么同时失败,不能够出现数据状态不一致的问题,比如发生从用户账户扣除了金额但商品库存却扣减失败的问题.

然而从技术流程上来讲,两个步骤是相对独立的两个操作,底层涉及到的存储介质也是相互独立的,因此无法基于本地事务的实现方式.

因此,这本质上就是我们今天所要谈及的分布式事务问题。

2.2. 分布式系统的理论基础

2.2.1. CAP与BASE理论

CAP理论

CAP理论,由Eric Brewer在2000年提出,指的是在一个分布式系统中,Consistency(一致性)、Availability(可用性)、Partition Tolerance(分区容错性)这三个要素不能同时成立。CAP理论是分布式架构中的一种设计思想模型,用于指导在分布式系统设计中如何权衡这三个要素。

  • 一致性(Consistency):确保所有节点上的数据在同一时间保持一致。即所有主副节点在同一时间数据完全一致,保证数据的同步性。
  • 可用性(Availability):无论请求成功还是失败,每个请求都是有效的,并且系统能在可控时间内返回正确的响应。即服务始终可用,用户随时访问都能得到响应。
  • 分区容错性(Partition Tolerance):在分布式系统中,即使某个节点或网络分区发生故障,系统仍然能够继续提供服务。即系统能够容忍网络分区的情况,保证系统的稳健性。

在实际应用中,由于网络延迟、节点故障等原因,很难同时满足这三个要素。因此,在设计分布式系统时,通常需要根据实际需求在这三个要素之间进行权衡和取舍。

CAP理论在多个领域都有广泛的应用,如区块链、分布式文件系统、云计算和大数据等。它帮助这些系统提供更高水平的可用性和可靠性,并有效地解决网络分区问题。

BASE理论

BASE理论是eBay的架构师Dan Pritchett于2008年在CAP理论(那时CAP理论还未被证明正确性,仅是一个猜想)的基础上提出的,是对CAP理论中AP方案的一个补充。BASE理论代表基本可用(Basically Available)、软状态(Soft state)、最终一致(Eventually consistent),是一组用于设计分布式系统的指南。

BASE理论的核心思想是即使无法做到强一致性,也可以通过牺牲强一致性来获得高可用性,达到最终一致性。

具体来说,BASE理论强调分布式系统应始终可用于处理请求,即使这意味着放宽一致性保证。在软状态方面,它允许系统存在中间状态,并且这个中间状态不会立即影响系统的整体一致性。最终一致性则是指,系统在没有新的更新操作的情况下,最终所有的副本都将达到一致的状态。

总之,BASE理论为分布式系统的设计提供了一种权衡一致性和可用性的方法,使其在面对故障和网络分区时仍能保持较高的可用性。

2.2. 分布式事务中的“一致性”

  • 在分布式事务中,我们谈到的“数据状态一致性”,指的是数据的最终一致性,而非数据的即时一致性,因为即时一致性通常是不切实际的
  • 没有分布式事务能够保证数据状态具备百分之百的一致性,根本原因就在于网络环境和第三方系统的不稳定性

2.3. 分布式事务的解决方式

2.3.1. 基于消息队列(MQ)的方式

  • 消息队列基于at least once(至少被消费一次)特性保证下游消费者至少能消费一次上游生产者产生的消息。
  • MQ 组件能够保证消息不会在消费环节丢失,但是无法解决消息的重复性问题. 因此,倘若我们需要追求精确消费一次的目标,则下游的 consumer 还需要基于消息的唯一键执行幂等去重操作,在 at least once 的基础上过滤掉重复消息,最终达到 exactly once 的语义。

2.3.1.1. 实现流程

无标题.png

2.3.1.1.1.实现流程的局限性

1.下游服务即使通过重试手段也无法保证一定执行成功,当此种情况发生时会形成上游服务执行成功而下游失败的不一致问题。

2.上游服务执行本地事务与投递“执行消息”这两步也无法保证原子性。

** 解决问题1:后面补充1111111111111111111111111**

解决局限性2:

通过本地事务包裹执行操作与消息投递。

  • 首先 begin transaction,开启本地事务
  • 在事务中,执行本地状态数据的更新
  • 完成数据更新后,不立即 commit transaction
  • 执行消息投递操作
  • 倘若消息投递成功,则 commit transaction
  • 倘若消息投递失败,则 rollback transaction 640_wx_fmt=png.png 当前解决思路是完美的吗?

存在三个致命问题:

  • 在和数据库交互的本地事务中,夹杂了和第三方组件的 IO 操作,可能存在引发长事务的风险。
  • 执行消息投递时,可能因为超时或其他意外原因,导致出现消息在事实上已投递成功,但 producer 获得的投递响应发生异常的问题,这样就会导致本地事务被误回滚的问题。
  • 在执行事务提交操作时,可能发生失败. 此时事务内的数据库修改操作自然能够回滚,然而 MQ 消息一经发出,就已经无法回收了。

山重水复疑无路,柳暗花明又一村

  1. RocketMQ中TX Msg(事务消息)解决方式:
  • 生产方 producer 首先向 RocketMQ 生产一条半事务消息,此消息处于中间态,会暂存于 RocketMQ 不会被立即发出;
  • producer 执行本地事务;
  • 如果本地事务执行成功,producer 直接提交本地事务,并且向 RocketMQ 发出一条确认消息;
  • 如果本地事务执行失败,producer 向 RocketMQ 发出一条回滚指令;
  • 倘若 RocketMQ 接收到确认消息,则会执行消息的发送操作,供下游消费者 consumer 消费
  • 倘若 RocketMQ 接收到回滚指令,则会删除对应的半事务消息,不会执行实际的消息发送操作;
  • 此外,在 RocketMQ 侧,针对半事务消息会有一个轮询任务,倘若半事务消息一直未收到来自 producer 侧的二次确认,则 RocketMQ 会持续主动询问 producer 侧本地事务的执行状态,从而引导半事务消息走向终态。
  1. 流程图如下: 640_wx_fmt=png (1).png

2.3.2. TCC

TCC,全称 Try-Confirm-Cancel,指的是将一笔状态数据的修改操作拆分成两个阶段:

  • 第一个阶段是 Try,指的是先对资源进行锁定,资源处于中间态但不处于最终态
  • 第二个阶段分为 Confirm 和 Cancel,指的是在 Try 操作的基础上,真正提交这次修改操作还是回滚这次变更操作。

640_wx_fmt=png (2).png

2.3.2.1. TCC分布式事务架构中的三类角色

1.应用方 Application:指的是需要使用到分布式事务能力的应用方,即这套 TCC 框架服务的甲方。

2.TCC 组件 TCC Component:指的是需要完成分布式事务中某个特定步骤的子模块. 这个模块通常负责一些状态数据的维护和更新操作,需要对外暴露出 Try、Confirm 和 Cancel 三个 API。 Try:锁定资源,通常以类似【冻结】的语义对资源的状态进行描述,保留后续变化的可能性;

Confirm:对 Try 操作进行二次确认,将记录中的【冻结】态改为【成功】态;

Cancel:对 Try 操作进行回滚,将记录中的【冻结】状消除或者改为【失败】态. 其底层对应的状态数据会进行回滚。

  1. 事务协调器 TX Manager:负责统筹分布式事务的执行:

3.1. 实现 TCC Component 的注册管理功能;

3.2. 负责和 Application 交互,提供分布式事务的创建入口,给予 Application 事务执行结果的响应;

3.3. 串联 Try -> Confirm/Cancel 的两阶段流程. 在第一阶段中批量调用 TCC Component 的 Try 接口,根据其结果,决定第二阶段是批量调用 TCC Component 的 Confirm 接口还是 Cancel 接口。

2.3.2.2. TCC分布式事务框架在真实的业务场景中是如何实现的

现在假设我们需要维护一个电商后台系统,需要处理来自用户的支付请求. 每当有一笔支付请求到达,我们需要执行下述三步操作,并要求其前后状态保持一致性:

  • 在订单模块(TCCⅠ)中,创建出这笔订单流水记录;
  • 在账户模块(TCCⅡ)中,对用户的账户进行相应金额的扣减;
  • 在库存模块(TCCⅢ)中,对商品的库存数量进行扣减。

640_wx_fmt=png (3).png

流程说明:

下面描述一下,基于 TCC 架构实现后,对应于一次支付请求的分布式事务处理流程:

  1. Application 调用 TX Manager 的接口,创建一轮分布式事务:
  • Application 需要向 TX Manager 声明,这次操作涉及到的 TCC Component 范围,包括 订单组件、账户组件和库存组件;

  • Application 需要向 TX Manager 提前传递好,用于和每个 TCC Component 交互的请求参数( TX Manager 调用 Component Try 接口时需要传递);

  • TX Manager 需要为这笔新开启的分布式事务分配一个全局唯一的事务主键 Transaction ID

  • TX Manager 将这笔分布式事务的明细记录添加到事务日志表中

  • TX Manager 分别调用订单、账户、库存组件的 Try 接口,试探各个子模块的响应状况,比并尝试锁定对应的资源。

  1. TX Manager 收集每个 TCC Component Try 接口的响应结果,根据结果决定下一轮的动作是 Confirm 还是 Cancel。

  • 2.1. 倘若三笔 Try 请求中,有任意一笔未请求成功:
    • TX Manager 给予 Application 事务执行失败的 Response;
    •  TX Manager 批量调用订单、账户、库存 Component 的 Cancel 接口,回滚释放对应的资源;
    • 在三笔 Cancel 请求都响应成功后,TX Manager 在事务日志表中将这笔事务记录置为【失败】状态。
  • 2.2. 倘若三笔 Try 请求均响应成功了:
    • TX Manager 给予 Application 事务执行成功的 ACK;
    • TX Manager 批量调用订单、账户、库存 Component 的 Confirm 接口,使得对应的变更记录实际生效;
    • 在三笔 Confirm 请求都响应成功后,TX Manager 将这笔事务日志置为【成功】状态。

640_wx_fmt=png (5).png

TCC实现本质:

首先,TCC 本质上是一个两阶段提交(Two Phase Commitment Protocol,2PC)的实现方案,分为 Try 和 Confirm/Cancel 的两个阶段:

  • Try 操作的容错率是比较高的,原因在于有人帮它兜底. Try 只是一个试探性的操作,不论成功或失败,后续可以通过第二轮的 Confirm 或 Cancel 操作对最终结果进行修正;
  • Confirm/Cancel 操作是没有容错的,倘若在第二阶段出现问题,可能会导致 Component 中的状态数据被长时间”冻结“或者数据状态不一致的问题。

针对于这个场景,TCC 架构中采用的解决方案是:在第二阶段中,TX Manager 轮询重试 + TCC Component 幂等去重. 通过这两套动作形成的组合拳,保证 Confirm/ Cancel 操作至少会被 TCC Component 执行一次。

TCC的容错逻辑:

首先,针对于 TX Manager 而言:

  1. 需要启动一个定时轮询任务;
  2. 对于事务日志表中,所有未被更新为【成功/失败】对应终态的事务,需要摘出进行检查;
  3. 检查时查看其涉及的每个组件的 Try 接口的响应状态以及这笔事务的持续时长。

情况处理:

  1. 倘若事务应该被置为【失败】(存在某个 TCC Component Try 接口请求失败),但状态却并未更新: 说明之前批量执行 Cancel 操作时可能发生了错误. 此时需要补偿性地批量调用事务所涉及的所有 Component 的 Cancel 操作,待所有 Cancel 操作都成功后,将事务置为【失败】状态;

  2. 倘若事务应该被置为【成功】(所有 TCC Component Try 接口均请求成功),但状态却并未更新: 说明之前批量执行 Confirm 操作时可能发生了错误. 此时需要补偿性地批量调用事务所涉及的所有 Component 的 Confirm 操作,待所有 Confirm 操作都成功后,将事务置为【成功】状态

需要注意,在 TX Manager 轮询重试的流程中,针对下游 TCC Component 的 Confirm 和 Cancel 请求只能保证 at least once 的语义,换句话说,这部分请求是可能出现重复的。

因此,在下游 TCC Component 中,需要在接收到 Confirm/Cancel 请求时,执行幂等去重操作. 幂等去重操作需要有一个唯一键作为去重的标识,这个标识键就是 TX Manager 在开启事务时为其分配的全局唯一的 Transaction ID,它既要作为这项事务在事务日志表中的唯一键,同时在 TX Manager 每次向 TCC Component 发起请求时,都要携带上这笔 Transaction ID。

640_wx_fmt=png (6).png TX Manager 和 TCC Component 职责领域划分:

对于事务协调器 TX Manager,其核心要点包括:

1.暴露出注册 TCC Component 的接口,进行 Component 的注册和管理;

2.暴露出启动分布式事务的接口,作为和 Application 交互的唯一入口,并基于 Application 事务执行结果的反馈 ;

3.为每个事务维护全局唯一的 Transaction ID,基于事务日志表记录每项分布式事务的进展明细;

4.串联 Try——Confirm/Cancel 的两阶段流程,根据 Try 的结果,推进执行 Confirm 或 Cancel 流程;

5.持续运行轮询检查任务,推进每个处于中间态的分布式事务流转到终态。

640_wx_fmt=png (7).png

对于 TCC Component 而言,其需要关心和处理的工作包括:

  1. 暴露出 Try、Confirm、Cancel 三个入口,对应于 TCC 的语义;
  2. 针对数据记录,新增出一个对应于 Try 操作的中间状态枚举值;
  3. 针对于同一笔事务的重复请求,需要执行幂等性校验;
  4. 需要支持空回滚操作. 即针对于一笔新的 Transaction ID,在没收到 Try 的前提下,若提前收到了 Cancel 操作,也需要将这个信息记录下来,但不需要对真实的状态数据发生变更。

空回滚操作解释:

从执行逻辑上,Try 应该先于 Cancel 到达和处理,然而在事实上,由于网络环境的不稳定性,请求到达的先后次序可能颠倒. 在这个场景中,Component A 需要保证的是,针对于同一笔事务,只要接受过对应的 Cancel 请求,之后到来的 Try 请求需要被忽略. 这就是 TCC Component 需要支持空回滚操作的原因所在。

640_wx_fmt=png (8).png

TCC 分布式事务实现方案的优劣势:

优势:

  1. TCC 可以称得上是真正意义上的分布式事务:任意一个 Component 的 Try 操作发生问题,都能支持事务的整体回滚操作

  2. TCC 流程中,分布式事务中数据的状态一致性能够趋近于 100%,这是因为第二阶段 Confirm/Cancel 的成功率是很高的,原因在于如下三个方面:

    • TX Manager 在此前刚和 Component 经历过一轮 Try 请求的交互并获得了成功的 ACK,因此短时间内,Component 出现网络问题或者自身节点状态问题的概率是比较小的;
    • TX Manager 已经通过 Try 操作,让 Component 提前锁定了对应的资源,因此确保了资源是充分的,且由于执行了状态锁定,出现并发问题的概率也会比较小;
    • TX Manager 中通过轮询重试机制,保证了在 Confirm 和 Cancel 操作执行失败时,也能够通过重试机制得到补偿。

劣势:

  1. TCC 分布式事务中,涉及的状态数据变更只能趋近于最终一致性,无法做到即时一致性;
  2. 事务的原子性只能做到趋近于 100%,而无法做到真正意义上的 100%,原因就在于第二阶段的 Confirm 和 Cancel 仍然存在极小概率发生失败,即便通过重试机制也无法挽救. 这部分小概率事件,就需要通过人为介入进行兜底处理;
  3. TCC 架构的实现成本是很高的,需要所有子模块改造成 TCC 组件的格式,且整个事务的处理流程是相对繁重且复杂的. 因此在针对数据一致性要求不那么高的场景中,通常不会使用到这套架构。

事实上,上面提到的第二点劣势也并非是 TCC 方案的缺陷,而是所有分布式事务都存在的问题,由于网络请求以及第三方系统的不稳定性,分布式事务永远无法达到 100% 的原子性。

文正主要思想引用自:mp.weixin.qq.com/s/Z-ZY9VYUz…