6. InnoDB存储引擎对MVCC的实现

发布时间 2023-09-19 14:37:26作者: 壹索007

MVCC:多版本控制 (multi versioning) 就是对非锁定读的实现。如果读取的行正在执行 DELETE 或 UPDATE 操作,这时读取操作不会去等待行上锁的释放。相反地,InnoDB 存储引擎会去读取行的一个快照数据,对于这种读取历史数据的方式,我们叫它快照读 (snapshot read)
  在 Repeatable Read(可重复读) 和 Read Committed(提交读) 两个隔离级别下,如果是执行普通的 select 语句(不包括 select ... lock in share mode ,select ... for update)则会使用 一致性非锁定读(MVCC)。并且在 Repeatable Read 下 MVCC+next-key lock 实现了可重复读和防止部分幻读
锁定读(当前读):读取数据的最新版本
MVCC的实现依赖于:隐藏字段、Read View、undo log。在内部实现中,InnoDB通过数据行的DB_TRX_ID和Read View来判断数据的可见性,如不可见,则通过数据行的 DB_ROLL_PTR找到undo log中的历史版本。每个事务读到的数据版本可能不一样,在同一个事务中,用户只能看到该事务创建Read View之前已经提交的修改和该事务本身做的修改。                  
MVCC是通过Undo Log + Read View进行数据读取,Undo Log保存了历史快照,而Read View规则帮我们判断当前版本的数据是否可见


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

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 均可以被Purge ids_t m_ids;  //创建 Read View 时的活跃事务列表m_closed;  //标记 Read View 是否 close}
主要是用来做可见性判断,里面保存了 “当前对本事务不可见的其他活跃事务”
·  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

(3)Undo log:

主要有两个作用:
  当事务回滚时用于将数据恢复到修改前的样子
  另一个作用是MVCC,当读取记录时,若该记录被其他事务占用或当前版本对该事务不可见,则可以通过undo log 读取之前的版本数据,以此实现非锁定读。
在 InnoDB 存储引擎中 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线程进行最后的删除。(只有在快照读和事务回滚不涉及该日志时,对应的日志才会被purge线程统一删除)
不同事务或者相同事务的对同一记录行的修改,会使该记录行的 undo log 成为一条链表,链首就是最新的记录,链尾就是最早的旧记录。

(3)数据可见性算法:
  在InnoDB存储引擎中,创建一个新事务后,执行每个select语句前,都会创建一个快照(Read View),快照中保存了当前数据库系统中正处于活跃(没有 commit)的事务的ID号。其实简单的说保存的是系统中当前不应该被本事务看到的其他事务ID列表(即m_ids)。当用户在这个事务中要读取某个记录行的时候,InnoDB会将该记录行的DB_TRX_ID与Read View 中的一些变量及当前事务 ID 进行比较,判断是否满足可见性条件。
 
(4)RC 和 RR 隔离级别下 MVCC 的差异
在事务隔离级别 RC 和 RR (InnoDB 存储引擎的默认事务隔离级别)下,InnoDB 存储引擎使用 MVCC(非锁定一致性读),但它们生成 Read View 的时机却不同
在RC隔离级别下的每次select查询前都生成一个Read View (m_ids 列表)
在RR隔离级别下只在事务开始后第一次select数据前生成一个Read View(m_ids 列表)
 
(5)MVCC 解决不可重复读问题
虽然 RC 和 RR 都通过 MVCC 来读取快照数据,但由于 生成 Read View 时机不同,从而在 RR 级别下实现可重复读。
  读已提交的隔离级别下,每次的查询都会重新创建一个新的read-view,导致每次查询的read-view都是最新的,所以再版本链的匹配中,总是能拿到其他事务已经提交的最新数据,造成了不可重复读问题的出现;

  再可重复读的情况下,就只会再执行第一条查询sql的时候创建一个read-view,并且再事务结束之前都不会发生改变;这也是为什么再可重复读的隔离界别下为什么能解决不可重复读的真正原因。
 
(6)MVCC➕Next-key-Lock 防止幻读
InnoDB存储引擎在 RR 级别下通过 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 


mvcc多版本并发控制
  mvcc在mysql innodb中主要是为了提高数据库并发性能,用更好的方式去处理读写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。
实现原理:
  mvcc的实现是通过保存数据在某个时间点的快照来实现的。也就是说不管需要执行多长时间,每个事务看到的数据都是一致的。
  就是为了查询一些正在被另一个事务更新的行,并且可以看到它们被更新之前的值,这样在做查询的时候就不用等待另一个事务释放锁。
 
如果被访问版本的trx_id属性值与ReadView中的creator_trx_id 值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
如果当前的事务ID在绿色部分,是已经提交事务,则数据可见
如果当前的事务ID在蓝色部分,会有俩种情况,如果当前事务ID在read-view数组内,是没有提交的事务不可见,如果不在数组内数据可见
如果落在红色部分,则不考虑,对于未来的事情不去想即可。此时找之前的旧版本。
 
在MVCC机制中,多个事务对同一个行记录进行更新会产生多个历史快照,这些历史快照保存在Undo Log里。如果一个事务想要查询这个行记录,需要读取哪个版本的行记录呢?这时就需要用到ReadView了,它帮我们解决了行的可见性问题。
 
使用 READ UNCOMMITTED 隔离级别的事务,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了。
使用 SERIALIZABLE 隔离级别的事务,InnoDB规定使用加锁的方式来访问记录。
使用 READ COMMITTED 和 REPEATABLE READ 隔离级别的事务,都必须保证读到已经提交了的事务修改过的记录。假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的,核心问题就是需要判断一下版本链中的哪个版本是当前事务可见的,这是ReadView要解决的主要问题。
 
(7)MVCC整体操作流程:
1.首先获取事务自己的版本号,也就是事务 ID;
2.获取ReadView;
3.查询得到的数据,然后与 ReadView 中的事务版本号进行比较;
4.如果不符合 ReadView 规则,就需要从 Undo Log 中获取历史快照;
5.最后返回符合规则的数据。
  如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录。