参考自波波烤鸭:MySQL数据库的核心MVCC详解

一、前置内容

1.事务的ACID

image-20230405183226152

2.MySQL的核心日志

在MySQL数据库中有三个非常重要的日志binlog,undolog,redolog.

image-20230405183255274

image-20230405183439653

3.隔离级别

 1、脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据**

​ 2、不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。

 3、幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

 小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表

  • MySQL事务隔离级别
事务隔离级别 脏读 不可重复读 幻读
读未提交(read-uncommitted) RU
不可重复读(read-committed)RC
可重复读(repeatable-read)mysql默认 RR
串行化(serializable)

二、MVCC

1.什么是MVCC

​ MVCC(Multi-Version Concurrency Control):多版本并发控制,是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。
  MVCC 在 MySQL InnoDB 中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读

2.什么是当前读和快照读

类型 说明
快照读(普通读) 普通的 select 语句。执行方式是生成 readview,直接利用 MVCC 机制来读取,并不会对记录进行加锁。它是基于多版本并发控制即 MVCC机制,既然是多版本,那么快照读读到的数据不一定是当前最新的数据,有可能是之前历史版本的数据。
如下的操作是快照读:
1、 不加锁的 select 操作 (前提 :事务级别不是串行化,串行化的是快照读=当前读)
当前读(锁定读) 它读取的记录都是数据库中当前的最新版本会对当前读取的数据进行加锁,防止其他事务修改数据,这种锁是一种悲观锁。
当前读的规则,就是要能读到所有已经提交的记录的最新值
如下操作都是当前读:
1、 select … lock in share mode 当前读,加读锁 ,也叫共享锁
2、 select … for update 当前读,加写锁,又叫排他锁
3、 innoDB 里面 update (排他锁)、insert (排他锁)、delete (排他锁),都会自动给涉及的语句添加写锁。
4、 串行化事务的隔离级别
实现方式:next-key(行记录锁+间隙锁)即临键锁,是前开后闭区间。

3.当前读快照读 与 MVCC的关系

首先三者都是一种概念,

MySQL的InonDB利用 3 个隐式字段,undo 日志 ,Read View 实现了MVCC

MVCC 在不使用锁的前提下 就能解决了 并发环境下 读写冲突的问题,也就实现了快照读(非阻塞读)的功能

当前读是一种 必须加锁的读,与MVCC无关了

4.MVCC的好处

  首先我们要清楚数据库中的并发场景有三种,分别是

image-20230406105442359

然后来看MVCC的好处:
  多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。 所以 MVCC 可以为数据库解决以下问题:

1、在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
2、同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题

5.MVCC工作原理

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

三个隐藏字段

在这里插入图片描述

DB_TRX_ID 是当前操作该记录的事务 ID ,

而 DB_ROLL_PTR 是一个回滚指针,用于配合 undo日志,指向上一个旧版本(上一个修改)
举例如:

DB_ROW_ID 是数据库默认为该行记录生成的唯一隐式主键(InnoDB表如果没有主键或者唯一索引会自动生成这种隐式主键的聚簇索引)

undo log 版本链

所谓版本是针对不同事物的每一次修改来的,不是针对事务

我同时开启了如下事务

image-20230406112000039

image-20230406112256853

不同事务或相同事务对同一条记录进行修改,会导致该记录的undolog生成一条记录版本链表,链表的头部是最新的旧记录,链表尾部是最早的旧记录

Read View

Read View作用:决定快照读时,读取undo log版本链中的哪一个条记录

ReadView(读视图)是快照读SQL执行时MVCC提取数据的依据,记录并维护系统当前活跃的事务(未提交的) id。

生成ReadView的时机

  • RC:在事务中每一次执行快照读时生成ReadView。
  • RR:仅在事务中第一次执行快照读时生成ReadView,后续复用该ReadView。

ReadView有四个核心字段

字段 含义
m_ids 当前活跃事务id集合
min_trx_id 最小活跃事务id
max_trx_id 预分配事务id,当前最大事务+1(因为事务id是自增的)也即下一个要分配的事务id
creator_trx-id ReadView创建者的事务id

Read View中规定了版本链数据的访问规则( trx_id 代表当前undo log版本链上节点对应事务ID )

条件 条件 说明
trx_id==creator_trx_id 可以访问该版本 数据是当前这个事务更改的,自己创建的事务当然可以访问啊 ;
trx_id < min_trx_id 可以访问该版本 访问的事务已经肯定已经提交了 ,不管是谁创建的事务,只要提交了就是共享的,当然可以访问
trx_id > max_trx_id 不可以访问该版本(只能说明不能访问,不能说明可以访问,作用是否决) 该事务是在ReadView生成后才开启 ,太早了
min_trx_id<= trx_id<=max_trx_id 如果trx_id不在m_ids中,可以访问该版本 如果条件成立,数据已经提交

如果undo log版本链的头节点记录(最新记录),四个条件判断后的结果都是不可访问(只要有一个说明可以访问就能访问),则根据回滚指针找到上一个版本的记录,继续判断,直到找到一个可以访问的记录。

RC级别下

第一次快照读

m_ids(活跃事务id):3,4,5 (不包括2,读之前已经提交了)

min_trx = min m_ids

max_trx = max m_ids +1

creator_trx_id = 所在事务的id

第二次快照读

image-20230406115008890

对第一次快照读,

把最新的 db_trx_id=4 带入

1、 不满足等于创建者 ,不能访问该版本

2、不满足小于最小事务版本 ,不满足访问该版本

3、不满足 ,

4、满足 3<=4<=6 但是不满足 不在活跃县城里面,所以不可以访问

访问下个版本 db_trx_id=3 带入

都不满足 不成立

访问下个版本 db_trx_id=2 带入

可以,事务2已提交可以访问 把这个版本的记录返回

RR级别下

RR级别下只有第一次执行快照读时生成ReadView,后续复用该ReadView。保证了下次每次读取和前面保持一致

除非commit 重新读取

三、MVCC是否解决了幻读?

严格意义上并没有解决幻读MVCC

利用版本链,undo log,Read View可以在快照读模式下解决幻读问题,并且不用加锁解决读写冲突问题,极大的增加了数据库的并发量。

但在当前读模式下仅仅依靠MVCC不能解决幻读问题,必须依赖next-key锁(行锁+GAP锁)来解决,这是因为当前读必须获取最新数据