MySQL MVCC实现原理详解
MySQL MVCC实现原理详解
一、MVCC基本概念
MVCC(Multi-Version Concurrency Control)是多版本并发控制技术,它是MySQL InnoDB引擎实现事务隔离性的核心机制。
核心特点:
- 读不加锁,读写不冲突
- 通过数据版本链实现并发控制
- 不同事务看到的数据版本可能不同
二、MVCC核心组件
2.1 隐藏字段
InnoDB每行记录包含3个隐藏字段:
DB_TRX_ID
:最近修改的事务IDDB_ROLL_PTR
:回滚指针,指向undo日志DB_ROW_ID
:行ID(当没有主键时自动生成)
2.2 undo log
- 存储数据修改前的状态
- 用于事务回滚和MVCC版本链
- 分为insert undo log和update undo log
undo log版本链示例:
让我们通过一个具体的例子来说明undo log如何记录数据的变更历史:
- 初始数据
id | age | name | DB_TRX_ID | DB_ROLL_PTR | DB_ROW_ID |
---|---|---|---|---|---|
30 | 30 | A30 | 1 | null | 1 |
- 事务2修改age=25
当前记录:
id | age | name | DB_TRX_ID | DB_ROLL_PTR | DB_ROW_ID |
---|---|---|---|---|---|
30 | 25 | A30 | 2 | 0x000001 | 1 |
undo log (0x000001):
id | age | name | DB_TRX_ID | DB_ROLL_PTR | DB_ROW_ID |
---|---|---|---|---|---|
30 | 30 | A30 | 1 | null | 1 |
- 事务3修改name=A30_new
当前记录:
id | age | name | DB_TRX_ID | DB_ROLL_PTR | DB_ROW_ID |
---|---|---|---|---|---|
30 | 25 | A30_new | 3 | 0x000002 | 1 |
undo log (0x000002):
id | age | name | DB_TRX_ID | DB_ROLL_PTR | DB_ROW_ID |
---|---|---|---|---|---|
30 | 25 | A30 | 2 | 0x000001 | 1 |
版本链结构(undo log):
说明:
- 粉色框:当前最新记录
- 蓝色框:第一个历史版本(undo log 1)
- 绿色框:最早的历史版本(undo log 2)
- 箭头:表示DB_ROLL_PTR的指向,通过回滚指针串联成版本链
- 每个版本都包含完整的行记录信息,包括所有隐藏字段
不同事务或相同事务对同一条记录进行修改,会导致该记录的undolog生成一条记录版本链表,链表的头部是最新的旧记录,链表尾部是最早的旧记录。
通过这个版本链,不同事务根据其可见性规则可以看到不同的历史版本:
- 事务4在RR隔离级别下,如果在事务2和事务3执行期间开始,将看到最早的版本(age=30, name=A30)
- 事务4在RC隔离级别下,如果在事务3提交后查询,将看到最新的版本(age=25, name=A30_new)
2.3 ReadView
ReadView是事务在快照读时产生的读视图,包含:
trx_ids
:当前活跃事务ID列表up_limit_id
:最小活跃事务IDlow_limit_id
:预分配的下一个事务IDcreator_trx_id
:创建该ReadView的事务ID
三、MVCC工作流程
3.1 版本链访问规则
flowchart LR
A[最新记录] -->|DB_ROLL_PTR| B[版本1]
B -->|DB_ROLL_PTR| C[版本2]
C -->|DB_ROLL_PTR| D[...]
- 从最新记录开始,通过
DB_ROLL_PTR
访问历史版本 - 比较事务ID与ReadView中的限制条件
- 找到对当前事务可见的版本
具体示例:
假设有一张用户表,初始数据:
id | age | name | DB_TRX_ID | DB_ROLL_PTR | DB_ROW_ID |
---|---|---|---|---|---|
30 | 30 | A30 | 1 | null | 0x000001 |
现在执行以下事务操作:
-- 事务2:修改age为3
BEGIN;
UPDATE users SET age = 3 WHERE id = 30;
COMMIT;
-- 事务3:修改name为'A30_v2'
BEGIN;
UPDATE users SET name = 'A30_v2' WHERE id = 30;
COMMIT;
执行后的版本链如下:
最新数据记录:
id | age | name | DB_TRX_ID | DB_ROLL_PTR | DB_ROW_ID |
---|---|---|---|---|---|
30 | 3 | A30_v2 | 3 | 0x000002 | 0x000001 |
undo log版本1(事务3之前):
id | age | name | DB_TRX_ID | DB_ROLL_PTR | DB_ROW_ID |
---|---|---|---|---|---|
30 | 3 | A30 | 2 | 0x000003 | 0x000001 |
undo log版本2(事务2之前):
id | age | name | DB_TRX_ID | DB_ROLL_PTR | DB_ROW_ID |
---|---|---|---|---|---|
30 | 30 | A30 | 1 | null | 0x000001 |
当不同事务读取该记录时,会根据ReadView规则沿着版本链查找可见版本。
3.2 可见性判断算法
判断条件与结果:
条件判断 | 结果 | 说明 |
---|---|---|
DB_TRX_ID == creator_trx_id | 可见 | 当前事务的修改 |
DB_TRX_ID < up_limit_id | 可见 | 已提交的历史事务 |
DB_TRX_ID >= low_limit_id | 不可见 | 未开始的新事务 |
up_limit_id <= DB_TRX_ID < low_limit_id | - | 需进一步判断 |
活跃事务判断:
当 up_limit_id <= DB_TRX_ID < low_limit_id 时:
- 在
trx_ids
列表中:事务未提交,不可见 - 不在
trx_ids
列表中:事务已提交,可见
具体示例:
假设当前有三个事务,状态如下:
事务ID | 状态 |
---|---|
1 | 已提交 |
2 | 已提交 |
3 | 活跃中 |
4 | 未开始 |
此时,事务5开始并创建ReadView:
ReadView信息:
- trx_ids = [3](当前活跃事务列表)
- up_limit_id = 3(最小活跃事务ID)
- low_limit_id = 6(下一个将被分配的事务ID)
- creator_trx_id = 5(创建ReadView的事务ID)
对于前面例子中的版本链,事务5的可见性判断过程:
-
检查最新记录(DB_TRX_ID=3):
- 3 == 5?否,不满足条件1
- 3 < 3?否,不满足条件2
- 3 >= 6?否,不满足条件3
- 3 <= 3 < 6 且 3在trx_ids中?是,满足条件4第一项,不可见
- 继续沿版本链查找
-
检查undo log版本1(DB_TRX_ID=2):
- 2 == 5?否,不满足条件1
- 2 < 3?是,满足条件2,可见
- 找到可见版本,停止查找
因此,事务5在RR隔离级别下看到的数据是:
id | age | name | DB_TRX_ID | DB_ROLL_PTR | DB_ROW_ID |
---|---|---|---|---|---|
30 | 3 | A30 | 2 | 0x000003 | 0x000001 |
这就是MVCC如何通过版本链和ReadView实现事务隔离性的具体过程。
四、不同隔离级别的实现
4.1 读已提交(RC)
- 每次读取都生成新的ReadView
- 能看到其他事务已提交的修改
RC隔离级别示例:
-- 事务A:RC隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
-- T1时刻:第一次查询
SELECT * FROM users WHERE id = 30; -- 生成ReadView1
-- 此时看到:id=30, age=3, name=A30
-- 此时事务B修改并提交:
-- UPDATE users SET name = 'A30_v3' WHERE id = 30;
-- COMMIT;
-- T2时刻:第二次查询
SELECT * FROM users WHERE id = 30; -- 生成ReadView2
-- 此时看到:id=30, age=3, name=A30_v3(能看到事务B的修改)
COMMIT;
在RC隔离级别下,每次SELECT都会生成新的ReadView,因此能够看到其他已提交事务的修改。
4.2 可重复读(RR)
- 只在第一次读取时生成ReadView
- 后续读取复用同一个ReadView
- 解决不可重复读问题
RR隔离级别示例:
-- 事务A:RR隔离级别(MySQL默认)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
-- T1时刻:第一次查询
SELECT * FROM users WHERE id = 30; -- 生成ReadView1
-- 此时看到:id=30, age=3, name=A30
-- 此时事务B修改并提交:
-- UPDATE users SET name = 'A30_v3' WHERE id = 30;
-- COMMIT;
-- T2时刻:第二次查询
SELECT * FROM users WHERE id = 30; -- 复用ReadView1
-- 此时看到:id=30, age=3, name=A30(看不到事务B的修改)
COMMIT;
在RR隔离级别下,事务内的所有查询都复用第一次生成的ReadView,因此无法看到其他事务已提交的修改,实现了可重复读。
五、MVCC与锁的协同
5.1 当前读与快照读
- 快照读:普通SELECT,使用MVCC
- 当前读:加锁的SELECT/UPDATE/DELETE,使用记录锁
快照读与当前读对比:
特性 | 快照读 | 当前读 |
---|---|---|
语句示例 | SELECT * FROM table | SELECT * FROM table FOR UPDATE |
锁机制 | 无锁 | 记录锁/间隙锁 |
读取内容 | 历史版本(MVCC) | 最新提交版本 |
并发性能 | 高 | 低 |
示例:
-- 事务A:使用快照读
BEGIN;
SELECT * FROM users WHERE id = 30; -- 使用MVCC机制,读取符合ReadView规则的版本
-- 事务B:使用当前读
BEGIN;
SELECT * FROM users WHERE id = 30 FOR UPDATE; -- 读取最新版本并加锁
UPDATE users SET age = 35 WHERE id = 30; -- 修改数据
COMMIT;
5.2 解决幻读问题
- RR级别下,MVCC+间隙锁防止幻读
- 通过Next-Key Lock锁定记录和间隙
幻读问题示例:
-- 事务A:第一次查询
BEGIN;
SELECT * FROM users WHERE age BETWEEN 20 AND 40; -- 返回1条记录
-- 事务B:插入新记录
-- BEGIN;
-- INSERT INTO users(id, age, name) VALUES(31, 25, 'A31');
-- COMMIT;
-- 事务A:第二次查询(快照读,使用MVCC)
SELECT * FROM users WHERE age BETWEEN 20 AND 40; -- 仍然返回1条记录(MVCC机制)
-- 事务A:使用当前读
SELECT * FROM users WHERE age BETWEEN 20 AND 40 FOR UPDATE; -- 返回2条记录(当前读)
-- 此时发现幻读问题
COMMIT;
解决方案:
在RR隔离级别下,InnoDB通过Next-Key Lock(记录锁+间隙锁)解决当前读的幻读问题:
-- 事务A:使用当前读并加锁
BEGIN;
SELECT * FROM users WHERE age BETWEEN 20 AND 40 FOR UPDATE; -- 加Next-Key Lock
-- 事务B:尝试插入(会被阻塞)
-- BEGIN;
-- INSERT INTO users(id, age, name) VALUES(31, 25, 'A31'); -- 被间隙锁阻塞
-- COMMIT;
-- 事务A:再次查询
SELECT * FROM users WHERE age BETWEEN 20 AND 40 FOR UPDATE; -- 结果一致,无幻读
COMMIT;
这样,MVCC处理快照读,Next-Key Lock处理当前读,共同保证了事务的隔离性。
六、总结
小结
1.MVCC允许多个事务同时读取同一行数据,而不会彼此阻塞,每个事务看到的数据版本是该事务开始时的数据版本。这意味着,如果其他事务在此期间修改了数据,正在运行的事务仍然看到的是它开始时的数据状态,从而实现了非阻塞读操作。
2.对于「读提交」和「可重复读」隔离级别的事务来说,它们是通过 Read View 来实现的,它们的区别在于创建 Read View 的时机不同,大家可以把 Read View 理解成一个数据快照,就像相机拍照那样,定格某一时刻的风景。
3.「读提交」隔离级别是在「每个select语句执行前」都会重新生成一个 Read View;
「可重复读」隔离级别是执行第一条select时,生成一个 Read View,然后整个事务期间都在用这个 Read View
关键知识点
- MVCC核心组件:隐藏字段、undo日志、ReadView
- 快照的读取依据:通过事务ID与ReadView比较确定可见版本
- 隔离级别实现:RC每次生成新ReadView,RR复用ReadView
- 与锁的关系:MVCC处理快照读,锁处理当前读
- 读已提交(RC):每次执行SELECT语句时都会重新生成一个Read View,因此能看到其他事务已提交的最新修改
- 可重复读(RR):只在事务中第一次执行SELECT时生成Read View,后续查询都复用同一个Read View
深入解析
- 版本链构建:通过
DB_ROLL_PTR
指针连接undo日志形成版本链 - ReadView生成时机:RC在每次查询时生成,RR在事务首次查询时生成
- 幻读解决方案:RR级别下MVCC+间隙锁共同作用
- 性能优化:MVCC避免了读锁,大幅提升并发性能
- RC级别的Read View生成:
- 每次SELECT都会获取最新的活跃事务列表
- 能看到其他事务已提交的修改
- 可能导致不可重复读问题
- RR级别的Read View复用:
- 事务开始时生成Read View后固定不变
- 确保事务内多次读取数据一致性
- 解决了不可重复读问题