什么是MVCC机制

2023年 7月 31日 63.8k 0

前言

本文将讲述InnoDB引擎下MVCC机制在可重复读和读已提交级别的一个实现;当前读如何解决幻读问题;MVCC机制到底能不能完全避免幻读问题?

什么是MVCC

MVCC全称是多版本并发控制 (Multi-Version Concurrency Control),只有在InnoDB引擎下存在。MVCC机制的作用其实就是避免同一个数据在不同事务之间的竞争,提高系统的并发性能。

它的特点如下:

  • 允许多个版本(指的是不同版本的数据)同时存在。
  • 不依赖锁机制,性能高。
  • 只在读已提交和可重复读的事务隔离级别下工作。

为什么需要MVCC

并发性能:

在早期的数据库中,只有读读之间的操作才可以并发执行,读写,写读,写写操作都要阻塞,这样就会导致MySQL的并发性能极差。

采用了MVCC机制后,只有写写之间相互阻塞,其他三种操作都可以并行,这样就可以提高了MySQL的并发性能。

防止读写冲突:

MVCC通过为每个事务创建一个独立的数据版本(也就是ReadView),使得读操作不会受到其他事务写操作的干扰。这样可以避免脏读、不可重复读和幻读等问题。

InnoDB 对 MVCC 的实现

MVCC的实现依赖于:隐藏字段、ReadView、undo log。在内部实现中,通过数据行的DB_TRX_IDReadView判断数据对当前事务的可见性,如果不可见,则会通过数据行的DB_ROLL_PTR回滚指针找到版本链中的历史数据,重复刚刚的操作,在同一个事务当中,用户只能看到该事务创建ReadView之前其他事务已经提交的数据和自己修改的数据。

隐藏字段

在内部,InnoDB 存储引擎为每行数据添加了三个隐藏字段:

  • DB_TRX_ID(6字节):表示最后一次插入或更新该行的事务 id。此外,delete 操作在内部被视为更新,只不过会在记录头 Record header 中的 deleted_flag 字段将其标记为已删除
  • DB_ROLL_PTR(7字节) 回滚指针,指向该行的 undo log 。如果该行未被更新,则为空
  • DB_ROW_ID(6字节):如果没有设置主键且该表没有唯一非空索引时,InnoDB 会使用该 id 来生成聚簇索引

ReadView

class ReadView {
  /* ... */
private:
  trx_id_t m_low_limit_id;      /* 大于等于这个 ID 的事务均不可见 */
​
  trx_id_t m_up_limit_id;       /* 小于这个 ID 的事务均可见 */
​
  trx_id_t m_creator_trx_id;    /* 创建该 Read View 的事务ID */
​
  trx_id_t m_low_limit_no;      /* 事务 Number, 小于该 Number 的 Undo Logs 均可以被 清除 */
​
  ids_t m_ids;                  /* 创建 Read View 时的活跃事务列表 */
​
  m_closed;                     /* 标记 Read View 是否 close */
}

Read View主要是用来做可见性判断,里面保存了 “当前对本事务不可见的其他活跃事务”

主要有以下字段:

  • m_low_limit_id:目前出现过的最大的事务 ID+1,即下一个将被分配的事务 ID。大于等于这个 ID 的数据版本均不可见
  • m_up_limit_id:活跃事务列表 m_ids 中最小的事务 ID,如果 m_ids 为空,则 m_up_limit_idm_low_limit_id。小于这个 ID 的数据版本均可见
  • m_idsRead View 创建时其他未提交的活跃事务 ID 列表。创建 Read View时,将当前未提交事务 ID 记录下来,后续即使它们修改了记录行的值,对于当前事务也是不可见的。m_ids 不包括当前事务自己和已提交的事务(正在内存中)
  • m_creator_trx_id:创建该 Read View 的事务 ID

undo log

该日志主要有两个作用:

  • 用于事务的回滚,当一个事务中执行了dml时,会产生update(delete被视为update,底层通过记录头的delete_mask字段标识为是否删除)和insert类型的undolog日志,回滚时就会通过该日志存储的事务前的原本数据进行回滚。
  • 用于MVCC,当事务读取数据时,发现数据对当前事务不可见,就会通过DB_ROLL_PTR回滚指针找到undo log中的历史版本数据。
  • InnoDB 存储引擎中 undo log 分为两种:insert undo logupdate undo log

    insert undo log:指在 insert 操作中产生的 undo log。因为 insert 操作的记录只对事务本身可见,对其他事务不可见,并且也不需要提供支持给MVCC,故该 undo log 可以在事务提交后直接删除。不需要交给 purge 线程删除

    insert 时的数据初始状态:

    img

    update undo logupdatedelete 操作中产生的 undo log。该 undo log需要提供支持给 MVCC 机制,因此不能在事务提交时就进行删除。提交时放入 undo log 链表,等待 purge线程 进行最后的删除
    数据第一次被修改时:

    img

    数据第二次被修改时:

    img

    不同事务或者相同事务的对同一记录行的修改,会使该记录行的 undo log 成为一条链表,链首就是最新的记录,链尾就是最早的旧记录。

    在可重复读隔离级别下如何实现

    当开启一个事务时,第一次执行普通select语句前会创建一个ReadView,该快照中存储的数据,简单点说就是当前事务不可见和可见的事务id信息。

    该事务之后的普通select语句在查找数据时,就会通过数据行的DB_TRX_IDReadView中存储的不可见和可见的事务id就行比对,如果数据对当前事务不可见,则会通过数据行的DB_ROLL_PTR回滚指针找到undolog中的历史数据重复刚刚的操作。
    img

    img

    以上方的两张图为例:

  • 当第一次执行普通select操作时,数据状态为图1,此时还没有undolog,并且当前事务id为1。
  • 此时来了一个事务id为101的事务修改了这条数据,之后又来了一条事务id为102的事务又修改了这条数据。
  • 此时当事务id为1的事务此时执行select * from user时,发现当前数据的DB_TRX_ID为102,对我不可见,就会通过DB_ROLL_PTR找到undolog中的历史版本数据继续比对DB_TRX_ID对我是否可见。
  • 最后就会找到name为菜花的这条数据。
  • 这就是在可重复读隔离级别中MVCC的实现。

    在读已提交隔离级别下的实现

    该级别下MVCC的实现与可重复读的唯一差异就在于创建ReadView的时机不同,该级别是在每次执行普通select语句都会创建出ReadView,每次都会创建,所以就无法避免不可重复读问题。其他则没有差异

    快照读,当前读

    说到这了,你应该知道了什么是快照读,快照读就是读取数据时,如果数据不可见,或者因为其他事务的写操作数据被加锁了,不需要等待锁的释放,而是去读取数据的历史版本数据,这个就是快照读。

    当前读指的是读取最新的数据。

    间隙锁加记录锁解决当前读的幻读问题

    我们前面说的selete语句都有强调普通两个字,当如果我们执行的是select...for update/lock in share mode、insert、update、delete这样的语句时,是不会创建ReadView的,这类语句属于当前读。

    普通select语句是属于快照读,而这类语句它属于当前读,当前读也就是会读取最新的数据,此时如果有事务在我读取的范围内做了一个插入操作那么就会出现幻读问题,针对当前读这个情况InnoDB会为我们读取的数据加上间隙锁跟记录锁来防止幻读。

    MVCC机制到底能不能完全避免幻读问题?

    答案是不能的。
    我们假设有一张user表

    id name
    1 Linda
    4 Jack
    5 Smith
  • 当事务A执行select * from user时,会创建一个ReadView,读取出了id为1、4、5三个人
  • 此时事务B执行了insert into user values(2,'Anne')并且提交了事务。
  • 这个时候事务A在去读取user表时,数据和第一次读取时一样的,并不会读取到id位2的记录,因为id为2的记录对当前事务不可见,并且回滚指针DB_ROLL_PTR为null。
  • 但是如果事务A执行了对id为2的一个update操作,因为update是当前读,是可以读到这条记录的,就去执行修改了,此时这条记录的事务id就变成了当前事务的id。
  • 这个时候再去执行select * from user时,就读到了id为2的数据,此时就产生了幻读问题。
  • 还有第二种情况

  • 当事务A执行select * from user
  • 事务B插入了insert into user values(2,'Anne'),并提交。
  • 此时事务A执行了select * from user for update当前读。
  • 这两种情况都是因为在创建出快照后,执行了当前读导致的幻读问题,所以说MVCC机制并不会完全避免幻读,又或者说是MVCC机制不能避免快照读后又执行当前读导致的幻读问题。

    想要彻底解决幻读,可以在一开始事务时就执行select ... for update这样的select语句,通过next-key-lock(记录和间隙锁的结合)这样的锁,拒绝其他事务对我读取的数据进行写操作,从而完全避免幻读问题。

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论