MySQL-InnoDB MVCC实现、Read View使用详解

InnoDB事务支持

  MySQL InnoDB 引擎使用 redo log(重做日志) 保证事务的持久性,使用 undo log(回滚日志) 来保证事务的原子性

  MySQL InnoDB 引擎通过 锁机制、MVCC 等手段来保证事务的隔离性( 默认支持的隔离级别是 可重复读 )。

  保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。

  ACID以及事务隔离性参考另外一篇文章:MySQL-ACID、事务隔离级别

InnoDB 对 MVCC 的实现

  MVCC 全称Multi-Version Concurrency Control,MVCC是一种通过增加版本冗余数据来实现并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。

  InnoDB中实现了MVCC主要是为了提高数据库的并发性能,在无锁的情况下也能处理读写并发,大大提高数据库的并发度。

  MVCC 的实现依赖于:隐藏字段(DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID)Read Viewundo log

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

隐藏字段

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

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

  innodb表加上隐藏字段如下:
在这里插入图片描述

ReadView

  ReadView 是事务快照读的时候产生的数据读视图,在该事务执行快照读的那一刻,会生成一个数据系统当前的快照,记录并维护系统当前活跃事务的id,事务的id值是递增的。

  ReadView 的最大作用就是判断数据的可见性,当某个事务执行快照读的时候,会对此记录创建一个ReadView 的视图,在整个事务期间根据某些条件判断该事务能够看到的版本链上的哪条历史数据。

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

  主要有以下字段:

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

  事务可见性示意图:MySQL-InnoDB MVCC实现、Read View使用详解_第1张图片

访问记录的版本事务ID即前面提到的隐藏DB_TRX_ID。
根据访问记录的版本DB_TRX_ID分为数据行的DB_TRX_ID和undo log中数据行的DB_TRX_ID。
当访问undo log中数据行d时,即使用d中的DB_TRX_ID与ReadView 的数据对比判断当前数据行是否可见。

  1. DB_TRX_ID=m_creator_trx_id 该版本对当前事务可见
    如果被访问记录的版本事务ID(DB_TRX_ID)与ReadView 中的m_creator_trx_id值相同,那么表示当前事务访问的是自己修改过的记录,那么该版本对当前事务可见;
  2. DB_TRX_ID该版本可以被当前事务访问:
    如果被访问版本的 事务ID(DB_TRX_ID)小于 ReadView 中的m_up_limit_id的值,那么表示生成该版本的事务在当前事务生成 ReadView 前已经提交,所以该版本可以被当前事务访问。
  3. DB_TRX_ID>m_low_limit_id 该版本不可以被当前事务访问 :
    如果被访问版本的事务ID(DB_TRX_ID)大于 ReadView 中的m_low_limit_id 值,那么表示生成该版本的事务在当前事务生成 ReadView 后才开启,所以该版本不可以被当前事务访问。
  4. m_up_limit_id<=DB_TRX_ID 如果被访问版本的 事务ID(DB_TRX_ID)在 ReadView 的m_up_limit_id和m_low_limit_id 之间,那就需要判断一下版本的事务ID是不是在 m_ids 列表中。
    如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问;
    如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。

  如果某个版本对当前事务不可见,那么顺着版本链找到下个版本记录,然后继续上面的对比规则,直到找到版本链中的最后一个版本,如果最后一个版本都不可见,那么该条记录对此事务完全不可见,也就查不到这个记录。

undo log

  undo log 主要有两个作用:

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

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

  insert undo log :指在 insert 操作中产生的 undo log。因为 insert 操作的记录只对事务本身可见,对其他事务不可见,故该 undo log 可以在事务提交后直接删除。不需要进行 purge 操作。
  update undo log :update 或 delete 操作中产生的 undo log。该 undo log可能需要提供 MVCC 机制,因此不能在事务提交时就进行删除。提交时放入 undo log 链表,等待 purge线程 进行最后的删除。

当前读和快照读

当前读

  当前读获取的数据是最新数据,而且在读取时不能被其他修改的,所以会对读取的记录加锁来控制。如下

select * from user where id > 1 lock in share mode;
select * from user where id >1 for update;
快照读

  快照读,顾名思义读取的是一份快照数据,所以读到的并不一定是最新数据,可能是历史数据。

  简单的select查询就是快照读,不加锁非阻塞读,降低数据库的开销。如下:

select * from user where id >1

  但是快照读在隔离级别是串行化级别是没有意义的,因为串行化的sql都是排队执行的,不存在并发,所以就会变成当前读。

Read View生成时机与数据可见性判断

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

Read View生成时机:

  • RC 级别下每次查询都会生成Read View;
  • RR 级别下只会生成一次Read View。

  举个例子:MySQL-InnoDB MVCC实现、Read View使用详解_第2张图片

RC 下 ReadView 生成情况

  假设时间线来到 T4 ,那么此时数据行 id = 1 的版本链为:MySQL-InnoDB MVCC实现、Read View使用详解_第3张图片
  由于 RC 级别下每次查询都会生成Read View ,并且事务 101、102 并未提交,此时 103 事务生成的 Read View 中活跃的事务
m_ids 为:[101,102] ,
m_low_limit_id为:104,
m_up_limit_id为:101,
m_creator_trx_id 为:103

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

  时间线来到 T6 ,数据的版本链为:MySQL-InnoDB MVCC实现、Read View使用详解_第4张图片  因为在 RC 级别下,重新生成 Read View,这时事务 101 已经提交,102 并未提交,所以此时 Read View 中活跃的事务
m_ids:[102] ,
m_low_limit_id为:104,
m_up_limit_id为:102,
m_creator_trx_id为:103

  • 此时最新记录的 DB_TRX_ID 为 102,m_up_limit_id <= 102 < m_low_limit_id,所以要在 m_ids 列表中查找,发现 DB_TRX_ID 存在列表中,那么这个记录不可见 ;
  • 根据 DB_ROLL_PTR 找到 undo log 中的上一版本记录,上一条记录的 DB_TRX_ID 为 101,满足 101 < m_up_limit_id,记录可见,所以在 T6 时间点查询到数据为 name = 李四,与时间 T4 查询到的结果不一致,不可重复读!

  时间线来到 T9 ,数据的版本链为:MySQL-InnoDB MVCC实现、Read View使用详解_第5张图片  重新生成 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 View(m_ids 列表)。
1.在 T4 情况下的版本链为:MySQL-InnoDB MVCC实现、Read View使用详解_第6张图片  在当前执行 select 语句时生成一个 Read View,此时
m_ids:[101,102] ,
m_low_limit_id为:104,
m_up_limit_id为:101,
m_creator_trx_id 为:103。

  此时和 RC 级别下一样:

  • 最新记录的 DB_TRX_ID 为 101,m_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 = 菜花

2.时间点 T6 情况下:MySQL-InnoDB MVCC实现、Read View使用详解_第7张图片  在 RR 级别下只会生成一次Read View,所以此时依然沿用
m_ids :[101,102] ,
m_low_limit_id为:104,
m_up_limit_id为:101,
m_creator_trx_id 为:103。

  • 最新记录的 DB_TRX_ID 为 102,m_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 = 菜花

  时间点 T9 情况下:MySQL-InnoDB MVCC实现、Read View使用详解_第8张图片  此时情况跟 T6 完全一样,由于已经生成了 Read View,此时依然沿用 m_ids :[101,102] ,所以查询结果依然是 name = 菜花。

InnoDB 存储引擎的锁的算法

  1. 行锁(Record Lock):锁直接加在索引记录上面,锁住的是key。
  2. 间隙锁(Gap Lock):锁定索引记录间隙,确保索引记录的间隙不变。间隙锁是针对事务隔离级别为可重复读或以上级别而已的。
  3. Next-Key Lock :行锁和间隙锁组合起来就叫Next-Key Lock。

InnoDB问题

MyISAM 和 InnoDB 存储引擎使用的锁

  • MyISAM 采用表级锁(table-level locking)。
  • InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁

  表级锁和行级锁对比:

  • 表级锁: MySQL 中锁定 粒度最大 的一种锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。其锁定粒度最大,触发锁冲突的概率最高,并发度最低,MyISAM 和 InnoDB 引擎都支持表级锁。
  • 行级锁: MySQL 中锁定 粒度最小 的一种锁,只针对当前操作的行进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。

参考:
https://javaguide.cn/database/mysql/innodb-implementation-of-mvcc/#锁定读
MySQL进阶系列:多版本并发控制mvcc的实现

你可能感兴趣的:(MySQL技术,mysql,InnoDB,MVCC,undo,log,read,view)