参考链接:

https://pdai.tech/md/db/sql-mysql/sql-mysql-mvcc.html
注: MVCC 又称多版本并发控制。 在Mysql InnoDB 中的实现主要是为了提高数据库的并发控制能力。

当前读和快照读

  1. 当前读读的是数据库的最新版本。读取时还要保证其他事务不会修改数据,需要加行锁和表锁
  2. 快照读读的是数据的历史版本。 MVCC会将数据快照截取加入到 undo log 链表中,读的时候再从链表中读取快照。

MVCC 的实现原理

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

  • DB_ROW_ID 6byte, 隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引
  • DB_TRX_ID 6byte, 最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务ID
  • DB_ROLL_PTR 7byte, 回滚指针,指向这条记录的上一个版本(存储于rollback segment里)
  • DELETED_BIT 1byte, 记录被更新或删除并不代表真的删除,而是删除flag变了

    Undo 日志

  • Insert undo log :插入一条记录时,至少要把这条记录的主键值记下来,之后回滚的时候只需要把这个主键值对应的记录删掉就好了。

  • Update undo log:修改一条记录时,至少要把修改这条记录前的旧值都记录下来,这样之后回滚时再把这条记录更新为旧值就好了。
  • Delete undo log:删除一条记录时,至少要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中就好了。
  • 删除操作都只是设置一下老记录的DELETED_BIT,并不真正将过时的记录删除。
  • 为了节省磁盘空间,InnoDB有专门的purge线程来清理DELETED_BIT为true的记录。为了不影响MVCC的正常工作,purge线程自己也维护了一个read view(这个read view相当于系统中最老活跃事务的read view);如果某个记录的DELETED_BIT为true,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录一定是可以被安全清除的。

image.png

Read View 视图

Read View 就是事务进行快照读操作时产生的视图,MVCC会将当前活跃的事务ID记录下来根据一系列算法决定读取哪个事务的undo log 日志
Read View 有三个全局属性

  • trx_list 未提交事务ID列表,用来维护Read View生成时刻系统正活跃的事务ID
  • up_limit_id 记录trx_list列表中事务ID最小的ID
  • low_limit_id ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1

Read view 读取算法

  • 首先比较DB_TRX_ID < up_limit_id, 如果小于,则当前事务能看到DB_TRX_ID 所在的记录,如果大于等于进入下一个判断
  • 接下来判断 DB_TRX_ID 大于等于 low_limit_id , 如果大于等于则代表DB_TRX_ID 所在的记录在Read View生成后才出现的,那对当前事务肯定不可见,如果小于则进入下一个判断
  • 判断DB_TRX_ID 是否在活跃事务之中,trx_list.contains(DB_TRX_ID),如果在,则代表我Read View生成时刻,你这个事务还在活跃,还没有Commit,你修改的数据,我当前事务也是看不见的;如果不在,则说明,你这个事务在Read View生成之前就已经Commit了,你修改的结果,我当前事务是能看见的。

MVCC 的相关问题

RR 是如何在RC的基础上解决不可重复读的

例1:
image.png
例2:
image.png

RR 相比 RC 解决不可重复读的原理

由上图两个例子所示,由于Read View 生成时机的不同,造成RR, RC级别下快照读的结果不同。

  • 在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 而不会创建新的Read View

隔离级别的实现

读未提交:永远读到的都是最新修改的数据。不需要加锁
RC: MVCC 实现
RR:MVCC实现
串型化: 加排它锁来实现事务之间严格的串型化。

为什么MVCC 无法解决幻读

有用的链接:
https://www.zhihu.com/question/334408495
首先明确什么是幻读,幻读就是在两次范围读的间隙有另一个事务插入数据,导致两次读的数据范围不一致。MVCC 把读取数据分为了当前读和快照读,快照读读取的是数据快照确实可以避免幻读,如下:
image.png

但是MySQL 里除了普通查询是快照度,其他都是当前读,比如update、insert、delete,这些语句执行前都会查询最新版本的数据,然后再做进一步的操作。
这很好理解,假设你要 update 一个记录,另一个事务已经 delete 这条记录并且 提交事务了,这样不是会产生冲突吗,所以 update 的时候肯定要知道最新的数据。
另外,select … for update 这种查询语句是当前读,每次执行的时候都是读取最新的数据。
因此,要讨论「可重复读」隔离级别的幻读现象,是要建立在「当前读」的情况下。 可以由下图来解释。
image.png
为了解决MVCC无法解决幻读的问题,MySql 推出了Next-Key lock。

Next-Key Lock

Next-Key Locks 是 MySQL 的 InnoDB 存储引擎的一种锁实现。
MVCC 不能解决幻读的问题,Next-Key Locks 就是为了解决这个问题而存在的。在可重复读(REPEATABLE READ)隔离级别下,使用 MVCC + Next-Key Locks 可以解决幻读问题。

Record Locks

锁定一个记录上的索引,而不是记录本身。
如果表没有设置索引,InnoDB 会自动在主键上创建隐藏的聚簇索引,因此 Record Locks 依然可以使用。

Gap Locks

锁定索引之间的间隙,但是不包含索引本身。例如当一个事务执行以下语句,其它事务就不能在 t.c 中插入 15

  1. SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;

Next-key Locks

它是 Record Locks 和 Gap Locks 的结合,不仅锁定一个记录上的索引,也锁定索引之间的间隙。例如一个索引包含以下值: 10, 11, 13, and 20,那么就需要锁定以下区间:

(negative infinity, 10]
(10, 11]
(11, 13]
(13, 20]
(20, positive infinity)

总结

所以MVCC 解决 赃读和不可重复读的问题, MVCC + Next-Key Lock 解决幻读的问题。