MySQL 的多版本并发控制(MVCC)是一种用于处理并发事务的机制,主要用于提高数据库的性能和一致性。InnoDB 是 MySQL 中最常用的存储引擎之一,它实现了 MVCC。
MVCC 的基本原理
-
数据版本化:在 MVCC 中,每行记录都有多个版本。每当有事务对数据进行修改时,不是直接覆盖旧的数据,而是创建一个新的版本。这些版本包含了必要的元数据信息,比如创建时间、过期时间等。
-
快照读:大多数 SELECT 查询都是使用快照读,这意味着查询会读取到某个时间点的一致性视图。通过这种方式,
读操作不需要加锁,可以极大地提高并发性能。 -
当前读:对于更新操作,InnoDB 使用当前读,即总是读取最新版本的数据并对其进行加锁以避免并发问题。
-
隐藏列:InnoDB 表中有两个隐藏的列,用于 MVCC:
trx_id: 记录最近一次插入或更新该行的事务 ID。roll_pointer: 指向撤销日志的指针,用于恢复旧版本数据。
-
撤销日志(Undo Log) :存储之前版本的数据,以便在需要时可以执行回滚,也为快照读提供支持。
-
事务隔离级别:MVCC 支持多种事务隔离级别,常见的是 REPEATABLE READ。在这个级别下,事务开始时创建的快照在整个事务期间保持不变。
MVCC 快照的具体内容
-
事务 ID 列表
- 活跃事务列表:在快照创建时,正在运行的所有事务的 ID 列表。通过这个列表,InnoDB 可以知道哪些事务是活跃的,从而判断当前事务可见的版本。
- 最小事务 ID (
min_trx_id):在活跃事务列表中最小的事务 ID,即这些事务在快照创建之前已经启动。 - 最大事务 ID (
max_trx_id):通常设定为系统中下一个将要分配的事务 ID,用于标识快照中的“未来”部分。
-
可见性规则
使用快照时,InnoDB 依据以下规则判断一个版本是否对当前事务可见:
-
如果数据版本的
trx_id小于min_trx_id,则表示该版本是在快照创建前已经提交的,因此对当前事务可见。 -
如果数据版本的
trx_id大于或等于max_trx_id,表示该版本在快照创建后生成,对当前事务不可见。 -
如果数据版本的
trx_id介于min_trx_id和max_trx_id之间,则需要进一步检查:- 如果
trx_id属于活跃事务列表,说明该事务尚未提交,因此该版本对当前事务不可见。 - 如果
trx_id不在活跃事务列表中,则该版本对当前事务可见。
- 如果
-
-
版本链及回滚指针
- 每条记录都有多个版本,通过
roll_pointer链接成一个版本链。 - 当读取一个记录时,InnoDB 会沿着版本链查找第一个符合可见性规则的版本。
- 每条记录都有多个版本,通过
MVCC 执行流程
在 MySQL 的 InnoDB 存储引擎中,事务隔离级别为 REPEATABLE READ 时,MVCC 的实现流程主要涉及如何管理并发事务的读取视图和数据版本。以下是详细流程:
1. 事务启动
当一个事务以 REPEATABLE READ 隔离级别启动时,InnoDB 会创建一个一致性视图(Consistent Read View),这个视图用于确保在整个事务过程中,读操作能看到的是事务启动时刻的一致性数据状态。
2. 快照读
- 读取视图:在事务开始时创建的读取视图会捕获当前数据库的状态,包括已提交事务和正在进行中的事务。
- 版本链:每个表行记录都有一个隐藏列
trx_id和一个回滚指针roll_pointer。通过这些信息,InnoDB 可以遍历数据版本链,从而找到符合当前事务可见性的版本。 - 可见性判断:当事务要读取某一行数据时,通过
trx_id和读取视图来判断该版本是否对当前事务可见。如果不可见,则沿着roll_pointer找到上一个版本进行同样的判断,直到找到一个可见版本或无版本可用。
3. 当前读
- 当事务执行更新、删除或插入操作时,会进行当前读。这种情况下,InnoDB 会锁住最新版本的数据,并确保其他事务不会同时修改这部分数据。
4. 版本控制
- 插入操作:新插入的行附带当前事务
trx_id作为其版本。 - 更新操作:生成一个新版本,旧版本保持不变并通过
roll_pointer保持链接。 - 删除操作:标记当前版本为删除,新版本不会被创建。
5. 事务提交与回滚
- 提交:事务提交后,其所做的更改对于之后启动的事务立即可见。
- 回滚:如果事务回滚,根据
undo log恢复数据到之前的状态。
6. 幻读处理
- 在 REPEATABLE READ 下,InnoDB 通过 Next-Key Locking 技术处理幻读问题,它不仅锁定存在的行,还会锁住索引范围,防止插入导致幻影行出现。
Next-Key Locking 结合了行锁(Record Lock)和间隙锁(Gap Lock)。它不仅锁住了索引记录本身,还锁住了这些记录之间的空隙。
案例说明
假设有一个表 accounts,包含以下字段:id, name, 和 balance。
初始状态
表中有一条记录:
| id | name | balance |
|---|---|---|
| 1 | Alice | 1000 |
版本链操作过程
-
事务 T1 开始
-
T1 开始时间戳为 10,读取
accounts。 -
当前数据版本:
trx_id: 9- 数据内容:
(id=1, name='Alice', balance=1000)
-
-
事务 T2 开始并更新
-
T2 开始时间戳为 11,对
balance进行更新:UPDATE accounts SET balance = 1200 WHERE id = 1; -
新数据版本(未提交):
trx_id: 11- 数据内容:
(id=1, name='Alice', balance=1200)
-
旧数据版本仍然存在,形成版本链。
-
-
事务 T3 开始并提交更新
-
T3 开始时间戳为 12,插入新记录:
INSERT INTO accounts (id, name, balance) VALUES (2, 'Bob', 500); -
提交后,生成新的已提交版本。
-
-
事务 T1 查询
-
T1 再次查询
accounts:SELECT * FROM accounts WHERE id = 1; -
可见性判断:
- T1 的开始时间为 10,它只看得到
trx_id小于等于 10 的版本。 - 因此,T1 看到的是旧版本
(id=1, name='Alice', balance=1000)。
- T1 的开始时间为 10,它只看得到
-
-
事务 T2 提交
-
T2 提交时,更新的版本
(trx_id: 11)成为有效版本。 -
数据链变为:
- 已提交版本:
(id=1, name='Alice', balance=1200, trx_id: 11)
- 已提交版本:
-
-
新事务 T4 开始并查询
-
T4 开始时间戳为 13,读取
accounts:SELECT * FROM accounts WHERE id = 1; -
可见性判断:
- T4 的开始时间为 13,它可以看到所有在时间戳 13 前提交的版本。
- 于是,T4 看到最新的已提交版本
(id=1, name='Alice', balance=1200)。
-
总结
- 版本链:每次数据修改都会产生一个新版本,旧版本通过
roll_pointer链接起来,形成版本链。 - 可见性判断:每个事务只能看到其开始之前提交的版本,通过对比事务的开始时间戳与行的
trx_id来决定可见性。
回滚流程
在 MySQL 的 InnoDB 存储引擎中,事务回滚操作会影响版本链的状态。回滚发生时,未提交的修改将被撤销,并且对最终的数据可见性不会产生影响。这是通过撤销日志(undo log)和版本链的管理实现的。
事务回滚过程中版本链的变化
场景描述
假设我们有一个表 accounts,其中包含以下记录:
| id | name | balance |
|---|---|---|
| 1 | Alice | 1000 |
操作步骤
-
事务 T1 开始
-
T1 对记录进行更新:
UPDATE accounts SET balance = 1200 WHERE id = 1; -
此时,InnoDB 会为该记录生成一个新的未提交的版本。
-
版本链:
- 新版本(未提交):
(id=1, name='Alice', balance=1200, trx_id=T1) - 旧版本:
(id=1, name='Alice', balance=1000)
- 新版本(未提交):
-
-
事务 T1 回滚
-
在 T1 决定回滚的时候,系统会使用 undo log 将数据恢复到它执行更新之前的状态。
-
未提交的新版本
(trx_id=T1)被丢弃,不再存在于版本链中。 -
旧版本成为当前有效版本:
- 有效版本:
(id=1, name='Alice', balance=1000)
- 有效版本:
-
-
回滚后的结果
- 在 T1 回滚完成后,其他事务或查询看到的都是回滚前的状态,即
balance=1000的记录。 - 由于 T1 的更改从未真正被提交到数据库,所以不会有任何其他事务看到其更改过的版本。
- 在 T1 回滚完成后,其他事务或查询看到的都是回滚前的状态,即
总结
- Undo Log:回滚时使用 undo log 将数据恢复到事务开始之前的状态。这是 MVCC 实现一致性快照和允许事务回滚的关键。
- 版本链维护:当事务未提交时,它的更改仅对自己可见。回滚后这些更改被撤销,未提交的版本从版本链中移除。
- 数据一致性:回滚保证了即使事务发生错误或被用户取消,数据库依然能保持一致的状态。
思考题:RR与RC性能对比
影响性能的因素
-
锁争用:
- 在高并发写操作下,REPEATABLE READ 由于使用间隙锁(gap locking),可能导致更多的锁争用,从而增加事务等待时间。
- READ COMMITTED 由于仅锁定当前访问的行,因此在同等条件下通常能提供更好的并发性能。
-
应用场景:
- 对于需要稳定视图以确保业务逻辑正确性的场景(例如,复杂的报表或分析查询),REPEATABLE READ 可以避免因数据变化所产生的不一致结果。
- 在实时性要求较高且允许轻微不一致的场景(例如,一些在线系统的浏览操作),READ COMMITTED 提供的灵活性和性能优势可能更明显。