当前读与快照读

1.当前读

读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。对于我们日常的操作,如:
select .. lock in share mode(共享锁), select…for update、update、insert、delete(排他锁)都是一种当前读。

2.快照读

简单的select(不加锁)就是快照读,快照读,读取的是记录数据的可见版本,有可能是历史数据,不加锁,是非阻塞读。

  • Read Committed:每次select,都生成一个快照读。
  • Repeatable Read:开启事务后第一个select语句才是快照读的地方。
  • Serializable:快照读会退化为当前读

3.MVCC

全称 Multi-Version Concurrency Control,多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突,快照读为MySQL实现

MVCC允许多个事务同时读取同一行数据,而不会彼此阻塞,每个事务看到的数据版本是该事务开始时的数据版本。这意味着,如果其他事务在此期间修改了数据,正在运行的事务仍然看到的是它开始时的数据状态,从而实现了非阻塞读操作。

MVCC提供了一个非阻塞读功能。MVCC的具体实现,还需要依赖于数据库记录中的三个隐式字段、undo log日志、readView。

MVCC 的实现依赖于:隐藏字段、Read View、undo log。在内部实现中,InnoDB 通过数据行的隐藏字段 creator_trx_idRead View 来判断数据的可见性,如不可见,则通过数据行的 隐藏字段DB_ROLL_PTR 找到 undo log 中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建 Read View 之前已经提交的修改和该事务本身做的修改

了解MVCC对可重复读的实现原理之前,需要明白三件事情,隐藏字段、undo log 、Read View

隐藏字段

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

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

对于MVCC的实现来说,creator_trx_idDB_ROLL_PTR 两个字段是最重要的。

ReadView

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

主要有以下字段:

  • max_trx_id:目前出现过的最大的事务 ID+1,即下一个将被分配的事务 ID。大于等于这个 ID 的数据版本均不可见

  • min_trx_id:活跃事务列表 m_ids最小的事务 ID,如果 m_ids 为空,则 min_trx_idmax_trx_id。小于这个 ID 的数据版本均可见

  • m_idsRead View 创建时其他未提交的活跃事务 ID 列表。创建 Read View时,将当前未提交事务 ID 记录下来,后续即使它们修改了记录行的值,对于当前事务也是不可见的。m_ids 不包括当前事务自己和已提交的事务(正在内存中)

  • creator_trx_id创建该 Read View 的事务 ID
    image.png

事务可见性示意图

image.png

image.png

undo-log

undo log 主要有两个作用:

  • 当事务回滚时用于将数据恢复到修改前的样子
  • 另一个作用是 MVCC ,当读取记录时,若该记录被其他事务占用或当前版本对该事务不可见,则可以通过 undo log 读取之前的版本数据,以此实现非锁定读

在undo log 中根据隐藏字段中的 DB_ROLL_PTR来将记录版本连成链表,通过隐藏字段creator_trx_id来记录事务id

数据可见性算法

InnoDB 存储引擎中,创建一个新事务后,执行每个 select 语句前,都会创建一个快照(Read View),快照中保存了当前数据库系统中正处于活跃(没有 commit)的事务的 ID 号。其实简单的说保存的是系统中当前不应该被本事务看到的其他事务 ID 列表(即 m_ids(事务 ID集合))。当用户在这个事务中要读取某个记录行的时候,InnoDB 会将该记录行的 creator_trx_idRead View 中的一些变量及当前事务 ID 进行比较,判断是否满足可见性条件

  1. 如果记录 creator_trx_id(当前事务id)< min_trx_id(最小的事务 ID) ,那么表明最新修改该行的事务creator_trx_id在当前事务创建快照之前就提交了,所以该记录行的值对当前事务是可见的

  2. 如果 creator_trx_id(当前事务id) >= max_trx_id(最大的事务 ID+1),那么表明最新修改该行的事务creator_trx_id在当前事务创建快照之后才修改该行,所以该记录行的值对当前事务不可见。跳到步骤 5

  3. m_ids(事务 ID集合) 为空,则表明在当前事务创建快照之前,修改该行的事务就已经提交了,所以该记录行的值对当前事务是可见的

  4. 如果 min_trx_id(最小的事务 ID) <= creator_trx_id < max_trx_id(最大的事务 ID+1),表明最新修改该行的事务creator_trx_id在当前事务创建快照的时候可能处于“活动状态”或者“已提交状态”;所以就要对活跃事务列表 m_ids(事务 ID集合) 进行查找(源码中是用的二分查找,因为是有序的)

  • 如果在活跃事务列表 m_ids(事务 ID集合) 中能找到 creator_trx_id,表明:① 在当前事务创建快照前,该记录行的值被事务 ID 为 creator_trx_id 的事务修改了,但没有提交;或者 ② 在当前事务创建快照后,该记录行的值被事务 ID 为 creator_trx_id 的事务修改了。这些情况下,这个记录行的值对当前事务都是不可见的。跳到步骤 5

  • 在活跃事务列表中找不到,则表明“id trx_id 的事务”在修改“该记录行的值”后,在“当前事务”创建快照前就已经提交了,所以记录行对当前事务可见

  1. 在该记录行的 DB_ROLL_PTR 指针所指向的 undo log 取出快照记录,用快照记录的creator_trx_id跳到步骤 1 重新开始判断,直到找到满足的快照版本或返回空

MVCC 解决不可重复读问题

在事务隔离级别 RCRR (InnoDB 存储引擎的默认事务隔离级别)下,InnoDB 存储引擎使用 MVCC(非锁定一致性读),但它们生成 Read View 的时机却不同

  • 在 RC 隔离级别下的 每次select 查询前都生成一个Read View (m_ids 列表)
  • 在 RR 隔离级别下只在事务开始后 第一次select 数据前生成一个Read View(m_ids 列表)

RC隔离级别下MVCC 不可重复读问题

虽然 RC 和 RR 都通过 MVCC 来读取快照数据,但由于 生成 Read View 时机不同,从而在 RR 级别下实现可重复读

image.png

对于RC的情况,在每次查询查询时都会生成ReadView,根据Read View里的四个字段,分别为当前事务ID,最小事务ID,最大事务ID+1,活动事务ID集合,再结合数据可见性算法来从undo log中的数据链中的事务ID一个一个的分析,直道找到一个可以访问的数据为止。

从undo log日志表中找到最新数据记录版本的事务ID,看这个事务ID和undo log四个字段的关系

  1. 如果当前版本事务ID等于当前事务ID,说明该数据是由当前事务修改的,则可以访问该版本数据
  2. 如果当前版本事务ID小于最小事务ID,说明事务已经提交,可以访问该版本数据
  3. 如果当前版本事务ID大于最大事务ID+1,说明该事务是在readview生成后才开启,不可以访问该版本数据
  4. 如果当前版本事务ID大于等于最小事务ID,并且小于等于最大事务ID+1,并且当前版本事务ID不处于事务ID集合中,说明数据已经提交,可以访问该版本数据。

如果这四个条件都不满足,表明undo log当前版本的数据不能访问,则顺着链找到下一个版本的数据,重复上面五个步骤,直到找到可以访问的版本数据。

在RC隔离级别下无法解决不可重复读的问题的根本原因就是因为ReadView的生成,在同一个事务中的每次查询都会生成新的ReadView,这就导致了不可重复读的问题

与RC相反的是RR,它解决了不可重复读的问题,原因就是在事务开启后,查询数据前生成一份ReadView,整个事务过程中只会生成这一份ReadView,所以可以解决不可重复读的问题

RR隔离级别下MVCC 解决不可重复读问题

RR和RC在处理不可重复读的问题上的不同之处,就是ReadView的生成问题

RR在事务开启后,查询数据前生成一份ReadView,整个事务过程中只会生成这一份ReadView,所以可以解决不可重复读的问题。在对版本数据访问的判断过程和RC的过程一样。

MVCC➕Next-key-Lock 防止幻读

InnoDB存储引擎在 RR 级别下通过 MVCCNext-key Lock 来解决幻读问题:

  • 快照读的情况下使用MVCC解决幻读问题
  • 当前读的情况下使用MVCC + Next-key Lock 解决幻读问题

1、执行普通 select,此时会以 MVCC 快照读的方式读取数据

在快照读的情况下,RR 隔离级别只会在事务开启后的第一次查询生成 Read View ,并使用至事务提交。所以在生成 Read View 之后其它事务所做的更新、插入记录版本对当前事务并不可见,实现了可重复读和防止快照读下的 “幻读”

2、执行 select…for update/lock in share mode、insert、update、delete 等当前读

在当前读下,读取的都是最新的数据,如果其它事务有插入新的记录,并且刚好在当前事务查询范围内,就会产生幻读InnoDB 使用 Next-key Lock (临键锁) 来防止这种情况。当执行当前读时,会锁定读取到的记录的同时,锁定它们的间隙,防止其它事务在查询范围内插入数据。只要我不让你插入,就不会发生幻读