MVCC 多版本并发控制 详解
目录
为什么需要MVCC?
MVCC的核心组件
1. 隐藏字段
2. undo log(回滚日志)
3. Read View(读视图)
MVCC的可见性判断规则
不同隔离级别的MVCC表现
示例:MVCC如何工作?
MVCC的优势
注意事项
MVCC(Multi-Version Concurrency Control,多版本并发控制)是数据库中用于处理并发访问的核心机制,其核心目标是在读写操作不冲突的前提下,保证事务的隔离性,同时提升数据库的并发性能。
为什么需要MVCC?
在并发场景中,传统的锁机制(如读锁、写锁)会导致读写操作互相阻塞:
- 读操作持有读锁时,写操作需要等待读锁释放才能修改数据;
- 写操作持有写锁时,读操作需要等待写锁释放才能读取数据。
这种阻塞会严重影响数据库的并发效率。而MVCC通过保留数据的历史版本,让读操作可以直接读取历史版本,无需等待写操作释放锁;同时写操作也无需阻塞读操作,从而大幅提升并发性能。
MVCC的核心组件
MVCC的实现依赖三个关键部分:隐藏字段、undo log 和 Read View,三者协同工作形成数据的多版本管理和可见性判断机制。
1. 隐藏字段
InnoDB中,每张表的每行记录除了用户定义的字段外,还隐含三个隐藏字段(用于版本管理):
- DB_TRX_ID:记录最后一次修改该记录的事务ID(6字节)。事务开启时,InnoDB会为其分配一个唯一的递增事务ID。
- DB_ROLL_PTR:回滚指针(7字节),指向当前记录的上一个版本(存储在undo log中),形成“版本链”。
- DB_ROW_ID:行ID(6字节,可选)。如果表没有主键或唯一索引,InnoDB会用该字段作为聚簇索引的键。
2. undo log(回滚日志)
当事务修改数据时,InnoDB会生成undo log,记录数据修改前的状态(用于回滚或供读操作访问历史版本)。
- 例如:事务T1将一条记录的
name
从“张三”改为“李四”,则undo log会记录“name=张三”的旧版本,同时当前记录的DB_ROLL_PTR
指向这条undo log。 - 多次修改后,undo log会通过
DB_ROLL_PTR
串联成版本链(最新版本在表中,旧版本在undo log中)。
版本链示例:
当前记录(最新版本)
DB_TRX_ID = 100(最后修改的事务ID)
DB_ROLL_PTR → undo log 1(版本1:name=张三,DB_TRX_ID=50)↓undo log 2(版本2:name=王五,DB_TRX_ID=30)
3. Read View(读视图)
Read View是事务在读取数据时生成的“快照”,用于判断哪个版本的记录对当前事务可见。它包含4个核心属性:
- m_ids:当前活跃的事务ID列表(即尚未提交的事务ID)。
- min_trx_id:m_ids中的最小事务ID(当前活跃事务的最小ID)。
- max_trx_id:下一个将要分配的事务ID(并非m_ids中的最大值,而是全局事务ID的下一个值)。
- creator_trx_id:生成该Read View的事务ID。
MVCC的可见性判断规则
当事务读取一条记录时,会从版本链的最新版本开始,依次通过Read View判断每个版本的DB_TRX_ID
是否可见。规则如下:
- 若当前版本的
DB_TRX_ID
=creator_trx_id
:
说明该版本是当前事务自己修改的,可见。 - 若
DB_TRX_ID
<min_trx_id
:
说明修改该版本的事务在当前Read View生成前已提交,可见。 - 若
DB_TRX_ID
>max_trx_id
:
说明修改该版本的事务在当前Read View生成后才开启,不可见(需通过DB_ROLL_PTR
找前一个版本)。 - 若
min_trx_id
≤DB_TRX_ID
≤max_trx_id
:
-
- 若
DB_TRX_ID
在m_ids
中(即事务仍活跃):不可见,找前一个版本; - 若
DB_TRX_ID
不在m_ids
中(即事务已提交):可见。
- 若
不同隔离级别的MVCC表现
MVCC的行为会因事务隔离级别不同而变化,主要影响Read View的生成时机:
隔离级别 | Read View生成时机 | 效果 |
读已提交(RC) | 每次查询时生成新的Read View | 可能读取到其他事务已提交的新数据(解决脏读,但可能出现不可重复读)。 |
可重复读(RR) | 事务第一次查询时生成Read View,全程复用 | 多次查询结果一致(解决不可重复读,InnoDB默认隔离级别)。 |
示例:MVCC如何工作?
假设初始数据:id=1, name=张三
(DB_TRX_ID=0
,表示未被事务修改)。
- 事务T1(ID=10)修改数据:
-
- 将
name
改为“李四”,生成undo log(记录旧值“张三”)。 - 当前记录
DB_TRX_ID=10
,DB_ROLL_PTR
指向undo log(旧版本)。
- 将
- 事务T2(ID=20)读取数据:
-
- T2生成Read View:
m_ids=[10]
(T1未提交),min_trx_id=10
,max_trx_id=30
,creator_trx_id=20
。 - 检查最新版本
DB_TRX_ID=10
:在m_ids
中(T1活跃),不可见。 - 通过
DB_ROLL_PTR
找到旧版本(DB_TRX_ID=0
):0 < min_trx_id=10
,可见,因此T2读取到“张三”。
- T2生成Read View:
- T1提交后,T2再次查询(RC级别):
-
- RC级别会生成新Read View:
m_ids=[]
(T1已提交),min_trx_id=30
,max_trx_id=30
。 - 最新版本
DB_TRX_ID=10 < min_trx_id=30
,可见,因此T2读取到“李四”(不可重复读)。
- RC级别会生成新Read View:
- 若T2是RR级别:
-
- 全程复用第一次生成的Read View(
m_ids=[10]
),即使T1提交,T2仍读取到“张三”(可重复读)。
- 全程复用第一次生成的Read View(
MVCC的优势
- 读写不冲突:读操作无需加锁,直接读取历史版本;写操作仅锁定当前版本,不阻塞读。
- 提升并发性能:避免了传统锁机制的阻塞,支持高并发读写。
- 简化隔离级别实现:通过Read View的生成时机,轻松实现RC和RR隔离级别。
注意事项
- MVCC仅解决读-写冲突,写-写冲突仍需行锁(如InnoDB的Record Lock)解决。
- undo log会占用空间,InnoDB的purge线程会定期清理“所有Read View都无法访问”的旧版本undo log。
总结:MVCC通过版本链、undo log和Read View的协同,实现了“读不加锁、读写并行”,是InnoDB高效支持并发事务的核心机制。