MySQL数据库中MVCC的底层原理
MVCC(多版本并发控制)是 MySQL 的 InnoDB 存储引擎实现高并发读写的核心技术。它的核心目标是实现**“读不加锁,读写互不阻塞”**,让读操作永远不阻塞写操作,写操作也永远不阻塞读操作,从而极大提升数据库的并发性能。
我们可以把 MVCC 比作“图书馆借书”:
- 传统加锁机制:你要借书(读)时,管理员必须等你还书,才能修改这本书的内容(写),两者互相阻塞。
- MVCC 机制:你要借书时,管理员直接复印一份当时的“副本”给你。与此同时,管理员可以随意修改原书。你手里的副本不会受原书修改的影响。
🛠️ MVCC 的底层三大核心组件
MVCC 的实现依赖于 InnoDB 的三大底层设计,它们共同协作完成了多版本数据的管理:
行记录的隐藏字段InnoDB 的每一行数据,除了我们自己定义的字段(如
id,name),还会自动添加 3 个隐藏字段:DB_TRX_ID(事务ID):记录最后一次插入或更新该行数据的事务 ID。DB_ROLL_PTR(回滚指针):指向该行数据在 Undo Log 中的上一个历史版本。DB_ROW_ID(行ID):如果没有设置主键,InnoDB 自动生成的隐藏主键(非 MVCC 核心,仅作了解)。
Undo Log(回滚日志)与版本链当对某行数据进行 UPDATE 或 DELETE 操作时,InnoDB不会直接覆盖原数据,而是:
- 把旧版本的数据写入 Undo Log。
- 更新当前行的
DB_TRX_ID为当前事务 ID。 - 更新当前行的
DB_ROLL_PTR指向 Undo Log 中的旧版本。
经过多次修改后,这些历史版本通过
DB_ROLL_PTR串联起来,就形成了一条单向的“版本链”。Read View(读视图)Read View 是事务在进行“快照读”(普通 SELECT)时生成的一个“可见性判断规则”。它主要包含 4 个核心信息:
m_ids:生成 Read View 时,当前系统中所有活跃(未提交)的事务 ID 列表。min_trx_id:活跃事务中最小的事务 ID。max_trx_id:系统分配给下一个事务的 ID(当前最大事务 ID + 1)。creator_trx_id:当前事务自己的 ID。
🔍 版本可见性判断规则
当一个事务去读取某行数据时,它会拿着自己的 Read View,从版本链的最新版本开始,按照以下规则逐条判断该版本是否“可见”:
- 如果版本的
DB_TRX_ID==creator_trx_id:说明是当前事务自己修改的数据,可见。 - 如果版本的
DB_TRX_ID<min_trx_id:说明修改该行的事务在生成 Read View 之前就已经提交了,可见。 - 如果版本的
DB_TRX_ID>=max_trx_id:说明修改该行的事务在当前事务之后才启动,不可见。 - 如果版本的
DB_TRX_ID在min_trx_id和max_trx_id之间:- 如果
DB_TRX_ID在m_ids列表中:说明该事务还未提交,不可见。 - 如果
DB_TRX_ID不在m_ids列表中:说明该事务已经提交,可见。
- 如果
如果当前版本不可见,就顺着DB_ROLL_PTR找上一个历史版本继续判断,直到找到可见版本或遍历完整个版本链。
💡 实战例子:RC 与 RR 隔离级别的区别
MVCC 在不同隔离级别下的表现,核心区别在于Read View 的生成时机。
场景设定:有一张users表,初始数据id=1, name="Alice"。
例子 1:读已提交(RC)级别
规则:每次执行 SELECT 语句时,都会生成一个新的 Read View。
| 时间 | 事务 A | 事务 B | MVCC 行为解析 |
|---|---|---|---|
| t0 | START TRANSACTION; | ||
| t1 | SELECT name FROM users WHERE id=1; | 生成 Read View 1,读到"Alice"。 | |
| t2 | START TRANSACTION; UPDATE users SET name="Bob" WHERE id=1; COMMIT; | B 修改并提交,生成新版本 "Bob",旧版本 "Alice" 存入 Undo Log。 | |
| t3 | SELECT name FROM users WHERE id=1; | 生成新的 Read View 2,根据规则能看到 B 提交的 "Bob",读到"Bob"。 |
结果:事务 A 在同一个事务内,两次读到了不同的数据,这就是**“不可重复读”**。
例子 2:可重复读(RR)级别(MySQL 默认)
规则:在事务第一次执行 SELECT 时生成 Read View,整个事务期间复用这个 Read View。
| 时间 | 事务 A | 事务 B | MVCC 行为解析 |
|---|---|---|---|
| t0 | START TRANSACTION; | ||
| t1 | SELECT name FROM users WHERE id=1; | 生成 Read View 1,读到"Alice"。 | |
| t2 | START TRANSACTION; UPDATE users SET name="Bob" WHERE id=1; COMMIT; | B 修改并提交,生成新版本 "Bob"。 | |
| t3 | SELECT name FROM users WHERE id=1; | 复用 Read View 1。根据 Read View 1 的规则,B 的事务 ID 属于“未提交活跃列表”,因此 "Bob" 版本不可见,顺着版本链读到旧版"Alice"。 |
结果:事务 A 两次读到的都是 "Alice",保证了**“可重复读”**。
📌 补充说明:快照读与当前读
MVCC 仅对快照读(Snapshot Read)生效,也就是普通的SELECT语句。
对于当前读(Current Read),例如SELECT ... FOR UPDATE、INSERT、UPDATE、DELETE,数据库会跳过 MVCC,直接读取数据的最新版本,并且必须加锁(排他锁或共享锁)来保证数据修改的原子性和一致性。
