TODO: 补充 INSERT, DELETE 语句的行为 用示例说明原理

1 概述

MySQL MVCC(多版本并发控制,下称 MVCC)是一种事务并发控制方式,其目标是提高事务并发度。

MVCC 通过快照读,而不是行锁,实现读操作的并发控制。在这种方式下,读操作不占用行锁,仅写操作占用行锁,相对于读写操作都占用行锁的方式,能获得更高的事务并发度。

MVCC 只适用于 InnoDB 引擎。

2 原理

2.1 数据结构

MVCC 基于一致性读视图(consistent read view))和 行版本链 2 个数据结构实现。

2.1.1 一致性读视图

一致性读视图用于实现 READ COMMITTED 和 REPEATABLE READ 事务隔离级别。一个事务的一致性读视图由创建该视图时所有活跃事务(未提交或回滚的事物)的ID集合,和此时事务系统已创建的事物的最大 ID + 1 (该最大ID + 1 称为高水位)组成。

2.1.2 行版本链

每当一个事务更新了一行时,就创建了该行的一个新版本。一个行版本包含行数据和创建该版本的事物的ID。一行的所有版本按其创建时间从晚到早的顺序链接成该行的版本链。行版本链基于 undo log 实现。

2.2 一致性读视图的生命周期

隔离级别为 READ COMMITTED 的事务的一致性读视图生命周期:

  • 创建:每条语句执行前。
  • 失效:每条语句执行后。

隔离级别为 REPEATABLE READ 的事务的一致性读视图生命周期:

  • 创建:与事务的启动方式有关:
    • 用 begin/start transaction 启动事务:执行首个快照读语句时创建读视图。
    • 用 start transaction with consistent snapshot 启动事务:执行 start transaction with consistent snapshot 时创建读视图。
  • 失效:事务结束后。

2.3 SELECT 语句的行为

假设有表如下:

  1. CREATE TABLE `t` (
  2. `id` int NOT NULL,
  3. `c` int DEFAULT NULL,
  4. PRIMARY KEY (`id`)
  5. ) ENGINE=InnoDB;

有 SELECT 语句:SELECT * FROM t WHERE id = 1。

2.3.1 关于锁

以上 SELECT 语句不会对 id = 1 这行加共享锁。

2.3.2 读取哪个行版本

事务 T 的 SELECT 语句看到的行版本,为从行版本链的最新版本开始检查,符合以下条件之一的第 1 个版本:

  • 创建该版本的事务是事务 T
  • 创建该版本的事务的 ID 小于事务 T 的一致性读视图的高水位,且其不在事务 T 的一致性读视图的活跃事务 ID 集合中

即事务 T 的 SELECT 语句看到的行版本,是事务 T 和 创建事务 T 的一致性视图时所有已提交事务,所创建的最新版本。

2.4 UPDATE 语句的行为

对于上节的表 t,假设有 UPDATE 语句: UPDATE t SET c = c + 1 WHERE id = 1。

2.4.1 关于锁

以上 UPDATE 语句会对 id = 1 这行加排他锁,且在事务提交或回滚后才释放锁。

2.4.2 读取哪个行版本

以上 UPDATE 语句要先读取记录的 c 字段值,然后才能计算新的 c 字段值。该语句总是读取行的最新行版本,而不是读取 2.3.2 节中的规则确定的行版本,这称为当前读(current read)。若最新版本是其他事务创建的,且该事务尚未提交,则该事务仍持有 id = 1 行的的排他锁,故以上 UPDATE 语句会因锁等待而阻塞。

3 示例

实验1

autocommit = 1, transaction_isolation = “REPEATABLE-READ”

(id, status) = (1, 1)

步骤 事务1 事务2
操作 结果 操作 结果
1 start transaction with consistent snapshot;
2 start transaction with consistent snapshot;
3 update tmp set status = status + 1 where id = 1; 成功
4 update tmp set status = status + 1 where id = 1; 锁等待超时,事务回滚
5 select status from tmp where id = 1;
6 commit;
7 commit;

实验2

autocommit = 1, transaction_isolation = “REPEATABLE-READ”

(id, status) = (1, 1)

步骤 事务1 事务2
操作 结果 操作 结果
1 start transaction with consistent snapshot;
2 update tmp set status = status + 1 where id = 1; 成功
3 update tmp set status = status + 1 where id = 1; 成功
4 select status from tmp where id = 1; 3
5 commit;

实验3

autocommit = 1, transaction_isolation = “REPEATABLE-READ”

(id, status) = (1, 1)

步骤 事务1 事务2
操作 结果 操作 结果
1 start transaction with consistent snapshot;
2 update tmp set status = status + 1 where id = 1; 成功
3 select status from tmp where id = 1; 1
4 update tmp set status = status + 1 where id = 1; 成功
5 select status from tmp where id = 1; 3
6 commit;

4 参考

极客时间,丁奇,《MySQL实战45讲》专栏,03 | 事务隔离:为什么你改了我还看不见

极客时间,丁奇,《MySQL实战45讲》专栏,08 | 事务到底是隔离的还是不隔离的