目录
🏗️ 二、InnoDB中MVCC的实现:隐藏字段与Read View
前言:一个“神奇”的现象
你是否遇到过这种情况?
-- 事务1(长时间运行)
START TRANSACTION;
SELECT SUM(balance) FROM accounts; -- 开始统计总余额 (耗时10秒)
-- ... 10秒后统计完成
-- 与此同时,事务2
UPDATE accounts SET balance = balance + 100 WHERE id = 1;
COMMIT; -- 立即完成
问题:事务2在事务1统计过程中修改了数据,事务1最终统计的总余额会包含这100元吗?
答案:不会! 事务1看到的仍然是修改前的数据。
更神奇的是:事务1的
SELECT语句并没有阻塞事务2的UPDATE操作!
👉 这就是 MVCC(Multi-Version Concurrency Control,多版本并发控制) 的魔力!它让读操作(SELECT)无需加锁,也能保证数据的一致性视图,实现了读写不冲突,极大地提升了并发性能。
🧩 一、什么是MVCC?核心思想
🔍 传统锁的痛点
在没有MVCC的时代(如早期的数据库或MyISAM引擎):
SELECT为了保证一致性,可能需要加S锁。UPDATE需要加X锁。- 结果:读写相互阻塞!一个长查询会阻塞所有写入,性能极差。
💡 MVCC的解决方案
MVCC的核心思想是:不直接覆盖旧数据,而是保存数据的多个版本。
- 每次更新数据时,不直接修改原记录,而是创建一个新版本。
- 每个事务在启动时,会看到一个基于当时状态的“快照”(Snapshot)。
- 事务只能看到在它启动之前已经提交的版本,以及它自己创建的版本。
- 这样,读操作直接从“快照”中读取,完全不需要加锁,自然不会阻塞写操作。
✅ 类比:就像Git版本控制。你
git checkout到某个历史提交(快照),你可以查看代码,但不会影响其他人正在git commit新代码。
🏗️ 二、InnoDB中MVCC的实现:隐藏字段与Read View
InnoDB通过 隐藏字段 和 Read View 机制来实现MVCC。
🔹 1. 隐藏字段(Hidden Columns)
InnoDB自动为每行数据添加几个隐藏字段:
| 字段 | 说明 |
|---|---|
DB_TRX_ID | 事务ID:记录最后一次修改(INSERT/UPDATE)该行的事务ID。 |
DB_ROLL_PTR | 回滚指针:指向undo log中的一条记录,该记录包含了该行的旧版本信息。 |
DB_ROW_ID | 行ID:如果表没有主键,InnoDB会创建一个隐式的行ID。 |
📌 关键:
DB_TRX_ID和DB_ROLL_PTR是MVCC的基石。
🔹 2. Undo Log(回滚日志)
- 当一条记录被更新时,InnoDB会把旧版本的数据(修改前的值)写入 Undo Log。
DB_ROLL_PTR就指向了这条Undo Log记录。- 多次更新会形成一条版本链(Version Chain),从最新版本通过
DB_ROLL_PTR可以一直追溯到最老的版本。
版本链(从新到旧):
[最新版: balance=300, DB_TRX_ID=102, DB_ROLL_PTR→] --> [旧版: balance=200, DB_TRX_ID=101, DB_ROLL_PTR→] --> [更旧版: balance=100, DB_TRX_ID=100, DB_ROLL_PTR=NULL]
🔹 3. Read View(读视图)
这是MVCC的核心!当一个事务(假设ID为trx_id)执行 普通SELECT(快照读)时,InnoDB会为它创建一个 Read View。
Read View 包含了以下关键信息:
| 信息 | 说明 |
|---|---|
m_ids | 在创建Read View时,所有活跃的(未提交的)事务ID列表。 |
min_trx_id | m_ids 中的最小值。 |
max_trx_id | 创建Read View时,系统应该分配给下一个事务的ID(即当前最大事务ID+1)。 |
creator_trx_id | 创建这个Read View的事务自身的ID。 |
🔄 三、MVCC工作流程:如何判断数据可见性?
当一个事务通过SELECT访问某一行数据时,InnoDB会拿着该行的DB_TRX_ID和当前事务的Read View,按照以下规则判断该行版本是否可见:
🔍 可见性判断算法
-
如果
DB_TRX_ID < min_trx_id:- 说明修改该行的事务在创建Read View之前就已经提交了。
- ✅ 可见(是“过去”的已提交数据)。
-
如果
DB_TRX_ID >= max_trx_id:- 说明修改该行的事务在创建Read View之后才开始。
- ❌ 不可见(是“未来”的数据)。
-
如果
min_trx_id <= DB_TRX_ID < max_trx_id:- 需要检查
DB_TRX_ID是否在m_ids列表中:- 如果在
m_ids中:说明修改该行的事务在创建Read View时还未提交。 - ❌ 不可见。
- 如果不在
m_ids中:说明修改该行的事务在创建Read View时已经提交了。 - ✅ 可见。
- 如果在
- 需要检查
-
如果
DB_TRX_ID == creator_trx_id:- 说明这行数据就是当前事务自己修改的。
- ✅ 可见(自己做的修改当然要看到)。
📌 简单记忆:一个版本对于当前事务是可见的,当且仅当:
- 它是由当前事务自身产生的,或者
- 它是由一个在当前事务创建Read View之前就已经提交的事务产生的。
🧪 四、实战案例:揭秘“非阻塞读”
🎯 场景重现
-- 假设当前系统事务ID分配到 100
-- 账户A: id=1, balance=200, DB_TRX_ID=99 (由事务99插入)
-- 事务1 (ID=101) 启动
START TRANSACTION; -- 创建Read View
-- Read View: m_ids=[], min_trx_id=null, max_trx_id=101, creator_trx_id=101
-- (此时无活跃事务)
-- 事务2 (ID=102) 启动
START TRANSACTION;
UPDATE accounts SET balance = 300 WHERE id = 1;
-- 生成新版本: balance=300, DB_TRX_ID=102, DB_ROLL_PTR指向旧版本(balance=200)
COMMIT; -- 事务102提交
-- 事务1 继续执行
SELECT balance FROM accounts WHERE id = 1;
-- 普通SELECT -> 快照读,使用事务1创建的Read View
-- 检查最新版本 (DB_TRX_ID=102):
-- 102 >= max_trx_id(101)? 是 -> 规则2 -> ❌ 不可见!
-- 回滚到旧版本 (balance=200, DB_TRX_ID=99):
-- 99 < min_trx_id? min_trx_id不存在,视为无穷大 -> 规则1 -> ✅ 可见!
-- 结果:返回 balance=200
-- 事务1 提交
COMMIT;
结果:事务1读到了200,而事务2的更新300对它不可见,实现了可重复读(REPEATABLE READ) 隔离级别。
关键:事务1的SELECT没有阻塞事务2的UPDATE,因为SELECT是快照读,不加锁!
🔍 五、快照读 vs 当前读
MVCC主要影响快照读。还有一种读叫当前读。
| 类型 | SQL示例 | 是否使用MVCC | 是否加锁 |
|---|---|---|---|
| 快照读 (Snapshot Read) | SELECT * FROM t; | ✅ 是 | ❌ 否 |
| 当前读 (Current Read) | SELECT ... LOCK IN SHARE MODE<br>SELECT ... FOR UPDATE<br>UPDATE ...<br>DELETE ...<br>INSERT ... | ❌ 否 | ✅ 是(加S锁或X锁) |
⚠️ 重要:
UPDATE和DELETE语句虽然包含SELECT,但它们是当前读!它们会读取最新的已提交数据,并加锁,以防止更新丢失。
🎯 总结:MVCC核心要点
| 概念 | 作用 |
|---|---|
| 多版本 | 通过Undo Log保存数据历史版本。 |
| 隐藏字段 | DB_TRX_ID 标记版本创建者,DB_ROLL_PTR 链接版本。 |
| Read View | 事务的“快照”,定义了可见性规则。 |
| 可见性算法 | 基于DB_TRX_ID和Read View判断版本是否可见。 |
| 快照读 | 普通SELECT,无锁,读历史版本。 |
| 当前读 | FOR UPDATE等,加锁,读最新版本。 |
| 优势 | 读写不冲突,高并发性能。 |
| 隔离级别 | RR(可重复读)和RC(读已提交)都基于MVCC实现。 |
深入理解MySQL的MVCC与非阻塞读
576

被折叠的 条评论
为什么被折叠?



