一、什么是MVCC?
MVCC,即 Multiversion Concurrency Control(多版本并发控制),它是数据库实现并发控制的一种方式。
MVCC 的核心思想是:
为每个事务提供数据的“快照”版本,从而避免加锁,提高读操作的并发性。
作用:
- 允许 读操作(SELECT)不用加锁,从而避免阻塞。
- 避免“读写冲突”,提高并发性能。
- 实现事务的 隔离性(特别是实现 REPEATABLE READ、READ COMMITTED)
💡 二、MVCC 是如何工作的(以 InnoDB 为例)
InnoDB 如何实现 MVCC?
它通过以下两个隐藏字段实现版本控制:
trx_id
:每行记录最后一次修改它的事务ID。roll_pointer
:指向旧版本(undo log)的指针,形成多版本链。
此外,事务开始时有一个 read view(快照视图),用来判断哪些版本是“可见的”。
举个例子:
- 假设 A 开始一个事务,读取一行数据。
- 此时 B 事务修改了该行数据,并提交。
- A 仍然能看到旧版本的数据(通过 undo log 追溯),这是快照隔离。
👉 这种读取叫做 一致性读(Consistent Read),完全不加锁!
📌 四、MyISAM 为什么不支持 MVCC?
MyISAM 不支持事务,因此也就不支持:
- undo log
- 多版本
- read view 快照
当有一个查询时,它只能使用表级锁来保证一致性。这种方式:
- 并发性能低(特别是写多的时候)
- 无法提供事务隔离级别(没有REPEATABLE READ等)
🧠 总结:
MySQL 存储引擎 MVCC 差异总结
- InnoDB:支持事务 + 行级锁 + MVCC(多版本并发控制)
-> 读不加锁,性能好,支持一致性读 - MyISAM:不支持事务,不支持 MVCC,使用表级锁
-> 读写容易阻塞,性能差,读的是最新数据
🧱 一、前置知识:事务隔离级别(SQL标准)
事务隔离级别从低到高:
隔离级别 | 描述 | 常见问题 |
---|---|---|
READ UNCOMMITTED | 可以读到未提交的数据 | 脏读(读未提交的数据) |
READ COMMITTED | 只能读到已提交的数据 | 不可重复读 |
REPEATABLE READ | 多次读取同一数据结果一致 | 幻读 |
SERIALIZABLE | 全部加锁,串行执行 | 性能差但最安全 |
🔍 二、MVCC 如何支持隔离性?
✅ REPEATABLE READ(可重复读,MySQL 默认)
多次
SELECT
相同数据时,读到的是事务开始时的数据快照(read view),不受其他事务影响。
✔ 具体实现:
- 事务一开始,InnoDB 创建一个 read view。
- 所有
SELECT
查询都是基于这个 read view。 - 即使别的事务修改并提交了数据,本事务看到的还是原来的版本(通过 undo log 回溯)。
- 会出现
幻读
,即读取到的不是最新数据,因为可重复读采用的是undo log的read view快照机制,用的是事务开始保存的快照,而不是实时数据。
🧠 举个例子:
-- 事务 A
START TRANSACTION;
SELECT * FROM user WHERE id=1; -- 假设 name='Tom'
-- 事务 B
START TRANSACTION;
UPDATE user SET name='Jerry' WHERE id=1;
COMMIT;
-- 回到事务 A
SELECT * FROM user WHERE id=1; -- name 仍然是 'Tom',实现了 repeatable read
事务 A 的所有读取都基于它开始时的快照,看到的是“旧世界”。
✅ READ COMMITTED(已提交读)
每次
SELECT
都读取当前最新提交版本的数据。
✔ 具体实现:
- 每次读取都会生成一个新的undo log的 read view。
- 所以可以读到别的事务已提交的新数据。
- 可能出现
不可重复读
,即,同一事务中select的是不同数据。
🧠 举个例子:
-- 事务 A
START TRANSACTION;
SELECT * FROM user WHERE id=1; -- name = 'Tom'
-- 事务 B
START TRANSACTION;
UPDATE user SET name='Jerry' WHERE id=1;
COMMIT;
-- 回到事务 A
SELECT * FROM user WHERE id=1; -- name = 'Jerry'(读到了新数据)
这种机制虽然不会脏读,但不能重复读,因为两次查询结果不一样。
🧠 总结:
InnoDB 实现事务隔离性核心在于:MVCC + undo log + read view
REPEATABLE READ:
- 每个事务在开始时创建 read view
- 所有查询基于这个快照版本
- 即使其他事务提交了修改,也读不到(实现“可重复读”)
READ COMMITTED:
- 每次查询时都重新生成 read view
- 总是读取最新提交的数据
- 能避免脏读,但可能“不可重复读”
MySQL 默认使用 REPEATABLE READ,避免幻读靠间隙锁
(gap lock)
📦 一、四种事务隔离级别及存在的问题汇总
隔离级别 | 是否可读未提交 | 是否可重复读 | 是否会幻读 | 使用场景(读一致性) |
---|---|---|---|---|
READ UNCOMMITTED | ✅ 是 | ❌ 否 | ✅ 是 | 最低一致性,无任何保障 |
READ COMMITTED | ❌ 否(读提交) | ❌ 否 | ✅ 是 | 大多数数据库默认,如Oracle |
REPEATABLE READ | ❌ 否 | ✅ 是 | ❌(InnoDB中) | MySQL默认,支持一致快照 |
SERIALIZABLE | ❌ 否 | ✅ 是 | ✅ 否(加锁) | 串行执行,开销大 |
🔍 每种隔离级别的问题示例
问题类型 | 描述 | 发生条件 |
---|---|---|
脏读(Dirty Read) | 读到了未提交的数据 | 仅在 READ UNCOMMITTED 下可能发生 |
不可重复读(Non-repeatable Read) | 同一条记录两次读结果不一致 | READ COMMITTED |
幻读(Phantom Read) | 两次读取结果行数不一致(新增/删除) | REPEATABLE READ 但有范围查询时才会发生 |
🧬 二、MVCC 实现关键机制:undo log + read view
✅ undo log(回滚日志)
- 记录旧版本数据,用于:
- 回滚(ROLLBACK)
- 一致性读(Consistent Read)
undo log 的基本结构
每条记录维护:
- 修改前的值(旧版本)
- 事务 ID(trx_id)
- 回滚指针(roll_pointer)指向上一个版本
✅ read view(快照视图)
- 事务在 第一次执行一致性读(SELECT)时创建
- 用于判断某条数据的哪个版本“对当前事务可见”
判断规则(InnoDB MVCC 可见性判断):
当前事务ID = T
记录版本的 trx_id = R
如果 R < 最小活跃事务ID:可见(已经提交)
如果 R == 当前事务ID:可见(自己改的)
如果 R 是活跃事务ID之一:不可见(别人还没提交)
🧱 三、幻读的由来与间隙锁的解决方案
🧨 什么是幻读(Phantom Read)?
指一类特殊的不可重复读 —— 两次范围查询返回不同数量的结果。
示例:
-- 事务 A
START TRANSACTION;
SELECT * FROM user WHERE age > 20;
-- 事务 B
INSERT INTO user (name, age) VALUES ('NewUser', 21);
COMMIT;
-- 回到事务 A
SELECT * FROM user WHERE age > 20; -- 发现多了一条,产生幻读
🧰 InnoDB 的解决方法:间隙锁(Gap Lock)
在可重复读隔离级别下,为防止幻读,InnoDB 对范围查询加“间隙锁”。
间隙锁含义:
- 不锁定具体记录,而锁定“两个记录之间的间隙”。
- 其他事务不能在这个间隙中插入新数据。
举例:
SELECT * FROM user WHERE age > 20 FOR UPDATE;
-- 会锁住 20 ~ ∞ 之间的“空隙”,禁止插入
⚠ 注意:
- 间隙锁只在 REPEATABLE READ + 索引条件范围查询 + FOR UPDATE 或隐式锁定下生效
- 不使用
FOR UPDATE
时,一般是一致性读,只靠 undo log,不加锁
🧠 总结:
事务隔离级别问题对比
- READ UNCOMMITTED: 有脏读,不推荐使用
- READ COMMITTED: 无脏读,有不可重复读,Oracle默认
- REPEATABLE READ: 无脏读、可重复读、InnoDB用间隙锁避免幻读(MySQL默认)
- SERIALIZABLE: 全部加锁,开销大,极少使用
InnoDB 实现关键:MVCC
- undo log:记录旧版本数据
- read view:事务可见性判断依据
- 间隙锁:锁定范围,阻止幻读(只在RR可重复读下有效)
https://github.com/0voice