【MySQL】04. 事务管理(三):MVCC

in #study4 months ago

多版本并发控制(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,用于非唯一索引的叶子节点,当行没有显式主键时作为替代。
  • 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的记录进行如下修改:

  1. T1将记录lilei更新为lilei1trx_id=100。
  2. T2将lilei1更新为lilei2trx_id=120。
  3. T3将lilei2更新为lilei3trx_id=200。
  4. T4开启事务,Read View的最大已提交事务ID为100,活跃事务ID列表为[80,120,200]。
  5. T5将lilei3更新为lilei4trx_id=270。
  6. T6将lilei4更新为lilei5trx_id=300。
  7. T4查询id=1的数据记录

undo日志版本链.png

当T4尝试读取记录时,它将遵循以下流程:

  • lilei5trx_id=300,大于100且不在活跃事务列表中,不可见。
  • lilei4trx_id=270,大于100且不在活跃事务列表中,不可见。
  • lilei3trx_id=200,在活跃事务列表中,不可见。
  • lilei2trx_id=120,在活跃事务列表中,不可见。
  • lilei1trx_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确保了事务能够看到与其视图匹配的数据快照。在可重复读隔离级别下,事务一旦启动便能看到一个固定的数据视图,不受后续事务影响;而在读已提交级别下,每次读取都能反映最新提交的状态。这种设计不仅保持了数据一致性,还优化了读取操作,使其免受写操作的阻塞,实现了高效的数据管理和访问。