1
0
mirror of https://github.com/Snailclimb/JavaGuide synced 2025-07-24 07:21:46 +08:00
Java-Interview-Guide/docs/database/InnoDB对MVCC的实现.md
2021-07-15 23:47:50 +08:00

16 KiB
Raw Blame History

一致性非锁定读和锁定读

一致性非锁定读

对于 一致性非锁定读Consistent Nonlocking Reads 的实现,通常做法是加一个版本号或者时间戳字段,在更新数据的同时版本号 + 1或者更新时间戳。查询时将当前可见的版本号与对应记录的版本号进行比对如果记录的版本小于可见版本则表示该记录可见

InnoDB 存储引擎中,多版本控制 (multi versioning) 就是对非锁定读的实现。如果读取的行正在执行 DELETEUPDATE 操作,这时读取操作不会去等待行上锁的释放。相反地,InnoDB 存储引擎会去读取行的一个快照数据,对于这种读取历史数据的方式,我们叫它快照读 (snapshot read)

Repeatable ReadRead Committed 两个隔离级别下,如果是执行普通的 select 语句(不包括 select ... lock in share mode , select ... for update)则会使用 一致性非锁定读MVCC。并且在 Repeatable ReadMVCC 实现了可重复读和防止部分幻读

锁定读

如果执行的是下列语句,就是 锁定读Locking Reads

  • select ... lock in share mode
  • select ... for update
  • insert、update、delete 操作

在锁定读下,读取的是数据的最新版本,这种读也被称为 当前读current read。锁定读会对读取到的记录加锁:

  • select ... lock in share mode:对记录加 S 锁,其它事务也可以加S锁,如果加 x 锁则会被阻塞

  • select ... for updateinsertupdatedelete :对记录加 X 锁,且其它事务不能加任何锁

在一致性非锁定读下,即使读取的记录已被其它事务加上 X 锁,这时记录也是可以被读取的,即读取的快照数据。上面说了在 Repeatable ReadMVCC 防止了部分幻读,这边的 “部分” 是指在 一致性非锁定读 情况下只能读取到第一次查询之前所插入的数据根据Read View判断数据可见性Read View在第一次查询时生成但如果是当前读 ,每次读取的都是最新数据,这时如果两次查询中间有其它事务插入数据,就会产生幻读。所以 InnoDB 在实现Repeatable Read 时,如果执行的是当前读,则会对读取的记录使用 Next-key Lock ,来防止其它事务在间隙间插入数据

InnoDB对MVCC的实现

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

隐藏字段

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

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

ReadView

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

undo log 主要有两个作用:

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

InnoDB 存储引擎中 undo log 分为两种: insert undo logupdate undo log

  1. insert undo log :指在 insert 操作中产生的 undo log。因为 insert 操作的记录只对事务本身可见,对其他事务不可见,故该 undo log 可以在事务提交后直接删除。不需要进行 purge 操作

insert 时的数据初始状态:

markdown

  1. update undo log updatedelete 操作中产生的 undo log。该 undo log可能需要提供 MVCC 机制,因此不能在事务提交时就进行删除。提交时放入 undo log 链表,等待 purge线程 进行最后的删除

数据第一次被修改时:

markdown

数据第二次被修改时:

markdown

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

数据可见性算法

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

具体的比较算法如下:图源

markdown

  1. 如果记录 DB_TRX_ID < m_up_limit_id那么表明最新修改该行的事务DB_TRX_ID在当前事务创建快照之前就提交了所以该记录行的值对当前事务是可见的

  2. 如果 DB_TRX_ID >= m_low_limit_id那么表明最新修改该行的事务DB_TRX_ID在当前事务创建快照之后才修改该行所以该记录行的值对当前事务不可见。跳到步骤5

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

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

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

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

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

RC和RR隔离级别下MVCC的差异

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

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

MVCC解决不可重复读问题

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

举个例子:

markdown

在RC下ReadView生成情况

  1. 假设时间线来到 T4 ,那么此时数据行 id = 1 的版本链为

    markdown

由于 RC 级别下每次查询都会生成 Read View 并且事务101、102 并未提交,此时 103 事务生成的 Read View 中活跃的事务 m_ids 为:[101,102] m_low_limit_id104m_up_limit_id101m_creator_trx_id103

  • 此时最新记录的 DB_TRX_ID 为101m_up_limit_id <= 101 < m_low_limit_id所以要在 m_ids 列表中查找,发现 DB_TRX_ID 存在列表中,那么这个记录不可见
  • 根据 DB_ROLL_PTR 找到 undo log 中的上一版本记录,上一条记录的 DB_TRX_ID 还是101不可见
  • 继续找上一条 DB_TRX_ID为1满足 1 < m_up_limit_id可见所以事务103查询到数据为 name = 菜花
  1. 时间线来到 T6 ,数据的版本链为

    markdown

因为在 RC 级别下,重新生成 Read View这时事务101已经提交102并未提交所以此时 Read View 中活跃的事务 m_ids[102] m_low_limit_id104m_up_limit_id102m_creator_trx_id103

  • 此时最新记录的 DB_TRX_ID 为102m_up_limit_id <= 102 < m_low_limit_id所以要在 m_ids 列表中查找,发现 DB_TRX_ID 存在列表中,那么这个记录不可见

  • 根据 DB_ROLL_PTR 找到 undo log 中的上一版本记录,上一条记录的 DB_TRX_ID 为101满足 102 < m_up_limit_id记录可见所以在 T6 时间点查询到数据为 name = 李四,与时间 T4 查询到的结果不一致,不可重复读!

  1. 时间线来到 T9 ,数据的版本链为

markdown

重新生成 Read View 这时事务 101 和 102 都已经提交,所以 m_ids 为空,则 m_up_limit_id = m_low_limit_id = 104最新版本事务ID为102满足 102 < m_low_limit_id可见查询结果为 name = 赵六

总结: 在RC隔离级别下事务在每次查询开始时都会生成并设置新的 Read View所以导致不可重复读

在RR下ReadView生成情况

在可重复读级别下只会在事务开始后第一次读取数据时生成一个Read Viewm_ids列表

  1. 在 T4 情况下的版本链为

markdown

在当前执行 select 语句时生成一个 Read View,此时 m_ids[101,102] m_low_limit_id104m_up_limit_id101m_creator_trx_id103

此时和 RC 级别下一样:

  • 最新记录的 DB_TRX_ID 为101m_up_limit_id <= 101 < m_low_limit_id所以要在 m_ids 列表中查找,发现 DB_TRX_ID 存在列表中,那么这个记录不可见
  • 根据 DB_ROLL_PTR 找到 undo log 中的上一版本记录,上一条记录的 DB_TRX_ID 还是101不可见
  • 继续找上一条 DB_TRX_ID为1满足 1 < m_up_limit_id可见所以事务103查询到数据为 name = 菜花
  1. 时间点 T6 情况下

    markdown

    在 RR 级别下只会生成一次Read View,所以此时依然沿用 m_ids [101,102] m_low_limit_id104m_up_limit_id101m_creator_trx_id103

    • 最新记录的 DB_TRX_ID 为102m_up_limit_id <= 102 < m_low_limit_id所以要在 m_ids 列表中查找,发现 DB_TRX_ID 存在列表中,那么这个记录不可见

    • 根据 DB_ROLL_PTR 找到 undo log 中的上一版本记录,上一条记录的 DB_TRX_ID 为101不可见

    • 继续根据 DB_ROLL_PTR 找到 undo log 中的上一版本记录,上一条记录的 DB_TRX_ID 还是101不可见

    • 继续找上一条 DB_TRX_ID为1满足 1 < m_up_limit_id可见所以事务103查询到数据为 name = 菜花

  2. 时间点 T9 情况下:

markdown

此时情况跟 T6 完全一样,由于已经生成了 Read View,此时依然沿用 m_ids [101,102] ,所以查询结果依然是 name = 菜花

MVCCNext-key-Lock防止幻读

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

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

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

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

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

参考