MySQL 事务原理

说明:事务的核心实现与存储引擎相关,所以默认情况下,本文中的事务均指 InnoDB 事务。

事务可以简单地由一条 SQL 语句组成,也可以由一组复杂的 SQL 语句组成。事务是一个程序执行单元,用于访问和更新数据库中的各种数据项。在事务中的操作要么全部执行修改,要么全部不执行,这是事务的目的,也是事务模型与文件系统的一个重要区别。

1 事务的 ACID 特性

理论上,事务有着非常严格的定义,必须同时满足四个特性,即通常所说的事务的 ACID 特性。ACID 是 Atomicity(原子性)、Consistency(一致性)、Isolation(隔离性)和 Durability(持久性)四个词的缩写。接下来,我们将详细介绍这四个属性。

1.1 Atomicity(原子性)

原子性指的是整个数据库事务作为一个不可分割的工作单位。只有当事务中的所有数据库操作都成功执行时,整个事务才被认为是成功的。如果事务中的任何一个 SQL 语句执行失败,已经执行成功的 SQL 语句也必须被撤销,数据库状态应该回滚到执行事务前的状态。

对于只读操作来说,保持原子性是相对简单的。一旦发生错误,可以选择重试或返回错误代码,因为只读操作不会改变系统中的任何相关部分。然而,当事务中的操作需要改变系统状态,例如插入记录或更新记录时,情况可能就不像只读操作那么简单了。操作的失败可能会引起状态的变化,因此必须保护系统中受影响数据的并发用户访问。

1.2 Consistency(一致性)

一致性指的是事务开始之前和事务结束之后,数据库的完整性限制未被破坏。一致性包括两方面的内容,分别是约束一致性和数据一致性。

  • 约束一致性:创建表结构时所指定的外键、Check(MySQL 不支持)、唯一索引等约束。
  • 数据一致性:是一个综合性的规定,因为它是由原子性、持久性、隔离性共同保证的结果,而不是单单依赖于某一种技术。

1.3 Isolation(隔离性)

隔离性是指在并发执行的多个事务之间存在一定程度的隔离,确保它们互相独立、互不干扰的特性。

1.3.1 事务并发可能导致的问题

事务并发处理可能会带来以下几个问题:

  • 更新丢失:当两个或多个事务同事更新一行记录时,可能会产生更新丢失现象。更新丢失分为回滚覆盖和提交覆盖两种:
    • 回滚覆盖:一个事务的回滚操作,覆盖了其它事务已经提交的数据;
    • 提交覆盖:一个事务的提交操作,覆盖了其它事务已经提交的数据。
  • 脏读:一个事务读取了另一个事务尚未提交的数据。如果后续事务回滚,那么读取到的数据实际上是无效的。
  • 不可重复读:一个事务中多次读取同一行记录不一致,在一个事务中如果能读取到其它事务已经提交的或未提交的数据,都会导致该问题。
  • 幻读:一个事务中按相同的条件多次查询,结果集或多几行或少几行数据,这个现象被称作幻读。这些多出或减少的行称作“幻行”。

1.3.2 事务的隔离级别

MySQL 支持以下四个 ANSI 规范定义的隔离级别:

  • READ UNCOMMITTED (读未提交):需要解决回滚覆盖类型的更新丢失问题,但允许发生脏读现象,也就是允许读取到其它会话中未提交事务修改的数据。
  • READ COMMITTED (读已提交):需要解决脏读问题,只能读取到其它会话中已经提交的数据。但允许发生不可重复读现象,也就是允许在一个事务中两次查询结果不一致。
  • REPEATABLE READ (可重复读):需要解决不可重复读问题,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。但是允许出现幻读现象。
  • SERIALIZABLE (串行化):所有的增删改查串行执行,从而解决所有并发问题。这个级别可能导致大量的超时现象的和锁竞争,效率低下。

不同的隔离级别可能存在的不同的并发问题,详情如下:

不同的隔离级别可能存在的并发问题
不同的隔离级别可能存在的并发问题

数据库的事务隔离级别越高,并发问题就越小,但是并发处理能力越差。读未提交隔离级别最低,并发问题多,但是并发处理能力好。实际使用时,可以根据系统特点来选择一个合适的隔离级别,比如对不可重复读和幻读并不敏感,更多关心数据库并发处理能力,此时可以使用 READ COMMITTED 隔离级别。

1.4 Durability(持久性)

事务一旦提交,其结果就是永久性的。即使发生宕机等故障,数据库也能够将数据恢复。

需要注意的是,只能从事务本身的角度来保证结果的永久性。例如,在事务提交后,所有的变化都是永久的。即使数据库因为崩溃而需要进行恢复,也能够确保恢复后提交的数据不会丢失。但如果导致数据库问题的原因不是数据库本身发生故障,而是一些外部原因,如 RAID 卡损坏、自然灾害等,那么所提交的数据依旧有可能丢失。因此,持久性保证了事务系统的高可靠性(高可靠性是指系统数据的完整性和一致性),而不是高可用性(高可用性是指系统连续运行和对外提供服务的能力)。要实现高可用性,需要系统中的其它组件与事务共同配合。

2 InnoDB WAL 机制

Write-Ahead Logging(WAL,预写日志系统)是 InnoDB 存储引擎实现事务特性的关键技术之一。简单来说,它是一种先写日志,再写数据的机制,InnoDB 的写操作并不会立刻更新到磁盘上,而是先记录在日志上,然后再在合适的时间更新到磁盘上。

2.1 WAL 保证原子性

事务写操作需要进过修改数据、写入缓冲池 (Buffer Pool) 和刷盘 (Flush) 三个步骤。在这个过程中,可能会面临以下两种情况:

  1. 如果一个事务已经提交,理论上修改的数据应该已经生效。然而,如果此时缓冲池中的脏页(未写入磁盘的数据页)尚未被刷盘,而 MySQL 发生了崩溃,那么可以通过 InnoDB 的 RedoLog 来恢复已经提交的数据。RedoLog 记录了已提交事务所做的修改,通过重新执行这些修改,可以将数据恢复到崩溃前的状态。
  2. 相反地,如果一个事务尚未提交,但是缓冲池中的脏页已经被刷盘,而此时 MySQL 发生了崩溃,那么可以通过 InnoDB 的 UndoLog 来恢复事务开始之前的数据。UndoLog 记录了事务操作的逆向操作,通过执行这些逆向操作,可以撤销尚未提交的事务对数据所做的修改,从而恢复到事务开始之前的状态。

2.2 WAL 保证持久性

所谓持久性就是指一个事务一旦成功提交,它对数据库中数据的改变就应该是永久性的,接下来的操作或故障不应该对其有任何影响。而 InnoDB 事务的持久性也与 WAL 机制相关,前面已经讲到,事务的原子性可以保证一个事务要么全执行,要么全不执行的特性,这可以从逻辑上保证用户看不到中间的状态。一旦事务提交,通过原子性,即便是遇到宕机,也可以从逻辑上将数据找回来后再次写入物理存储空间,这样就从逻辑和物理两个方面保证了数据不会丢失,即保证了数据库的持久性。

2.3 WAL 保证隔离性

隔离性指的是一个事务的执行不能被其它事务干扰,即一个事务内部的操作及使用的数据对其它的并发事务是隔离的。

锁和多版本控制 (MVCC) 可以保证隔离性,其中 MVCC 的实现也会依赖 WAL 机制。

2.4 WAL 保证一致性

一致性可以被理解为数据的完整性,而数据的完整性则是通过其它三个特性来实现的。因此,数据库的一致性间接地通过 WAL 机制来保证。

3 事务控制的演进

MySQL 提供了多种并发事务控制手段,按照并发度由低到高的顺序,包括:串行化、读写锁、多版本并发控制(MVCC)以及乐观与悲观锁。

  • 串行化:最低级别的并发控制,它确保每个事务按照顺序执行,避免了并发冲突。
  • 读写锁:也称为共享-排它锁,允许多个事务同时读取共享资源,但只有一个事务可以对资源进行写操作。这样可以提高读操作的并发性能,但仍然需要等待锁的排它访问。
  • 多版本并发控制 (MVCC):通过使用版本号或时间戳来管理数据的不同版本,允许读取旧版本的数据而不会被锁阻塞。这种机制可以提高读操作的并发性能,并避免读写冲突。
  • 乐观和悲观锁
    • 乐观锁假设并发冲突较少,事务在提交之前不会立即加锁,而是在提交时检查是否发生冲突。
    • 悲观锁则假设并发冲突较多,事务在读取数据时就会加锁,直到事务结束。
    • 乐观锁适用于并发冲突较少的情况,而悲观锁适用于并发冲突较多的情况。

4 分布式 (XA) 事务

存储引擎的事务特性能够保证在存储引擎级别实现 ACID,而分布式事务则让存储引擎级别的 ACID 可以扩展到数据库层面,甚至可以扩展到多个数据库之间。

4.1 X/Open XA 规范

XA 规范是由 X/Open 组织提出的分布式事务处理规范,主要定义了事务管理器 TM 和局部资源管理器 RM 之间的接口。目前主流的数据库,如 Oracle、DB2、MySQL 都支持该规范。

XA 为了实现分布式事务,引用了 2PC 与 3PC 这两种分布式一致性协议。

4.1.1 2PC

2PC (The two-phase commit protocol) 是 XA 用于在全局事务中协调多个资源的机制之一,它将事务的提交分成了两个阶段来处理:

  • prepare 阶段:
    • TM 向所有 RM 发送事务内容,询问是否可以提交事务,并等待所有 RM 答复;
    • RMs 执行事务操作,将操作信息记入事务日志中,但不提交事务;
    • 每个 RM 执行成功后,给 TM 反馈 YES;如执行失败,给 TM 反馈 NO。
  • commit 阶段:
    • 如果所有 RM 均反馈 YES,则 TM 向 RM 发出 commit 指令;
    • 任何一个 RM 反馈 NO,或者任何一个 RM 返回超时,则发出 rollback 指令;
    • RM 根据指令执行 commit 或者 rollback 操作,并释放所有资源。

2PC 的优点就是实现简单,因此目前绝大多数关系型数据库都是采用的这种协议,但它的缺点也很明显:

  1. 同步阻塞问题:执行过程中,所有 RM 的事务都是阻塞型的,当有 RM 占用公共资源时,其它访问公共资源的第三方节点将不得不处于阻塞状态,同时各个 RM 在等待 TM 的指令期间也会一直阻塞,如果 TM 宕机了(单点),RM 将会一直阻塞,无法达成一致性。
  2. 单点故障:TM 重要性极高,一旦 TM 发生故障,RMs 会一直阻塞下去。
  3. 数据不一致隐患:出现网络分区、网络故障时可能导致数据不一致。在第二个阶段中,如果 TM 像 RMs 发送 commit 请求的过程中出现了网络故障,可能会导致只有一部分 RM 收到了 commit 指令,另一部分 RM 会因为收不到任何指令而陷入阻塞,于是导致了数据不一致的现象。
  4. 太过保守:2PC 没有设计相应的容错机制,任何一个 RM 出现异常都会导致整个事务中断回滚(这一点 ZooKeeper 的 ZAB 协议就相对开放些)。
  5. 某些情况下的状态不确定问题:如果 TM 在二阶段发出 TM 指令后宕机,而唯一接收到这条消息 RM 也宕机了,这时即便选举出了新 Leader,也无法确定这条事务的状态,因为没有节点可以知道这条事务是否被提交成功。

由于 2PC 存在的这些问题,研究者们在此基础上又提出了 3PC。

4.1.2 3PC

3PC 是 2PC 的改进版,它将二阶段提交协议的 “prepare” 阶段一份为二,形成了 CanCommit,PreCommit,DoCommit 三个阶段。相比于 2PC,3PC 有两个改进的地方:

  1. 引入了超时提交策略。当第三阶段的 RM 等待 TM 的指令超时后会自动提交事务,解决 RM 同步阻塞的问题,同时能在 TM 发生单点故障时,继续达成一致性。
  2. 新增了一个 CanCommit 阶段,缩减了同步阻塞的发生范围。

下面我们详细介绍这三个阶段。

  • CanCommit:
    • TM 向 RMs 发送 CanCommit 请求,询问是否可以执行事务提交操作,然后开始等待各 RM 的响应;
    • RMs 接到 CanCommit 请求之后,正常情况下,如果可以顺利执行事务,则返回 Yes,并进入预备状态;否则反馈 No。
  • PreCommit:
    • 正常情况:所有 RM 均反馈 YES 时,开始提交事务
      • TM 向 RM 发送 PreCommit 请求,并进入 prepared 阶段;
      • RM 接收到 PreCommit 请求后,会执行事务操作,并将 Undo 和 Redo 信息记录到事务日志中;
      • 如果 RM 成功的执行了事务操作,则返回 ACK 响应,同时开始等待最终指令。
    • 异常情况:有任何一个 RM 向 TM 发送了 CanCommit No 响应,或者等待超时之后 TM 都没有接到任何 RM 的响应,则执行事务的中断
      • TM 向所有 RM 发送 abort 请求;
      • RM 收到来自 TM 的 abort 请求之后(或超时之后,仍未收到 PreCommit 请求),执行事务的中断。
  • DoCommit:
    • 正常情况

      • TM 接收到 RM 发送的 ACK 响应后,从 prepare 状态进入到 Commit 状态,并向所有 RM 发送 DoCommit 请求;
      • RM 接收到 DoCommit 请求之后,执行正式的事务提交,并在完成事务提交之后释放所有事务资源;
      • 事务提交完之后,向 TM 发送 ACK 响应;
      • TM 接收到所有 RM 的 ACK 响应之后,完成事务。
    • 异常情况 1:TM 超时没有收到任何 PreCommit 成功的 ACK,或者存在 RM 返回 PreCommit 执行失败的消息时

      • TM 向所有 RM 发送 abort 请求;
      • RM 接收到 abort 请求之后,利用其在阶段二记录的 Undo 信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源;
      • RM 完成事务回滚之后,向 TM 发送 ACK 消息;
      • TM 接收到 RM 反馈的 ACK 消息之后,执行事务的中断。
    • 异常情况 2:TM 发出了最终指令后,如果某个正常进入三阶段的 RM 直到超时仍然未收到指令,则执行最终提交操作。

      异常情况 2,为什么超时要自动提交,而非回滚?
      这是由概率决定的,当 RM 成功进入了第三阶段,说明它是 CanCommit 的,所以能够 Commit 成功的概率极大。

相比于 2PC,3PC 降低了 TM 单点故障导致阻塞的可能性,但是它仍然无法杜绝数据不一致问题:假如 TM 发送的 abort 命令没有被所有 RM 收到,就会导致一部分 RM 成功回滚,另一部分 RM 超时提交成功,进而导致数据不一致。而且,3PC 协议还是十分保守:3PC 仍然只要有一个 RM 返回错误信息,就中断事务进行回滚。

4.2 MySQL 的内部 XA 事务

MySQL 的插件式架构要求内部必须使用 XA 事务。由于 MySQL 中的各个存储引擎是独立的且彼此不知道对方的存在,因此跨存储引擎的事务需要外部协调者来确保一致性。如果没有内部 XA 事务的支持,跨存储引擎的事务只能按顺序要求每个存储引擎单独提交,这容易破坏事务的特性。尤其是当某个存储引擎发生崩溃时,整个事务的一致性就无法保证了。

此外,为了兼容非事务存储引擎的主从复制,MySQL 在 Server 层引入了 BinLog。然而,这也带来了 BinLog 与 InnoDB Redo 之间的一致性问题。

因此,MySQL 引入内部 XA 事务,协调了 BinLog 与 InnoDB RedoLog,保障了跨存储引擎事务的一致性和完整性。

MySQL 内部使用的 XA 事务遵循 XA 规范中的 2PC 协议。

4.2.1 MySQL 2PC 提交过程

提交动作开始时,首先会生成一个 XID 写入 Redo,然后再将 data 写入 BinLog,待 BinLog 写入成功后,会将当前写入的 BinLog 的 filename、文件内偏移量 (position) 一起写到 Redo,这样才算事务完成。如果只有 XID 写入 Redo,没有后边的 filename 和 position,表示事务处于 prepare 状态。流程图如下:

提交过程

由上图可知:

  1. 当 Session 发起提交后,首先由 Engine 进行一阶段 Prepare 操作:将生成的 XID 写入 Redo 并落盘,同时记录 Undo。

  2. Redo 落盘返回 OK 后,Server 将数据写入 BinLog。

    这里可以通过 sync_binlog 参数控制 fsync 行为,上图展示的是 sync_binlog = 1 的情形,即每次 Engine Commit 前都要确保 BinLog 落盘。

  3. BinLog 落盘后,Server 通知 Engine 执行二阶段 Commit,引擎提交后将 BinLog 的 filename、position 写入 Redo。

  4. Server 收到提交成功的信息后,将事务执行成功的结果返回 Session。

  5. 过期的 Undo 页后续由特定的线程销毁。

4.2.2 事务崩溃恢复过程

  • 当事务从崩溃中恢复时,首先通过 RedoLog 恢复数据;
  • 然后扫描最后一个 BinLog 文件中的 XID;
  • InnoDB 中会维护状态为 prepare 的事务链表,将这些事务的 XID 与最后一个 BinLog 文件中的 XID 比较:
    • 如果最后一个 BinLog 中存在这个 XID,说明这个事务已经完成了两阶段,可以提交;
    • 否则回滚这个事务。

根据以上流程,我们可以总结出事务在不同的阶段发生崩溃时,响应的事务状态:

崩溃发生阶段事务状态事务结果
当在 prepare 阶段崩溃Engine 未写 Redo,Server 也未写 BinLog事务 rollback
当在 BinLog 写阶段崩溃Engine Redo 已经落盘,但是 BinLog 未成功写入事务 rollback
当在 BinLog 落盘后崩溃,但 Engine 还没有 CommitRedo、BinLog 均已落盘,但 Redo 中事务状态未正确结束读取 BinLog 中的 XID,通知 Engine 提交这些事务

4.2.3 事务的组提交过程

以下分析的前提条件是 sync_binlog 设为 1 且 innodb_flush_log_at_trx_commit 设为 1,即安全性最高的场景。

当不开启 BinLog 时,RedoLog 的落盘操作很容易成为 MySQL TPS 的性能瓶颈,为了解决这个问题,MySQL 组提交将多个 RedoLog 刷盘操作合并到了一个事务中,大大提高了 I/O 性能。然而,在生产环境中通常会开启 BinLog,这又引入了新的性能瓶颈,即 BinLog 的 I/O。为了再次解决这个问题,MySQL 又引入了 BinLog 组提交机制。

BinLog 组提交结合 RedoLog 的组提交,将整个事务提交过程划分为 Flush、Sync、Commit 三个阶段,每个阶段中都有一个队列,每个队列都有一把锁保护,第一个进入队列的事务会获取到锁成为 Leader,Leader 领导所在队列(组)内的所有事务,全权负责组员的操作,完成后通知队内其它事务操作结束。各阶段详细流程如下:

  • Flush 阶段:
    1. 第一个事务加入 Flush 队列,获得 Flush_lock 锁后成为 Leader,并等待其它事务的加入;
    2. 一段时间过后,Leader 获取当前队列中所有的事务,划为一组;
    3. Leader 为组内的所有事务执行 prepare 操作,将 XID 写入 RedoLog,并落盘(即 RedoLog 组提交阶段);
    4. Leader 将组内所有事务的 BinLog 写入文件的 OS Cache(不落盘,崩溃后数据可能会丢失);
  • Sync 阶段:
    1. Leader 释放 Flush_lock 后,带着组内成员加入 Sync 队列,并尝试获得 Sync 队列的 Sync_lock 锁;

    2. 这里为了尽可能提高落盘收益,会等待一段时间,看看有没有其它事务也进入 Sync 队列;

      扩展: MySQL 引入了两个参数来控制队列获取事务组的时长:

      • binlog_group_commit_sync_delay=N:等待 N μs 后,开始 BinLog 落盘;
      • binlog_group_commit_sync_no_delay_count=N:队列中的事务数量达到 N 后,无视等待时间直接开始 BinLog 落盘。
    3. 如果有新的事务组到来,则新事务作为 Follower 加入这个 Sync 队列,合并成一组;

    4. 等条件成熟后,Leader 会控制组内所有事务,完成 BinLog 落盘操作(即 BinLog 组提交阶段,这一步如果成功,即使后续崩溃,也能继续提交);

  • Commit 阶段:
    1. Leader 释放 Sync_lock 锁后,带着组内成员加入 Commit 队列,并尝试获取 Commit_Lock 锁;

    2. Leader 依次将 RedoLog 中已经 prepare 的事务在 Engine 中 Commit;

      Commit 阶段会把事务的 BinLog filename、position 写入 RedoLog,但是不再需要紧急落盘了,因为到这里已经可以保证事务安全提交了,所以会把落盘任务交给参数 innodb_flush_log_at_trx_commit 控制。

    3. Leader 释放 Commit_Lock 锁,并唤醒组内的所有事务,告诉它们事务已经提交完毕。

MySQL 的组提交功能是默认开启的。

扩展:prepare_commit_mutex 锁

正常情况下,事务肯定不会一个一个的提交,因此这个过程需要并发控制,否则会导致一致性问题。MySQL 5.6 之前,为了保证 BinLog 写入顺序与 InnoDB Engine Commit 顺序一致,引入了 prepare_commit_mutex 锁机制。当事务提交时,需要先获得这个锁,只有当上一个事务完成两阶段提交,下一个事务才能开始。

毫无疑问,这回带来严重的性能问题,而且该机制还与组提交冲突,所以生产环境应该关闭这个配置。

4.3 MySQL 的外部 XA 事务

MySQL 能够参与外部分布式事务作为一个参与者,但它对外部 XA 协议的支持并不完整。举例来说,XA 协议要求一个事务中的多个连接能够建立关联,但目前的 MySQL 版本还无法满足这个要求。

由于通信延迟和参与者可能出现故障,外部 XA 事务的开销要比内部事务大。在使用 XA 事务时,特别是在广域网中,由于网络性能的不可预测性,事务失败的风险也会增加。如果存在太多不可控因素,例如不稳定的网络通信或用户长时间等待而不提交事务,最好避免使用外部 XA 事务。原因在于 XA 协议存在同步阻塞问题,一旦出现问题,会导致所有参与者都处于等待状态。任何可能引起事务提交延迟的操作都会带来巨大的代价。

通常情况下,还有其它方式可以实现高性能的分布式事务。例如,可以将数据先本地写入并放入队列,然后在一个较小、较快的事务中自动分发数据。还可以利用 MySQL 本身的复制机制来发送数据。事实上,许多应用程序完全可以避免使用分布式事务。

总之,外部 XA 事务是一种用于在多个服务器之间同步数据的方法。在无法使用 MySQL 本身的复制或性能不是瓶颈的情况下,可以考虑尝试使用外部 XA 事务。


欢迎关注我的公众号,第一时间获取文章更新:

微信公众号

相关内容