分布式-分布式事务

事务

为什么需要事务?

在复杂的系统操作中,数据一致性是一个关键问题。例如,在转账场景中,A向B转账100元,执行顺序为先扣除A账户中的100元,再向B账户中增加100元。如果系统在扣除A账户的100元后突然宕机,B账户并未增加100元,这将导致数据不一致。为了解决这类问题,事务机制应运而生。事务确保一组操作要么全部完成,要么全部不完成,从而保证数据的一致性和完整性。

数据库事务

在日常开发中,特别是在单体架构中,数据库事务是最常见的事务类型。数据库事务可以保证多个数据库操作作为一个逻辑上的整体来执行,遵循“要么全部完成,要么全部不完成”的原则。

1
2
3
4
5
6
# 开启事务
START TRANSACTION;
# 多条SQL语句
...
# 提交事务
COMMIT;

数据库事务具有ACID特性,确保数据的可靠性和一致性:

  • 原子性(Atomicity):事务是最小的执行单位,不可分割。事务的原子性确保动作要么全部完成,要么全部失败。
  • 一致性(Consistency):执行事务前后,数据要保持一致。例如,在转账事务中,无论转账成功与否,转账者和收款者的总额应保持不变。
  • 隔离性(Isolation):并发数据库访问时,一个事务不会影响另外一个事务,事务之间是独立的。
  • 持久性(Durability):一个事务被提交之后,在数据库中的修改是永久的,即使数据库发生故障也不会对其有任何影响。

需要注意的是,A、I、D是实现手段,而C是最终目的。

数据库事务实现原理

MySQL默认的InnoDB引擎通过以下机制实现事务的ACID特性:

  • 原子性(Atomicity):通过undo log(回滚日志)来保证。undo log记录了事务执行前的数据状态,如果事务执行过程中发生错误,可以通过undo log回滚到事务开始前的状态。
  • 持久性(Durability):通过redo log(重做日志)来保证。redo log记录了事务执行后的数据状态,即使数据库发生故障,也可以通过redo log恢复到事务提交后的状态。
  • 隔离性(Isolation):通过锁机制和MVCC(多版本并发控制)来保证。锁机制确保事务在执行过程中不会被其他事务干扰,MVCC通过为每个事务创建一个版本号,确保事务之间的数据隔离。
  • 一致性(Consistency):通过上述机制的协同作用,确保事务执行前后数据的一致性。

事务隔离级别

MySQL默认的事务隔离级别是Repeatable-read(可重复读),不同的隔离级别会影响事务的并发性能和数据一致性:

  • Read Uncommitted(读未提交):最低的隔离级别,事务可以读取到其他事务未提交的数据,可能导致脏读、不可重复读和幻读。
  • Read Committed(读已提交):事务只能读取到其他事务已提交的数据,避免了脏读,但可能出现不可重复读和幻读。
  • Repeatable Read(可重复读):事务在执行过程中多次读取同一数据时,结果保持一致,避免了不可重复读,但可能出现幻读。
  • Serializable(串行化):最高的隔离级别,事务串行执行,避免了所有并发问题,但性能最低。

通过理解事务的ACID特性和实现原理,开发人员可以更好地设计和实现可靠的数据库操作,确保系统的数据一致性和稳定性。

分布式事务

在微服务架构下,一个系统被拆分为多个小的微服务,每个微服务可能存在不同的服务器上,并且每个微服务可能都有一个单独的数据库供自己使用。这种情况下,一组操作可能涉及多个微服务以及多个数据库。

例如,在电商系统中,创建一个订单一般会涉及到两个表以上的操作:订单表加1,库存表减1。而订单表和库存表可能分别存放在不同服务器的数据库中。这种场景下想要保证数据的一致性,应该如何实现?

此时,想通过数据库自带的事务管理可能不太够用了,需要通过分布式事务来实现。

涉及在不同的数据库进行操作时,都应该引入分布式事务。比如在单个数据库的性能达到瓶颈或数据量过大时,我们需要进行分库,分库之后,同一个数据库的表数据分布在了不同的表中。

分布式事务的最终目的,也就是保证多个相关联的数据库之间的数据保持一致。按道理来说既然是保证一致性,那么应该也是保证ACID特性。但由于性能、可用性等方面考虑,分布式事务往往无法保证ACID,而只能选择一个比较折中的方案。

分布式事务解决方案

刚性事务

在互联网分布式场景中,某些关键业务场景要求数据具备强一致性,因此需要采用能够保证这种特性的解决方案,这类解决方案被称为刚性事务。常见的刚性事务解决方案包括两阶段提交(2PC)、三阶段提交(3PC)等。

2PC 和 3PC 属于业务代码无侵入式的解决方案,它们都是基于 XA 规范衍生出来的实现。

XA 规范是由 X/Open 组织提出的分布式事务处理标准,定义了分布式事务管理器(Transaction Manager, TM)和资源管理器(Resource Manager, RM)之间的接口:

  • AP(Application Program):应用程序本身。
  • RM(Resource Manager):资源管理器,是事务的参与者,绝大多数情况下指代的都是数据库,一个分布式事务通常涉及多个RM。
  • TM(Transaction Manager):事务管理器/协调者,负责管理全局事务,分配事务唯一标识符,监控事务的执行进度,并负责事务的提交、回滚、失败恢复等。

2PC(二阶段提交协议)

2PC

2PC(Two-Phase Commit),即两阶段提交协议,是一种经典的分布式事务协议,旨在确保分布式系统中的多个节点在事务提交或回滚时保持一致性。2PC 分为两个阶段:准备阶段(Prepare Phase)和提交阶段(Commit Phase)。

准备阶段(Prepare)

准备阶段的核心目标是确认所有事务参与者是否能够成功执行本地数据库操作。

准备阶段的工作流程如下:

  1. 询问阶段:事务管理者/协调者(Transaction Manager, TM)向所有涉及到事务的参与者(Resource Manager, RM)发送询问消息,询问它们是否可以执行事务操作,并等待 RM 的回复。
  2. 执行阶段:RM 接收到询问消息后,执行本地事务操作,如写入 redo log、undo log 等。此时,事务尚未提交。如果 RM 能够成功执行事务操作,则向 TM 响应“Yes”表示已就绪;如果事务执行失败,则响应“No”表示未就绪。

提交阶段(Commit Phase)

提交阶段的核心目标是确认所有事务参与者是否能够成功提交本地事务。

当准备阶段的所有参与者都响应“已就绪”状态给 TM 时,进入事务提交阶段:

  1. 提交指令:TM 向所有 RM 发送“Commit”指令,表示 RM 可以进行事务提交。
  2. 提交事务:RM 接收到“Commit”指令后,开始提交本地事务,并释放占用的数据库资源。
  3. 确认提交:RM 提交事务后,向 TM 回复“ACK”确认消息,表示事务已完成提交。
  4. 事务结束:TM 收到所有 RM 的确认消息后,整个分布式事务正式结束。

然而,如果在提交阶段有部分参与者响应“未就绪”状态,则表示有部分事务执行失败。根据事务的原子性原则,此时需要进行事务回滚:

  1. 回滚指令:TM 向所有 RM 发送“Rollback”指令,表示 RM 需要进行事务回滚。
  2. 回滚事务:RM 接收到“Rollback”指令后,开始回滚本地事务,并释放占用的数据库资源。
  3. 确认回滚:RM 回滚事务后,向 TM 回复“ACK”确认消息,表示事务回滚已完成。
  4. 事务中断:TM 收到所有 RM 的确认消息后,中断整个事务。

总结

2PC 的准备阶段旨在确认所有参与者是否能够执行事务操作;提交阶段则确认参与者的事务提交状态。提交阶段的结果由准备阶段决定,是进行提交还是进行回滚。

2PC 的优点:

  • 简单易实现:2PC 的实现相对简单,易于理解和部署。
  • 强一致性:2PC 能够确保事务的强一致性,适用于对数据一致性要求极高的场景。

2PC 的缺点:

  • 同步阻塞:由于需要保持强一致性,2PC 在事务期间会占用资源,导致其他事务被阻塞,降低了系统的可用性。
  • 数据不一致:如果 TM 在事务过程中突然宕机,可能会导致部分参与者已经提交事务,而其他参与者尚未提交,从而引发数据不一致问题。
  • 单点故障:TM 在 2PC 中扮演着至关重要的角色,如果 TM 宕机,其他 RM 将无法继续进行事务操作,导致系统停滞。

3PC(三阶段提交协议)

3PC

3PC(Three-Phase Commit)是对 2PC 的改进,通过引入一个预提交阶段(PreCommit Phase)来减少阻塞问题,并提高系统的可用性。3PC 将事务的准备阶段分为两个部分:准备阶段(CanCommit Phase)和预提交阶段(PreCommit Phase),再加上最终的提交阶段(DoCommit Phase)。

准备阶段(CanCommit Phase)

准备阶段(CanCommit Phase)的核心目的是确认所有参与者(Resource Manager, RM)的响应能力和是否能够执行本地数据库事务操作。

准备阶段的工作流程如下:

  1. 询问阶段:事务管理者/协调者(Transaction Manager, TM)向所有 RM 发送“CanCommit”询问消息,询问它们是否能够执行事务操作,并等待 RM 的回复。
  2. 响应阶段:RM 接收到“CanCommit”询问消息后,不会执行实际的事务操作,而是根据自身状态判断是否能够执行事务。如果 RM 能够执行事务操作,则向 TM 响应“Yes”表示可以执行;如果 RM 不能执行事务操作,则响应“No”表示不能执行;如果 RM 超时未响应,TM 也会认为 RM 不能执行事务。

预提交阶段(PreCommit Phase)

如果准备阶段中所有的 RM 都回复了“Yes”,则进入预提交阶段(PreCommit Phase)。预提交阶段的核心目的是让 RM 执行本地事务操作,但并不提交事务。

预提交阶段的工作流程如下:

  1. 预提交指令:TM 向所有 RM 发送“PreCommit”指令,表示 RM 可以执行本地事务操作,但不要提交。
  2. 执行事务操作:RM 接收到“PreCommit”指令后,执行本地事务操作,但并不提交事务。如果 RM 成功执行事务操作,则向 TM 响应“Yes”表示事务执行完毕;如果 RM 执行事务操作失败,则响应“No”表示事务执行失败。
  3. 超时机制:在预提交阶段,TM 和 RM 都引入了超时机制。如果 TM 在一定时间内未收到 RM 的响应,或者 RM 在执行事务操作时超时,都会触发事务回滚,以解除资源占用,避免事务阻塞。

执行事务提交阶段(DoCommit Phase)

进行真正的事务提交。

如果 TM 在预提交阶段收到了所有 RM 响应“Yes”,则进入最终的提交阶段(DoCommit Phase)。

提交阶段的工作流程如下:

  1. 提交指令:TM 向所有 RM 发送“DoCommit”指令,表示 RM 可以进行事务提交。
  2. 提交事务:RM 接收到“DoCommit”指令后,提交本地数据库事务,并释放占用的资源。
  3. 确认提交:RM 提交事务后,向 TM 回复“Committed”确认消息,表示事务已完成提交。
  4. 事务结束:TM 收到所有 RM 的“Committed”确认消息后,整个 3PC 事务正式结束。

如果在预提交阶段有任一 RM 响应“No”或者超时未响应,TM 会向所有 RM 发送“Abort”指令,中断事务。此时中断事务的损失对 RM 来说并不大,因为事务还未进行提交。

总结

3PC 通过引入预提交阶段,减少了 2PC 中的阻塞问题,并提高了系统的可用性。3PC 的工作流程如下:

  1. CanCommit Phase:确认 RM 的响应能力和是否能够执行事务操作。
  2. PreCommit Phase:RM 执行本地事务操作,但并不提交,引入超时机制以避免阻塞。
  3. DoCommit Phase:进行真正的事务提交,确保所有 RM 都成功提交事务。

3PC 的优点:

  • 减少阻塞:通过引入预提交阶段,减少了 2PC 中的阻塞问题,提高了系统的可用性。
  • 超时机制:引入了超时机制,避免了因单点故障导致的事务阻塞。

3PC 的缺点:

  • 复杂性增加:相较于 2PC,3PC 的实现更为复杂,增加了系统的复杂性。
  • 性能开销:由于引入了额外的阶段和超时机制,3PC 的性能开销相对较大。

柔性事务

基于分布式系统的 CAP 理论与 BASE 理论,日常的分布式架构下更多需要保证的是整体系统的可用性,在数据的一致性方面可以进行一定的妥协,更多采用的是“最终一致性”方案,此类方案称为柔性事务。常见的柔性事务解决方案包括 TCC(补偿事务)、消息队列事务(MQ 事务)、Saga 等。

TCC(补偿事务)

TCC(Try-Confirm-Cancel)是一种补偿型事务模型,它将事务分为三个阶段:尝试阶段(Try Phase)、确认阶段(Confirm Phase)和取消阶段(Cancel Phase)。TCC 通过显式地定义事务的尝试、确认和取消逻辑,来实现分布式事务的最终一致性。

尝试阶段(Try Phase)

尝试阶段的核心目标是尝试执行事务操作,并准备好业务所需的资源。

尝试阶段的工作流程如下:

  1. 业务检查:业务系统进行必要的业务检查,确保事务操作可以执行。
  2. 资源预留:业务系统预留必要的资源,如数据库锁、缓存资源等,但并不提交事务。

确认阶段(Confirm Phase)

确认阶段的核心目标是确认执行事务操作,并处理尝试阶段预留的业务资源。

确认阶段的工作流程如下:

  1. 确认执行:当所有参与者的尝试阶段都执行成功后,事务协调者(Transaction Coordinator, TC)向所有参与者发送确认指令(Confirm)。
  2. 资源提交:参与者接收到确认指令后,提交事务并释放预留的资源。

取消阶段(Cancel Phase)

取消阶段的核心目标是取消执行事务操作,并释放尝试阶段预留的资源。

取消阶段的工作流程如下:

  1. 取消执行:如果任何一个参与者的尝试阶段执行失败,或者在确认阶段出现失败,事务协调者向所有参与者发送取消指令(Cancel)。
  2. 资源释放:参与者接收到取消指令后,回滚事务并释放预留的资源。

失败处理与重试机制

在 TCC 的确认阶段或取消阶段,如果出现失败,TCC 会记录事务日志并持久化到某种存储介质上,如本地磁盘文件或关系型数据库等。事务日志包含了事务的执行状态。

如果检测到在确认或取消阶段执行事务出现失败状态,TCC 会进行重试该阶段的事务逻辑。重试次数一般为 6 次,若超过重试次数仍然失败,则需要人工介入处理。

事务日志的管理

事务日志在事务执行成功后可以被删除,以节省资源。事务日志的管理通常由事务协调者负责,确保在事务成功提交后清理相关日志。

业务侵入性

TCC 不需要依赖于底层的数据库资源,更多地是需要手动实现事务的尝试、确认和取消逻辑,因此属于业务侵入式分布式事务解决方案。开发人员需要在业务代码中显式地实现 TCC 的三个阶段,增加了开发复杂度,但同时也提供了更高的灵活性和控制力。

MQ事务

消息队列(Message Queue, MQ)事务允许事务流应用将生产、处理、消费消息的整个过程看作是一个原子操作。常见的消息队列系统如 RocketMQ、Kafka、Pulsar、QMQ 等都提供了事务相关的功能。下面以 RocketMQ 为例,详细介绍 MQ 事务的工作原理和处理机制。

RocketMQ 事务消息

RocketMQ 的事务消息机制采用了类似于两阶段提交(2PC)的流程,确保消息的生产和消费过程的原子性。

RocketMQ

事务消息的基本流程
  1. 发送半事务消息
    • 生产者在消息队列中开启一个事务,然后发送“半事务”消息(Half Message)给 RocketMQ 的 Broker。此时,“半事务消息”对于消费者来说是不可见的。
    • Broker 接收到“半事务消息”后,响应“半事务消息发送成功”信息给生产者。
  2. 执行本地事务
    • 生产者收到 Broker 的“发送成功”响应后,开始执行本地事务操作。
    • 根据本地事务的执行状态,生产者决定向 Broker 发送“Commit”或“Rollback”指令。
  3. 提交或回滚事务消息
    • 如果生产者发送“Commit”指令,Broker 将“半事务消息”转换为真正的消息,并投递给消费者进行消费。
    • 如果生产者发送“Rollback”指令,Broker 将丢弃该“半事务消息”,不进行投递。
失败处理与事务反查机制

如果在第 4 步,生产者发送“Commit”或“Rollback”消息失败了,RocketMQ 的 Broker 会定期查询生产者对应事务的本地事务执行情况,并根据反查结果决定提交或回滚这个“半事务消息”。

事务反查机制的实现依赖于业务代码实现的对应接口。RocketMQ 提供了事务反查的回调接口,业务系统需要实现该接口,以便 Broker 在需要时查询本地事务的状态。

消息消费失败处理

如果消息消费失败,RocketMQ 会进入消息重试机制,再次尝试消费消息。如果消息重试超过了最大次数,RocketMQ 会认为这个消费有问题,然后将其放入死信队列(Dead Letter Queue, DLQ)中,由人工手动排查处理。

QMQ 事务消息

QMQ 的事务消息机制相对简单,它借助了数据库自带的事务功能,采用了类似于 eBay 提出的本地消息表方案,将分布式事务拆分成本地事务进行处理。

本地消息表方案
  1. 业务操作与消息表写入
    • 业务系统在执行本地事务操作的同时,将消息发送状态写入本地消息表。这两个操作在一个事务中提交,确保业务操作成功时消息表也写入成功。
  2. 消息发送
    • 单独起一个线程定时轮询消息表,将未处理的消息发送到消息中间件(如 RocketMQ、Kafka 等)。
    • 消息发送成功后,更新消息状态为成功或直接删除消息。
优点与适用场景

QMQ 的事务消息方案中,即使消息队列挂掉,也不会影响数据库事务的执行,因此更加适应于大多数业务场景。这种方法同样适用于其他消息队列,只是 QMQ 封装得更好,提供了开箱即用的解决方案。