MVCC

目录

概述

MVCC是什么?

MVCC 是为了解决事务操作中多线程并发安全问题无锁并发控制技术。它的全称为:多版本并发控制。

为什么需要MVCC?

我们可以根据数据库三种并发场景来分析:

  1. 读读并发,不会产生并发问题,也不需要并发控制。
  2. 读写并发,会造成事务隔离问题,造成脏读、幻读、不可重复读的问题。
  3. 写写并发,可能出现数据更新丢失的问题。

MVCC 为每个修改保存一个版本,版本与事务时间戳关联,读操作只读事务开始前的数据库的快照。它是通过 Undo 日志和ReadView 来实现的。

MVCC 为数据库解决一下问题:

  1. 并发并发读写数据库时,读操作时不用去阻塞写操作,而写操作也不用去阻塞读操作,提高数据库的并发读写的处理能力。
  2. 实现一致性,解决脏读、幻读、不可重复读等事务隔离问题。但是它不经解决写写并发时,数据丢失问题。
  3. 采用乐观锁或者悲观锁来解决写写冲突。

MVCC 的全称:Multi-Version Concurrency Control。多版本并发控制。ReadView + 版本链。

在 MySQL InnoDB 引擎下,RC、RR 基于MVCC 进行并发事务控制的。

MVCC 是基于 ”数据版本“ 对并发事务进行访问控制的。

如下图:

  • 事务 A 将 id = 1 的 name 改为 ”A“
  • 事务 A 提交后,事务 B 将 id = 1 的 name 改为 ”B“。
  • 事务 B 提交后,事务 C 将 id = 1 的 name 改为 ”C“。
  • 事务 D 两次读取的时机非常特殊:
    • 第一次读取在事务 A 提交后和 事务 B 修改后未提交之间。
    • 第二次读取在事务 B 提交后和 事务 C 修改后未提交之间。

那么事务D 两次读取分别读的 name 是多少?

  • 如果数据库的隔离级别是:RR(Repeatable_read:可重复读),那么在同一个事务(D)中两次读取的结果应该是一样的。由于事务A 已经提交,那么事务D 两次读取 name 都是 A。
  • 如果数据库的隔离级别是:RC(READ_COMMITTED:读已提交),
    • 事务 D 第一次读取,事务 A 已提交,那么此次读取的 name 是 A
    • 事务 D 第二次读取,事务 B 已提交,那么此次读取的 name 是 B

MVCC 实现机制:基于 undo_log 的版本链

如下图:就是上边事务生成 undo_log 的版本链。

  • id:user 的主键
  • name:修改的字段
  • trx_id:事务ID,作为版本进行标识。
  • dp_roll_ptr:表示改行回滚段的指针。

注意:undo_log 日志是不是会被删除?中间数据万一被删除了版本链不就断了?

undo_log 版本链在回滚完毕后,不是立即删除,MySQL 确保版本链数据不再被 ”引用“ 后,再进行删除。

ReadView 是什么?

ReadView 是 ”快照读“ SQL 执行时 MVCC 提取数据的依据。

  • ”快照读“

    :就是最普通的 Select 查询 SQL 语句。

  • ”当前读“

    :执行下列语句时进行的数据读取的方式

    • Insert、Update、Delete(在这些操作前,需要先读取数据)。
    • Select … for update
    • Select … lock in share mode

ReadView 是一个数据结构

  • m_ids:当前活跃的事务编号集合
  • min_trx_id:最小活跃事务编号
  • max_trx_id:预分配事务编号,当前最大事务编号 + 1
  • creator_trx_id:ReadView 创建者的事务编号

ReadView 生成过程

读已提交(RC)

读已提交(RC):在每一次执行快照读是生成 ReadView

如下图:第一次 Select(快照读)生一个 ReadView

  • m_ids:当前活跃的事务编号集合:【2,3,4】
  • min_trx_id:最小活跃事务编号:2
  • max_trx_id:预分配事务编号,当前最大事务编号 + 1:5
  • creator_trx_id:ReadView 创建者的事务编号:4

第二次 Select(快照读)生一个 ReadView

  • m_ids:当前活跃的事务编号集合:【3,4】
  • min_trx_id:最小活跃事务编号:3
  • max_trx_id:预分配事务编号,当前最大事务编号 + 1:5
  • creator_trx_id:ReadView 创建者的事务编号:4

数据提取过程

验证 trx_id = 3 这一行的数据是否满足访问规则:

  1. trx_id = 3,不等于 creator_trx_id ,不满足第一条规则
  2. trx_id = 3,不小于 min_trx_id = 2,不满足第二条规则
  3. trx_id = 3,不大于 max_trx_id = 5,不满足第三条规则
  4. trx_id = 3,大于 min_trx_id = 2 小于 max_trx_id = 5 满足第一层判断,trx_id 存在 m_ids 集合,说明 trx_id 还未提交,本次的隔离级别是 RC,所以不能读取 trx_id = 3 这行数据。

同理:判断 trx_id = 2 这一行数据(由 db_roll_ptr 找到 trx_id 这条数据),不满足。在 trx_id = 1 这一行数据满足。那么读取到 A。

结合下图:是不是第一个 ReadView 只有 trx_id = 1的事务提交。只能读取 trx_id = 1 的数据。

可重复读(RR)

可重复读(RR):**仅在第一次执行快照读时生成 ReadView,后续快照读复用之前的ReadView(有例外)。**

如下图:第二次快照读复用了第一次的 ReadView,ReadView 就是去 undo_log 的版本链中的查询条件,查询条件相同,所以查到的结果也是相同的。

RR 级别下使用 MVCC 能避免幻读吗?

答:能,但不完全能。MVCC 不是通过锁的机制来对事务数据进行隔离,而是通过版本控制变现的解决了幻读功能。

连续多次快照读,ReadView 会产生复用,没有幻读的问题。

特例:当两次快照读之间存在当前读,ReadView 会重新生成,会产生幻读。

如下图:

  • 事务 B 先查询,获取到 id =1,name = B,age = 18 这条数据。这是快照读,生成 ReadView2

  • 事务 A 插入条数据:id = 2,name = B,age = 20,

  • 事务 B 将 user 表中的所有 age 都更新为 25。此时有一次当前读

  • 事务 B 再次快照读。由于之前的有一次当前读,因此重新生成 ReadView2。此时的 ReadView2 与 ReadView2 数据总量不同,产生了幻读。