什么是MySQL MVCC的ReadView?

背景

MySQL InnoDB RR隔离级别是否能够避免幻读?

我们都知道,在MySQL InnoDB中,支持四种事物隔离级别,分别为:

1、READ UNCOMMITED(未提交读):使用查询语句不会加锁,允许脏读,也就是可能读取到其他会话中未提交事务修改的数据。

2、READ COMMITED(提交读):只能读取到已经提交的数据,只对记录加记录锁,而不会在记录之间加间隙锁,所以允许新的记录插入到被锁定记录的附近,所以再多次使用查询语句时,可能得到不同的结果(Non-Repeatable Read)。

3、REPEATABLE READ(可重复读):多次读取同一范围的数据会返回第一次查询的快照,会返回相同的数据行。

4、SERIALIZABLE(串行读):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞。

其中,REPEATABLE READ(下文中简称RR) 是MySQL的默认事务隔离级别,而RR的核心实现机制,是基于MVCC(Multi-Version Concurrency Control) 机制来进行实现的。

MVCC中,非常重要的一个实现机制则是依赖于ReadView,那么什么是MVCC的 ReadView,本篇我们就来一起探讨一下。

MySQL InnoDB MVCC机制

在前面的文章中,我们介绍过MySQL的MVCC机制,这里我们做个简单回顾。

MVCC多版本控制指的是一种提高并发的技术。

最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。

引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了InnoDB的并发度。

InnoDB的一致性的非锁定读就是通过MVCC实现的,MySQL的大多数事务型存储引擎实现的都不是简单的行级锁。

基于提升并发性能的考虑,它们一般都同时实现了多版本并发控制(MVCC)。

MVCC的实现,是通过保存数据在某一个时间点的快照来实现的。因此每一个事务无论执行多长时间看到的数据,都是一样的。

MVCC实现细节

MVCC的核心实现主要基于两部分:多事务并发操作数据与一致性读实现

多事务并发操作数据

多事务并发操作数据核心基于Undo log进行实现,Undo log可以用来做事务的回滚操作,保证事务的原子性。

同时可以用来构建数据修改之前的版本,支持多版本读。

InnoDB中,每一行记录都有两个隐藏列:DATA_TRX_ID和DATA_ROLL_PTR。(若没有主键,则还有一个隐藏主键)

  • DATA_TRX_ID:记录最近更新这条记录的事务ID(6字节)
  • DATA_ROLL_PTR:指向该行回滚段的指针,通过指针找到之前版本,通过链表形式组织(7字节)
  • DB_ROW_ID:行标识(隐藏单增ID),没有主键时主动生成(6字节)

当存在多个事务进行并发操作数据时,不同事务对同一行的更新操作产生多个版本,通过回滚指针将这些版本链接成一条Undo Log链。

操作过程如下:

  • 1、将待操作的行加排他锁。

  • 2、将该行原本的值拷贝到Undo Log中,DB_TRX_IDDB_ROLL_PTR保持不变。(形成历史版本)

  • 3、修改该行的值,更新该行的DATA_TRX_ID为当前操作事务的事务ID,将DATA_ROLL_PTR指向第二步拷贝到Undo Log链中的旧版本记录。(通过DB_ROLL_PTR可以找到历史记录)

  • 4、记录Redo Log,包括Undo Log中的修改。

  • INSERT操作:产生新的记录,其DATA_TRX_ID为当前插入记录的事务ID。

  • DELETE操作:软删除,将DATA_TRX_ID记录下删除该记录的事务ID,真正删除操作在事务提交时完成。

一致性读实现

在InnoDB中,对于不同的事务隔离级别,一致性读实现均不相同,具体如下:

  • READ UNCOMMITED隔离级别:直接读取版本的最新记录。
  • SERIALIZABLE隔离级别:通过加锁互斥访问数据实现。
  • READ COMMITEDREPEATABLE READ隔离级别:使用版本链实现。(ReadView,可读性视图)

对于RCRR隔离级别,实现一致性读都是通过ReadView,也就是今天的重点,什么是ReadView

MVCC ReadView

ReadView是事务开启时,当前所有活跃事务(还未提交的事务)的一个集合,ReadView数据结构决定了不同事务隔离级别下,数据的可见性。

MySQL官方文档说明

Read view lists the trx ids of those transactions for which a consistent read should not see the modifications to the database.

ReadView的数据结构如下示例:
ReadView

ReadView的组成:

up_limit_id:
The read should see all trx ids which are strictly smaller (<) than this value.
In other words, this is the low water mark".

low_limit_id:
The read should not see any transaction with trx id >= this value.
In other words, this is the “high water mark”.

m_ids:
Set of RW transactions that was active when this snapshot was taken.

  • up_limit_id:最先开始的事务,该SQL启动时,当前事务链表中最小的事务id编号,也就是当前系统中创建最早但还未提交的事务
  • low_limit_id:最后开始的事务,该SQL启动时,当前事务链表中最大的事务id编号,也就是最近创建的除自身以外最大事务编号
  • m_ids:当前活跃事务ID列表,所有事务链表中事务的id集合

注:ID越小,事务开始的越早;ID越大,事务开始的越晚

OK,ReadVeiw核心数据结构如上所示,我们来解读一下两个核心字段low_limit_idup_limit_id


首先需要明确两个重点概念(敲黑板!!!):

  • 1、下面所说的db_trx_id,是来自于数据行中的db_trx_id字段,并非开启了一个事务分配的ID,分配的事务ID只有操作了数据行,才会更新数据行中的db_trx_id字段
  • 2、ReadView是与SQL绑定的,而并不是事务,所以即使在同一个事务中,每次SQL启动时构造的ReadViewup_trx_idlow_trx_id也都是不一样的

up_limit_id表示“低水位”,即当时活跃事务列表的最小事务id(最早创建的事务),如果读取出来的数据行上的的db_trx_id小于up_limit_id,则说明这条记录的最后修改在ReadView创建之前,因此这条记录可以被看见。

if (trx_id < view->up_limit_id) {
	return(TRUE);
}

low_limit_id表示“高水位”,即当前活跃事务的最大id(最晚创建的事务),如果读取出来的数据行上的的db_trx_id大于low_limit_id,则说明这条记录的最后修改在ReadView创建之后,因此这条记录肯定不可以被看见。

if (trx_id > view->low_limit_id) {
	return(FALSE);
}

如果读取出来的数据行上的的db_trx_idlow_limit_idup_limit_id之间,则查找该数据上的db_trx_id是否在ReadViewm_ids列表中:

  • 如果存在,则表示这条记录的最后修改是在ReadView创建之时,被另外一个活跃事务所修改,所以这条记录也不可以被看见。
  • 如果不存在,则表示这条记录的最后修改在ReadView创建之前,所以可以看到。

这里非常的绕,如果没看懂,请您多读几遍,仔细理解一下这个过程。

不同的事务隔离级别下,生成ReadView的时机则各不相同,下面我们分别来看看一下RRRCReadView

REPEATABLE READ下的ReadView生成

每个事务首次执行SELECT语句时,会将当前系统所有活跃事务拷贝到一个列表中生成ReadView

每个事务后续的SELECT操作复用其之前生成的ReadView

UPDATE,DELETE,INSERT对一致性读snapshot无影响。

示例:事务A,B同时操作同一行数据

  • 若事务A的第一个SELECT在事务B提交之前进行,则即使事务B修改记录后先于事务A进行提交,事务A后续的SELECT操作也无法读到事务B修改后的数据。
  • 若事务A的第一个SELECT在事务B修改数据并提交事务之后,则事务A能读到事务B的修改。

针对RR隔离级别,在第一次创建ReadView后,这个ReadView就会一直持续到事务结束,也就是说在事务执行过程中,数据的可见性不会变,所以在事务内部不会出现不一致的情况。

READ COMMITED下的ReadView生成

每次SELECT执行,都会重新将当前系统中的所有活跃事务拷贝到一个列表中生成ReadView

针对RC隔离级别,事务中的每个查询语句都单独构建一个ReadView,所以如果两个查询之间有事务提交了,两个查询读出来的结果就不一样。

总结一下:
RC的本质:每一条SELECT都可以看到其他已经提交的事务对数据的修改,只要事务提交,其结果都可见,与事务开始的先后顺序无关。
RR的本质:第一条SELECT生成ReadView前,已经提交的事务的修改可见。

从这里可以看出,在InnoDB中,RR隔离级别的效率是比RC隔离级别的高。

此外,针对RU隔离级别,由于不会去检查可见性,所以在一条SQL中也会读到不一致的数据。

针对串行化隔离级别,InnoDB是通过锁机制来实现的,而不是通过多版本控制的机制,所以性能很差。

结语

本篇,我们了解了InnoDB中的MVCC的实现机制,这里来总结一下:

  • MVCC的核心实现主要基于两部分:多事务并发操作数据与一致性读实现
  • 多事务并发操作数据核心基于Undo Log进行实现
  • 一致性读实现核心基于ReadView进行实现

关于MVCC的实现,还是极为复杂的,本文仅谈了一点皮毛,如果您对更多的核心细节感兴趣,还是建议阅读官方文档或其他权威资料,感谢您的阅读。

本篇参考:
ReadView Class Reference
MySQL · 引擎特性 · InnoDB 事务系统
MySQL InnoDB MVCC实现
MySQL中的MVCC

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值