多版本并发控制(MVCC,Multi-Version Concurrency Control)提供了一种不同于传统锁机制的解决方案,通过维护数据的多个版本,依据特殊的版本比对规则,每个事务仅能看到符合其读视图的数据版本,以此实现读不加锁、读写不冲突的效果,在确保事务的隔离性和一致性的同时,更提高了数据库的并发性。
1 快照读与当前读
在深入理解MVCC之前,我们首先需要了解”当前读“和“快照读”的概念。
- 当前读(Current Read):这是指读取数据时获取的是最新的数据状态。当一个事务执行当前读操作时,它会锁定所读取的数据行,以防止其他事务在此期间修改这些数据,直至事务结束释放锁。因此,当前读通常涉及行级锁,并可能引起等待或阻塞。
select * from [表名] where [条件] lock in share mode; -- 共享锁
select * from [表名] where [条件] for update; -- 排他锁
insert into [表名] values(....); -- 排他锁
update [表名] set [修改的字段=修改的值] where [条件]; -- 排他锁
delete from [表名] where [条件]; -- 排他锁
- 快照读(Snapshot Read):快照读则是在读取数据时不加锁的一种读取方式,它返回的是事务开始时刻的数据快照。这意味着即使在读取过程中有其他事务对数据进行了修改,快照读仍然能够看到初始的数据版本,不会受到干扰。快照读可以提高并发性能,因为它避免了读取和写入之间的冲突。
select * from [表名] where [条件];
其中快照读就是基于MVCC机制而实现,在读已提交和可重复读隔离级别下被广泛使用。
2 核心组件
MVCC的高效运作依赖于三个关键组件:隐藏字段、undo log(回滚日志)以及Read View(读视图)。
- 隐藏字段:InnoDB为每一行数据附加了几个隐藏字段,用于版本控制和事务管理:
- trx_id:事务id,并非在
begin/start transaction
时立即分配,而是在执行第一个对InnoDB表进行修改的语句时申请。MySQL按事务启动的顺序严格分配事务ID,确保了事务的唯一性和顺序性。 - roll_ptr:回滚指针,指向该行的undo log记录,用于回滚操作和读取历史版本。
- row_id:内部行id,用于非唯一索引的叶子节点,当行没有显式主键时作为替代。
- trx_id:事务id,并非在
- undo log:当行数据被修改时,InnoDB不会立即更新行数据,而是将修改前的数据保存在undo log中(包含隐藏字段),并更新行数据中的roll_ptr指针指向这个undo log记录,支持数据的回滚和恢复。
- Read View:构建一个事务可见性的快照,包含一个已提交的最大事务id和事务开始时所有活跃的事务id列表。事务只能看到在其开始之前已经提交的事务所做的更改,而不能看到在其开始之后开始的任何事务所做的更改。
- 可重复读隔离级别下,读视图在首次查询(任意表)时生成并保持不变,直到事务结束。
- 读已提交隔离级别下,每次查询时都会重新生成读视图,确保每次读取都是最新的提交状态。
3 实现原理
3.1 版本链比对
在MVCC机制下,一个事务能够看到的数据记录版本,完全依赖于该事务在执行过程中与版本链的比对结果。具体规则如下:
- 当前事务版本可见:如果一个记录的
trx_id
等于当前事务的ID,这意味着是自己所做的更改,那么该版本对当前事务是可见的。 - 已提交版本可见:如果一个记录版本的
trx_id
小于或等于Read View中的最大已提交事务ID,那么这个版本对当前事务是可见的。 - 活跃事务版本不可见:如果一个记录版本的
trx_id
出现在Read View的活跃事务ID列表中,这意味着该版本是由一个尚未提交的事务创建或修改的,因此对当前事务是不可见的。 - 未来版本不可见:如果一个记录版本的
trx_id
大于Read View中的最大已提交事务ID,同时也不在活跃事务ID列表中,这意味着该版本是在当前事务开始之后创建的,对当前事务是不可见的。
简而言之,当前事务只能看到自己的和在其之前已经提交的trx_id的事务数据,对于在其之后创建的trx_id事务数据和在其创建时还未提交的事务数据都不可见。
3.2 示例
在可重复读隔离级别下,假设我们依次对account
表id=1的记录进行如下修改:
- T1将记录
lilei
更新为lilei1
,trx_id
=100。 - T2将
lilei1
更新为lilei2
,trx_id
=120。 - T3将
lilei2
更新为lilei3
,trx_id
=200。 - T4开启事务,Read View的最大已提交事务ID为100,活跃事务ID列表为[80,120,200]。
- T5将
lilei3
更新为lilei4
,trx_id
=270。 - T6将
lilei4
更新为lilei5
,trx_id
=300。 - T4查询id=1的数据记录
当T4尝试读取记录时,它将遵循以下流程:
lilei5
的trx_id
=300,大于100且不在活跃事务列表中,不可见。lilei4
的trx_id
=270,大于100且不在活跃事务列表中,不可见。lilei3
的trx_id
=200,在活跃事务列表中,不可见。lilei2
的trx_id
=120,在活跃事务列表中,不可见。lilei1
的trx_id
=100,等于最大已提交事务ID,可见。
因此,T4将读取lilei1
的版本,即使后来有其他事务对其进行了更新。同理,在读已提交隔离级别下,T4查询id=1的数据记录时将会重新生成Read View,此时最大已提交事务ID为270,活跃事务ID列表为[80,120,200,300],最终将读取lilei4
的版本。
4 结语
总而言之,快照读是多版本并发控制(MVCC)机制的核心体现,它允许事务在无需加锁的情况下访问数据的历史版本,从而极大提升了数据库的并发性能。通过维护隐藏字段如事务ID和回滚指针,结合undo log和Read View,MVCC确保了事务能够看到与其视图匹配的数据快照。在可重复读隔离级别下,事务一旦启动便能看到一个固定的数据视图,不受后续事务影响;而在读已提交级别下,每次读取都能反映最新提交的状态。这种设计不仅保持了数据一致性,还优化了读取操作,使其免受写操作的阻塞,实现了高效的数据管理和访问。