一、什么是MVCC?
MVCC(Multi-Version Concurrency Control,多版本并发控制)是数据库中用于解决并发访问问题的一种机制。它通过为数据维护多个版本,让读写操作在不同版本上独立进行,从而避免了传统锁机制中读写冲突导致的性能损耗(如读操作不会阻塞写操作,写操作也不会阻塞读操作)。
MVCC的核心目标是:在保证事务隔离性的前提下,提高数据库的并发性能。它广泛应用于支持行级锁的数据库(如MySQL的InnoDB、PostgreSQL等),尤其在读已提交(Read Committed) 和可重复读(Repeatable Read) 隔离级别中发挥关键作用。
二、MVCC的实现原理
MVCC的实现依赖于三个核心组件:数据的隐藏列 、回滚日志(Undo Log) 和ReadView(读视图)。三者协同工作,使得事务能够访问到符合其隔离级别的数据版本。
1. 数据的隐藏列
InnoDB存储引擎会为表中的每一行数据添加三个隐藏列,用于记录版本信息和事务相关数据:
- DB_TRX_ID :记录最后一次修改该行数据的事务ID(6字节)。每次事务对行数据执行
INSERT
/UPDATE
/DELETE
操作时,都会将当前事务的ID写入该列。 - DB_ROLL_PTR:回滚指针(7字节),指向该行数据的回滚日志(Undo Log),通过该指针可以找到数据的上一个版本。
- DB_ROW_ID:行ID(6字节),当表没有主键或唯一索引时,InnoDB会用该列生成聚簇索引,确保每行数据有唯一标识。
2. 回滚日志(Undo Log)
回滚日志是MVCC实现多版本的基础,用于保存数据被修改前的旧版本。
- 作用 :
- 当事务需要回滚时,通过Undo Log恢复数据到修改前的状态(支持事务的原子性);
- 为MVCC提供数据的历史版本,供其他事务读取(支持并发读写)。
- 生成时机 :
当事务执行INSERT
/UPDATE
/DELETE
时,InnoDB会先将数据的旧版本写入Undo Log,再修改实际数据。INSERT
:Undo Log记录新插入的行信息,事务回滚时直接删除该行;UPDATE
/DELETE
:Undo Log记录修改前的行数据,事务回滚时通过回滚指针恢复旧版本。
- 版本链 :
多次修改同一行数据时,Undo Log会形成一条"版本链":每次修改后,新数据的DB_ROLL_PTR
指向旧版本的Undo Log,旧版本的DB_ROLL_PTR
再指向更早的版本,直至最初版本。
回滚日志为什么在update和delete时不会被立即删除,而insert之后可以立即删除?
update和delete之后还会被mvcc或者是快照读用到**,这里举个mvcc还需要回滚日志的例子**
MVCC(多版本并发控制)通过回滚日志来实现数据多版本管理,以解决并发事务中的读 - 写冲突等问题,在可重复读隔离级别下体现得较为明显。以下是一个基于MySQL的InnoDB存储引擎的例子:
- 假设当前有三个事务,事务ID分别为100、200、300。
- 事务200先执行了一条SQL语句:
UPDATE user SET name = '小王3号' WHERE id = 1
,此时数据库会将修改前的记录相关信息写入回滚日志,然后修改实际数据,并将数据的trx_id
更新为200。 - 接着事务200提交。
- 之后事务100开始执行查询语句,此时会生成一个ReadView,视图数组为
(100, 300)
,min_id
为100,max_id
为300。 - 然后事务300执行了一条SQL语句:
UPDATE user SET name = '小王4号' WHERE id = 1
,数据库同样会将修改前的记录(即name = '小王3号'
)写入回滚日志,再修改实际数据,将trx_id
更新为300。 - 此时事务100再次执行查询语句,根据MVCC的规则:
- 若
row的trx_id
落于min_id
和max_id
之间,且不在视图数组中,说明这个版本是已经提交的事务生成的,是可见的。 - 事务300的
trx_id
为300,在min_id
和max_id
之间,但在视图数组中,所以其修改后的结果对事务100不可见。 - 事务200的
trx_id
为200,也在min_id
和max_id
之间,不在视图数组中,所以事务100会根据回滚日志找到事务200修改前的记录,查询结果为name = '小王3号'
。
- 若
通过这个例子可以看到,MVCC利用回滚日志构建数据的旧版本,配合ReadView机制,让事务100在事务300已修改数据并提交的情况下,仍然能查询到符合可重复读规则的结果,体现了回滚日志在MVCC中的重要作用。
3. ReadView(读视图)
ReadView是事务在读取数据时生成的一个"快照",用于判断当前事务能看到哪些版本的数据。它本质上是一组用于过滤数据版本的规则,包含四个核心参数:
- m_ids:当前活跃(未提交)的事务ID列表。
- min_trx_id:活跃事务中最小的事务ID。
- max_trx_id:系统为下一个事务分配的ID(即当前最大事务ID+1)。
- creator_trx_id:生成该ReadView的事务ID。
4. 版本可见性判断规则
事务读取数据时,会根据ReadView的参数,对数据的DB_TRX_ID
(最后修改事务ID)进行判断,决定是否可见:
- 若
DB_TRX_ID == creator_trx_id
:数据是当前事务自己修改的,可见。 - 若
DB_TRX_ID < min_trx_id
:修改该数据的事务在当前事务启动前已提交,可见。 - 若
DB_TRX_ID > max_trx_id
:修改该数据的事务在当前事务启动后才开始,不可见(需通过回滚指针找更早版本)。 - 若
min_trx_id ≤ DB_TRX_ID ≤ max_trx_id
:- 若
DB_TRX_ID
在m_ids
中(事务仍活跃):不可见(需找更早版本); - 若
DB_TRX_ID
不在m_ids
中(事务已提交):可见。
- 若
如果当前版本不可见,事务会通过DB_ROLL_PTR
沿着Undo Log的版本链向上查找,直到找到符合规则的可见版本。
三、不同隔离级别下的MVCC行为
MVCC的具体表现与事务隔离级别相关,主要差异在于ReadView的生成时机:
- 读已提交(Read Committed) :
每次执行SELECT
时都会重新生成ReadView。因此,事务在两次查询之间若有其他事务提交,可能会看到新提交的数据("不可重复读")。 - 可重复读(Repeatable Read) :
仅在事务第一次执行SELECT
时生成ReadView,之后的查询复用该ReadView。因此,事务在整个生命周期中看到的数据版本是一致的("可重复读")。
四、MVCC的优势
- 读写不冲突:读操作通过访问旧版本数据,无需等待写操作释放锁;写操作仅锁定当前版本,不影响读操作。
- 隔离级别灵活:通过ReadView的生成时机和版本判断规则,适配不同隔离级别(读已提交、可重复读)。
- 支持事务回滚:结合Undo Log,确保事务失败时数据可恢复(原子性)。
总结
MVCC通过"隐藏列记录版本"、"Undo Log维护历史版本链"、"ReadView过滤可见版本"三者的配合,实现了多版本数据的并发访问。它既避免了传统锁机制的性能瓶颈,又保证了事务的隔离性,是现代数据库高效处理并发的核心技术之一。