【MySQL】一文彻底吃透MVCC执行原理

1.MVCC是什么?

MVCC全称Multi-Version Concurrency Control,即多版本并发控制。它通过维护数据的多个版本来实现高效的并发控制,用于在多个并发事务同时读写数据库时保持数据的一致性隔离性

在搞清楚MVCC的实现原理之前,还需要了解快照读和当前读的概念。

一致性非锁定读(快照读)

简单的select语句(不加锁)就是快照读,读取的是记录数据的可见版本,不加锁,是非阻塞读。

  • select ...

一致性锁定读(当前读)

读取的是记录的最新版本,读取时需要保证其他并发事务不能修改当前记录,会对读取的记录加锁。如果执行的是下列语句,就是锁定读。

  • select ... lock in share mode
  • select ... for update
  • insertupdatedelete 操作

2.MVCC实现原理

MVCC 的实现依赖于:隐藏字段、Read View、undo log

隐藏字段

在内部,InnoDB 存储引擎为每行数据添加了三个隐藏字段

隐藏字段含义
DB_ROW_ID(6字节)隐藏主键,如果当前表不存在主键,则将该隐藏字段作为主键
DB_TRX_ID(6字节)最近修改事务ID,记录插入这条数据或最后一次修改该记录的事务ID
DB_ROLL_PTR(7字节)回滚指针,指向这条记录的上一个版本,用于配合undo log

假设有一个学生表,该表没有指定主键。

那么该表实际上的字段如下,如果存在主键则不存在DB_ROW_ID字段。

undo log

undo log 分为两种类型:insert undo logupdate undo log

  • Insert undo log 是在事务进行插入操作时生成的日志。其主要作用是用于事务回滚时撤销插入操作。该日志只在回滚时需要,在事务提交后,可被立即删除。
  • Update undo log 是在事务进行更新或删除操作时生成的日志。其主要作用是用于事务回滚时撤销更新和删除操作。该日志不仅在回滚时需要,在快照读时也需要,不会被立即删除。

一条记录的每一次更新操作产生的 undo log 格式都有一个一个 DB_TRX_ID事务id 和 DB_ROLL_PTR 指针:

  • 通过 DB_TRX_ID 可以知道该记录是被哪个事务修改的;
  • 通过 DB_ROLL_PTR 指针可以将这些 undo log 串成一个链表,这个链表就被称为版本链;

举例说明:

事务1已经提前执行了INSERT INTO user (id, age, name) VALUES (10, 10, 'Jack');语句插入了一条记录。则DB_TRX_ID(插入这条数据或最后一次修改该记录的事务ID)为1,由于insert undo log在事务提交后自动删除,所以不存在undo log日志,DB_ROLL_PTR为null。

该示例中有四个并发事务,其他事务在不同的时刻将执行update语句修改记录。

所有事务执行完毕后,当前记录的DB_TRX_ID为4,且形成了一条Update Undo Log版本链,后续MVCC可以利用这条版本链获取旧数据。

Read View

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

ReadView有以下四个重要字段:

字段含义
m_ids当前活跃事务的ID集合
min_trx_id最小活跃事务ID
max_trx_id预分配事务ID,当前最大事务ID+1(事务ID自增)
creator_trx_idReadView创建者的事务ID

Tips:m_ids的长度可不是max_trx_id - min_trx_id,因为m_ids是当前活跃事务的ID集合,在min_trx_idmax_trx_id即可能有活跃事务,也可能有非活跃事务

当一个事务需要读取一条记录时,需要遵循以下四条规则进行读取(非常重要):

  1. DB_TRX_ID == creator_trx_id时,说明该数据就是当前事务更改的,可以访问该版本

  2. DB_TRX_ID < min_trx_id 时,比最小活跃事务ID小,说明当前事务已经提交了,可以访问该版本

  3. DB_TRX_ID > max_trx_id时,比预分配事务ID大,说明当前事务在ReadView生成后才开始,还没有提交不能访问该版本

  4. min_trx_id <= DB_TRX_ID <= max_trx_id 时,如果DB_TRX_ID不在m_ids中,即当前事务已经提交了,可以访问该版本。

看完这些规则我们可以总结以下规律:

  1. 当前事务可以读取自己更改的记录,对应第一条规则
  2. 只有一个事务提交了,才能去读取该事务ID下的版本记录(保证事务的隔离性,防止脏读),对应第二、三、四条规则

3.MVCC的执行流程

这里承接第二部分举过的案例,来具体分析事务5在不同隔离级别两次查询id为10的记录时,分别会读取哪个版本的数据。学会这个案例之后,就能理解MVCC如何解决不可重复读幻读的问题。

RC隔离级别

在RC隔离级别下,事务每一次执行快照读时都会生成一次ReadView。

在第一次查询时,还未提交的事务有3、4、5,那么m_ids(活跃事务ID集合)为{3,4,5},min_trx_id(最小活跃事务ID)为3,max_trx_id(预提交事务ID)为5+1=6,creator_trx_id(事务创建者ID)为5。

在第二次查询时,还未提交的事务有4、5,那么m_ids(活跃事务ID集合)为{4,5},min_trx_id(最小活跃事务ID)为4,max_trx_id(预提交事务ID)为5+1=6,creator_trx_id(事务创建者ID)为5。

RR隔离级别

在RR隔离级别下,仅在事务中第一次执行快照读时生成ReadView,后续复用该ReadView。

在第一次查询时,还未提交的事务有3、4、5,那么m_ids(活跃事务ID集合)为{3,4,5},min_trx_id(最小活跃事务ID)为3,max_trx_id(预提交事务ID)为5+1=6,creator_trx_id(事务创建者ID)为5。

在第二次查询时,直接复用ReadView。

读取版本记录

在读取版本记录时,需要根据DB_TRX_ID匹配ReadView的读取规则,判断当前记录对DB_TRX_ID对应的事务是否可见,如果可见,直接读取当前版本,如果不可见,则读取前一个undo log记录继续进行匹配。

我们以第一个ReadView举例,当前undo log版本链和读视图如下:

当DB_TRX_ID为4,存在于活跃事务列表中,因此不可以读取该行数据,需要向前找DB_TRX_ID为3的记录。

当DB_TRX_ID为3时,同样存在于活跃事务列表,因此不可以读取该行数据,需要向前找DB_TRX_ID为2的记录。

当DB_TRX_ID为2时,发现DB_TRX_ID<min_trx_id,符合规则,因此可以读取该行记录。

最后的读取结果为:

1020Jack20x00001

4.MVCC小结

MVCC解决不可重复读

RC隔离级别

在RC读取已提交下,事务每一次执行快照读时都会生成一次ReadView,这也就造成了每次读取就有不同 ReadView,那么就会读到已提交的事务修改的内容,不能解决不可重复读的问题。

RR隔离级别

解决 RR 不可重复读主要靠 Readview,在隔离级别为可重复读时,仅在事务中第一次执行快照读时生成ReadView,后续复用该ReadView。由于后续复用了 ReadView,所以数据对当前事务的可见性和第一次是一样的,所以从 undo log 中读到的数据快照和第一次是一样的,即便过程中有其他事务修改也读不到。因此解决了不可重复读的问题。

MVCC解决幻读

InnoDB存储引擎在 RR 级别下通过 MVCCNext-key Lock(临键锁) 来解决幻读问题:

1、执行快照读

快照读的情况下,RR 隔离级别使用MVCC,只会在事务开启后的第一次查询生成 Read View ,并使用至事务提交。所以在生成 Read View 之后其它事务所做的更新、插入记录版本对当前事务并不可见,实现了可重复读和防止快照读下的 “幻读”。

2、执行当前读

当前读的情况下,读取的都是最新的数据,如果其它事务有插入新的记录,并且刚好在当前事务查询范围内,就会产生幻读。InnoDB 使用Next-key Lock来防止这种情况。当执行当前读时,会锁定读取到的记录的同时,锁定它们的间隙,防止其它事务在查询范围内插入数据。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值