MVCC实现原理

一、什么是MVCC?

MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问多版本控制: 指的是一种提高并发的技术。最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了InnoDB的并发度。在内部实现中,与Postgres在数据行上实现多版本不同,InnoDB是在undo log中实现的,通过undo log可以找回数据的历史版本。找回的数据历史版本可以提供给用户读(按照隔离级别的定义,有些读请求只能看到比较老的数据版本),也可以在回滚的时候覆盖数据页上的数据。在InnoDB内部中,会记录一个全局的活跃读写事务数组,其主要用来判断事务的可见性。

MVCC用于实现提交读和可重复读这两种隔离级别。而未提交读隔离级别总是读取最新的数据行,无需使用 MVCC。可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。

基本思想:MVCC 利用了多版本的思想,写操作更新最新的版本快照,而读操作去读旧版本快照,没有互斥关系,这一点和 CopyOnWrite 类似。

二、什么是当前读和快照读?

当前读:就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁

快照读:可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本

三、MVCC的实现原理

MVCC的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突,它的实现原理主要是依赖记录中的 3个隐式字段,undo log ,Read View 来实现的

1、3个隐式字段

  每行记录除了我们自定义的字段外,还有数据库隐式定义的字段:

  • DB_ROW_ID用于标识记录的唯一ID,在InnoDB表中对应主键列或一个隐藏的主键列(隐含的自增ID);
  • DB_TRX_ID表示修改该记录的事务ID;
  • DB_ROLL_PTR回滚指针,指向该记录的undo log。也就是指向这条记录的上一个版本。

2、undo log

  undo log是一个用于存放事务执行前数据的备份的日志,在事务回滚时可以使用该日志来恢复到事务执行前的状态。

 undo log主要分为两种:

  • insert undo log
    代表事务在insert新记录时产生的undo log, 只在事务回滚时需要,并且在事务提交后可以被立即丢弃
  • update undo log
    事务在进行update或delete时产生的undo log; 不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快照读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除

   不同事务或者相同事务的对同一记录的修改,会导致该记录的undo log成为一条记录版本线性表,既链表,undo log的链首就是最新的旧记录,链尾就是最早的旧记录

3、Read View

Read View用于记录每个事务开始时间的数据结构,以便 MySQL 引擎在查询时能够根据事务启动时间戳和各个数据行对应的版本链信息来确定该事务能够看到哪些数据。

通俗的说,当一个事务开始执行时,MySQL引擎会记录下该事务开始的时间戳。之后,在这个事务执行的过程中,如果需要读取某个数据行,MySQL引擎会根据这个时间戳和该数据行对应的版本链信息,找到最近的、在该事务启动时间之前已经提交的数据版本。这样,就可以保证在该事务的执行过程中,读取的数据是和该事务启动时一致的。

需要注意的是,每个事务都会有自己的Read View,它们是相互独立的。因此,不同的事务对同一行数据的读取结果可能是不同的,这取决于它们启动的时间。

总之,Read View提供了一个机制,用于确定在某个事务启动时间之前已经提交的数据版本,以便实现可重复读和串行化隔离级别下的并发读取。

拓展

Read View就是事务进行快照读操作的时候生产的读视图,在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个trx_id, 这个trx_id是递增的,所以最新的事务,trx_id值越大)

所以我们知道 Read View主要是用来做可见性判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。

Read View 是一个结构体,包含多个字段。其中比较重要的字段有以下4个:

  • trx_ids
    InnoDB 为每个事务构造了一个数组trx_ids,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务 ID。(“活跃”指的就是,启动了但还没提交。)
  • low_limit_id   
    trx_ids数组中的最小事务ID。
  • up_limit_id
    ReadView生成时刻系统尚未分配的下一个事务ID的值,也就是目前已出现过的事务ID的最大值+1
  • creator_trx_id
    创建这个 ReadView 的事务ID。

 

数组里面事务 ID 的最小值记为低水位,当前系统里面已经创建过的事务 ID 的最大值加 1 记为高水位。这个视图数组和高水位,就组成了当前事务的一致性视图(read-view)。

低水位和高水位将事务分成了三段:

已提交事务 未提交事务(所有活跃事务的数组 trx_ids) 未开始事务

Read View 通过判断这四个字段的方式来确定哪些数据对于当前事务来说是可见的,哪些是不可见的。判断过程如下:

1) 如果被访问版本的trx_id,与readview中的creator_trx_id值相同,表明当前事务在访问自己修改过的记录,该版本可以被当前事务访问

2)如果落在绿色部分,表示这个版本是已提交的事务生成的,这个数据是可见的;

3)如果落在红色部分,表示这个版本是由将来启动的事务生成的,是不可见的;

4)如果落在黄色部分(low_limit_id <= trx_id <= up_limit_id),那就包括两种情况

​ a. 若 trx_id 在数组trx_ids中,表示这个版本是由还没提交的事务生成的,不可见;

​ b. 若 trx_id 不在数组中trx_ids,表示这个版本是已经提交了的事务生成的,可见。

【总之就是修改当前行的事务提交了,数据才能被其他事务查看】

四、RC,RR级别下的InnoDB快照读有什么不同?

正是Read View生成时机的不同,从而造成RC,RR级别下快照读的结果的不同

  • 在RR【可重复读】级别下的某个事务的对某条记录的第一次快照读会创建一个Read View, 将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,还是使用的是同一个Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见;【即事务在RR级别下,快照读生成Read View时,Read View会记录此时所有其他活动事务的快照,这些事务的修改对于当前事务都是不可见的。而早于Read View创建的事务所做的修改均是可见】
  • 事务在RC【读已提交】级别下,每次快照读都会新生成一个Read View, 这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因

总之在RC隔离级别下,是每个快照读都会生成并获取最新的Read View;而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View。

五、临键锁(Next-Key Locks)

在了解临键锁前,先熟悉一下记录锁和间隙锁:

1、记录锁(Record Locks)

记录锁其实很好理解,对表中的记录加锁,叫做记录锁,简称行锁。比如:

SELECT * FROM `test` WHERE `id` = 1 FOR UPDATE;

它会在 id=1 的记录上加上记录锁,以阻止其他事务插入,更新,删除 id=1 这一行。

需要注意的是:

  • id 列必须为唯一索引列或主键列,否则上述语句加的锁就会变成临键锁(有关临键锁下面会讲)。
  • 同时查询语句必须为精准匹配(=),不能为 >、<、like等,否则也会退化成临键锁。

其他实现:在通过 主键索引 与 唯一索引 对数据行进行 UPDATE 操作时,也会对该行数据加行锁:

-- id 列为主键列或唯一索引列 
UPDATE SET age = 50 WHERE id = 1;

锁定的是一个记录上的索引,而不是记录本身。如果要锁的列没有索引,则进行全表记录加锁。

2、间隙锁(Gap Locks)

间隙锁 是 Innodb 在 可重复读隔离级别下为了解决幻读问题时引入的锁机制。间隙锁是innodb中行锁的一种。

注意:使用间隙锁锁住的是一个区间,而不仅仅是这个区间中的每一条数据。也就是锁定索引之间的间隙,但是不包含索引本身

举例来说,假如emp表中只有101条记录,其empid的值分别是1,2,...,100,101,下面的SQL:

 SELECT * FROM emp WHERE empid > 100 FOR UPDATE

当我们用条件检索数据,并请求共享或排他锁时,InnoDB不仅会对符合条件的empid值为101的记录加锁,也会对empid大于101(即使这些记录并不存在)的“间隙”加锁。

这个时候如果你插入empid等于102的数据的,如果那边事物还没有提交,那你就会处于等待状态,无法插入数据。

3、临键锁(Next-Key Locks)

Tips:

1) 共享锁

共享锁(Shared Lock)也叫读锁,是用于读取数据的锁定方式。当一个事务获取了一行数据的共享锁后,其他事务还可以继续获取该行的共享锁,但是不能获取独占锁(排它锁),也即是其他事务不能修改该行的数据。

在数据库中,多个事务可以同时获取同一行的共享锁,实现多个事务同时读取同一行数据的操作,所以共享锁也称为读共享锁。

2) 行锁

行锁(Row Lock)也叫写锁或独占锁(Exclusive Lock),是用于修改数据的锁定方式。当一个事务获取了一行数据的行锁后,其他事务无法获取该行的任何锁,也无法修改该行的数据。

在数据库中,一般只允许一个事务获取同一行的行锁,确保操作的数据一致性,所以行锁也称为写锁或独占锁。

3) 区别

共享锁和行锁的区别在于它们的使用方式和作用对象。共享锁用于读取数据,多个事务可以同时获取同一行的共享锁,实现并发读取同一数据的操作;行锁用于修改数据,只允许一个事务获取同一行的行锁,确保数据的一致性。

Next-Key Locks 是 MySQL 的 InnoDB 存储引擎的一种锁实现。它是行锁和间隙锁的组合,不仅锁定一个记录上的索引,也锁定索引之间的间隙。它锁定一个前开后闭区间,例如一个索引包含以下值:10, 11, 13, 20,那么就需要锁定以下区间:

(-∞, 10]
(10, 11]
(11, 13]
(13, 20]
(20, +∞)

MVCC 不能解决幻影读问题,Next-Key Locks 就是为了解决这个问题而存在的。在可重复读(REPEATABLE READ)隔离级别下,使用 MVCC + Next-Key Locks 可以解决幻读问题。总之,Next-Key Locks是MySQL中实现MVCC的重要锁机制,在可重复读隔离级别下,能够保证读取数据时只能看到自己启动时间之前已经提交的数据,并能够避免幻读的发生。

4、总结

这里对 记录锁(行锁)、间隙锁、临键锁 做一个总结

  • InnoDB 中的行锁的实现依赖于索引,一旦某个加锁操作没有使用到索引,那么该锁就会退化为表锁。
  • 记录锁存在于包括主键索引在内的唯一索引中,锁定单条索引记录。
  • 间隙锁存在于非唯一索引中,锁定开区间范围内的一段间隔,它是基于临键锁实现的。
  • 临键锁存在于非唯一索引中,该类型的每条记录的索引上都存在这种锁,它是一种特殊的间隙锁,锁定一段左开右闭的索引区间。
posted @ 2021-11-15 16:16  danielzzz  阅读(1196)  评论(0编辑  收藏  举报