Mysql的MVCC原理

一、概述

MVCC(多版本并发控制)是一种常用于数据库管理系统中的并发控制方法,旨在通过为每个事务提供数据的多个版本,从而避免并发事务间的数据冲突。MVCC的核心优势在于读操作无需加锁,从而实现读不阻塞写,同时写操作不会阻塞其他事务的读操作。这种方式特别适用于读多写少的应用场景,如 OLTP 系统。在传统的基于锁的并发控制(Lock-based Concurrency Control)中,多个事务在访问共享数据时需要加锁,这就会导致在高并发环境下的性能瓶颈。而MVCC通过在事务间维护数据的多个版本,能够显著提高并发性能。

二、MVCC的实现原理

MVCC的实现依赖于三个关键要素:

  • 隐式字段:每条记录都会附带一些额外的隐式字段,用于标识记录的版本信息。
  • 版本链:每次更新数据时,都会生成新的版本并通过回滚指针链接到旧版本,形成版本链。
  • Read View:每个事务会在开始时生成一个快照(Read View),该快照决定了该事务在其生命周期内能够访问到哪些数据版本。

1. 隐式字段

每行记录除了我们自定义的字段外,还有数据库隐式定义的 DB_TRX_ID, DB_ROLL_PTR, DB_ROW_ID 等字段。

  • DB_TRX_ID:6 byte,最近修改(修改/插入)事务 ID:记录创建这条记录/最后一次修改该记录的事务 ID

  • DB_ROLL_PTR:7 byte,回滚指针,指向这条记录的上一个版本(存储于 rollback segment 里)

  • DB_ROW_ID:6 byte,隐含的自增 ID(隐藏主键),如果数据表没有主键,InnoDB 会自动以DB_ROW_ID产生一个聚簇索引

  • 实际还有一个删除DELETED_FLAG隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除 flag 变了

2. 版本链

每次对记录进行修改时,数据库不会覆盖原有数据,而是通过创建一个新的数据版本来保存变更。这些新的版本会通过回滚指针链接成一个版本链。

  • 每一条记录都有一个版本链,版本链的头结点是当前记录的最新版本。
  • 回滚指针指向上一个版本,通过回滚指针,数据库能够维护记录的历史版本,确保支持事务回滚和数据快照。

当一个事务修改了数据时,系统会将修改后的数据版本放到版本链的头部,之前的版本则会继续保留。

3. Undo日志

Undo日志是数据库中的一种日志机制,用于记录事务执行前的数据状态,以支持事务的回滚操作。每当事务对数据进行修改时,数据库会生成对应的Undo日志,并保存修改前的记录快照。如果事务出现错误或需要回滚,数据库就可以通过Undo日志将数据恢复到修改前的状态。

  • Undo日志的作用

    • 事务回滚:在事务执行过程中,如果出现错误或事务被显式回滚,数据库通过Undo日志来撤销所有已执行的修改,将数据恢复到事务开始之前的状态。
    • 支持快照:Undo日志不仅记录了事务回滚的信息,也为MVCC提供了数据的历史版本。每次修改数据时,数据库会生成一个新的版本,同时保存Undo日志,以便在回滚时恢复。
  • Undo日志的工作原理

    • 当事务执行修改操作时,Undo日志会记录修改前的数据。
    • 如果事务回滚或发生故障,数据库将利用Undo日志恢复被修改的数据。
    • Undo日志通常是顺序写入的,与版本链不同,版本链是通过回滚指针在记录间进行链接的。

4. Read View(读取视图)

为了保证事务的一致性和隔离性,MVCC依赖于Read View来确定当前事务可以读取的数据版本。每个事务在开始时都会生成一个独立的Read View,该视图定义了当前事务能够访问的版本范围。

  • 生成Read View:当事务开始时,系统会为事务生成一个快照,该快照记录了事务开始时的所有版本信息。
  • 隔离级别的影响:在READ COMMITTEDREPEATABLE READ两个隔离级别下,Read View会影响事务能看到的数据。具体来说:
    • 读已提交:每次select时生成一个read-view,并且可以看到其他事务已经提交的数据(即使这些数据是本事务之外的其他事务修改的)。这种级别提供较高的并发性,但可能会导致不可重复读问题。
    • 可重复读:在事务开始时生成read-view,并且在整个事务内,这个read-view不会改变。即使其他事务提交了数据,本事务也只能读取到其事务开始时的版本,这就避免了不可重复读问题。

三、MVCC的工作原理

1. 事务的快照和隔离

每个事务在启动时生成一个独立的快照,即Read View。该快照包含了当前事务能看到的数据版本范围。在执行读操作时,数据库会根据事务的快照来选择合适的版本:

  • 对于SELECT语句,数据库会检查当前数据的DB_TRX_ID,并根据事务的快照判断该版本是否对事务可见。
  • 事务提交后,如果没有冲突,数据库会将该事务的修改提交至数据库,并成为新的版本。
  • 事务回滚后,该事务所修改的数据会被回滚至原始版本,版本链中的其他事务版本依然可见。

2. 版本链的管理

数据库会为每条记录维护一个版本链。每次记录被修改时,系统会生成一个新的数据版本,并通过回滚指针将其链接到旧版本。例如:

  • 初始状态:记录版本V0。
  • 第一次更新:生成新的版本V1,V0成为V1的上一个版本(通过回滚指针)。
  • 第二次更新:生成新的版本V2,V1成为V2的上一个版本。

在查询时,数据库会根据事务的Read View判断哪些版本对当前事务可见,并返回符合条件的版本。

3. 事务间的冲突

在高并发环境中,可能会存在多个事务同时修改同一数据的情况。MVCC能够通过版本链和回滚指针有效地解决此类问题:

  • 读操作:每个事务读取的数据版本是根据事务开始时的快照来确定的,这样避免了其他事务的更新操作对当前事务的影响。
  • 写操作:当事务修改数据时,它会创建新的版本,并且不会直接修改现有版本,从而避免了锁争用问题。

4. 隔离级别与MVCC

MVCC的行为依赖于数据库的事务隔离级别。常见的隔离级别包括:

  • 读未提交—READ UNCOMMITTED:事务可以读取其他事务未提交的数据,MVCC不适用。
  • 读已提交—READ COMMITTED:每次读取时生成新的Read View,保证读取的数据已提交,但可能会出现不可重复读的问题。
  • 可重复读—REPEATABLE READ:事务在开始时生成一个固定的Read View,保证在整个事务过程中,读取的数据始终保持一致。
  • 序列化—SERIALIZABLE:最严格的隔离级别,所有事务都被串行化执行,通常会涉及加锁,MVCC在此级别下表现较差。

四、总结

优势

  • 高并发性:读操作不加锁,能够有效提高并发性能。
  • 避免死锁:由于不需要加锁,避免了传统锁机制下可能出现的死锁问题。
  • 读取一致性:通过事务快照和版本链,保证事务内的一致性。

不足

  • 存储开销:由于需要维护版本链和历史数据,存储空间消耗较大。
  • 版本管理复杂:频繁更新的表可能会导致版本链非常长,版本管理变得复杂。
  • 回滚性能问题:如果版本链很长,回滚操作可能会变得较慢。