概述
MVCC是什么?
MVCC 是为了解决事务操作中多线程并发安全问题的无锁并发控制技术。它的全称为:多版本并发控制。
为什么需要MVCC?
我们可以根据数据库三种并发场景来分析:
- 读读并发,不会产生并发问题,也不需要并发控制。
- 读写并发,会造成事务隔离问题,造成脏读、幻读、不可重复读的问题。
- 写写并发,可能出现数据更新丢失的问题。
MVCC 为每个修改保存一个版本,版本与事务时间戳关联,读操作只读事务开始前的数据库的快照。它是通过 Undo 日志和ReadView 来实现的。
MVCC 为数据库解决一下问题:
- 并发并发读写数据库时,读操作时不用去阻塞写操作,而写操作也不用去阻塞读操作,提高数据库的并发读写的处理能力。
- 实现一致性,解决脏读、幻读、不可重复读等事务隔离问题。但是它不经解决写写并发时,数据丢失问题。
- 采用乐观锁或者悲观锁来解决写写冲突。
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 这一行的数据是否满足访问规则:
- trx_id = 3,不等于 creator_trx_id ,不满足第一条规则
- trx_id = 3,不小于 min_trx_id = 2,不满足第二条规则
- trx_id = 3,不大于 max_trx_id = 5,不满足第三条规则
- 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 数据总量不同,产生了幻读。