分布式事务

分布式事务场景

事务核心特征

  • 原子性
    • 要么全部成功
    • 要么全部失败
  • 隔离性
    • 并发事务互不干扰
  • 持久性
    • 事务提交后,变更永久有效
  • 一致性
    • 宏观视角下的全局数据保持一致

分布式事务场景问题

处理一笔创建订单的请求时,需要执行两步操作

  • 从账户系统中,扣减用户的账户余额
  • 从库存系统中,扣减商品的剩余库存

事务消息方案

基于MQ实现分布式事务

依赖于 MQ 中 at least once 的性质,可以简单认为,只要一条消息被投递到了 MQ,那么这条消息一定会被下游消费。

倘若一个事务流程包含在服务 A 中执行动作 I 以及在服务 B 中执行动作 II,那么可以依据一下步骤使用 MQ 进行串联:

  • 服务 A 作为 MQ 的 producer,服务 B 作为 MQ 的 consumer
  • 服务 A 执行动作 I,执行成功后往 MQ 中投递消息
  • 服务 B 接收到消息后,完成动作 II

优势

  • 将服务 A 和服务 B 解耦,提高系统的吞吐量
  • 当动作 I 执行失败后,可以选择不发送消息,从而熔断流程
  • 基于MQ 的 at least once 语义,服务 B 一定能够收到消息
  • 依赖于 MQ 的 ack 机制,服务 B 可以进行有限次数的重试,提高动作 II 的执行成功率

缺点

  • 由于无法对上游动作 I 进行回滚,那么动作 II 在客观上无法成功完成时,就无法保证原子性
  • 服务 A 实际上执行的两步操作:执行动作、投递消息,不能保证原子性

下面将介绍如何保证执行动作(本地事务)、投递消息两步操作的原子性

原子性:本地事务和消息投递

方案一:将消息投递动作包含进本地事务中

缺点:

  • 本地事务操作中参杂了对第三方组件的操作,可能导致长事务问题
  • 本地事务和 MQ 强耦合,如果 MQ 成功发送了消息,但是获取响应时超时了,那么机制会误判,对本地事务进行回滚
  • 如果 MQ 成功发送消息,但是事务提交失败:此时本地事务中的数据自然而然地回滚,MQ 中的消息却覆水难收

方案二:事务消息

使用 RocketMQ 中的事务消息,本质还是两阶段提交的过程(类似于 redolog 和 binlog 一致性的做法)

为了避免本地事务未及时给MQ 响应,MQ 会定时(轮训)向 producer 查询本地事务的执行结果

缺点

  • 不具备逆向回滚的功能
    • 硬伤
  • 流程高度抽象:事务消息方案通过将分布式事务概括为本地事务 + 消息投递两个动作。实际上一个分布式事务中可能会涉及很多操作流程,笼统的概括为本地事务 + 消息投递,可能会导致大量繁重的操作被聚焦在本地事务中,会给 producer 带来极大的压力。

TCC

在 TCC 分布式事务框架中,主要有三类角色:

  • 应用方 Application
    • 指的是需要使用 TCC 分布式事务框架的应用方
  • 事务协调器 TX Manager
    • 负责统筹分布式事务的执行
    • 实现TCC Component 的注册管理
    • 负责和 Application 交互,提供分布式事务的创建入口,基于 Application 事务执行结果的响应
    • 串联 Try -> Confirm / Cancel 的两阶段流程。在第一阶段中批量调用 Try 接口,在第二阶段中批量调用 Confirm/Cancel 接口
  • TCC 组件 TCC Component
    • 指的是在分布式事务中需要完成特定操作的子模块。这个模块通常负责一些状态数据的维护和更新操作,需要对外暴露出 Try、Confirm和 Cancel 三个接口
    • Try:锁定资源,通常类似于冻结的语义,保留后续变化的可能性
    • Confirm:对 Try 操作进行二次确认,将记录中的冻结状态修改为成功状态
    • Cancel:对 Try 操作继续回滚,将记录中的冻结状态消除或修改为失败状态,将底层对应的数据状态进行回滚

优势:

  • 任意一个 Component 的 Try 操作失败了,都能对整体进行回滚
  • TX Manager 已经通过 Try 操作,让 Component 提前锁定了对应的资源,因此确保了资源是充足的,且由于提前执行了状态锁定,因此发生并发问题的概率更小

缺点

  • 实现成本很高
  • 数据状态只能趋于最终一致性,无法做到即时一致性