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 语句的行为
假设有表如下:
CREATE TABLE `t` (
`id` int NOT NULL,
`c` int DEFAULT NULL,
PRIMARY KEY (`id`)
) 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 | 事务到底是隔离的还是不隔离的