懵了!女友忽然问我MVCC实现原理

前言

都知道事务的可重复读级别实现原理是使用MVCC实现的,那么你对MVCC的底层实现原理知道多少呢?面试高频点,你值得拥有。web

1、MVCC究竟是什么?

MVCC即多版本控制器,其特色就是在同一时间,不一样事务能够读取到不一样版本的数据,从而去解决脏读和不可重复读的问题。面试

在这里插入图片描述
在这里插入图片描述

这样的解释你看了不下几十遍了吧!可是你真的理解什么是多版本控制器吗?数据库

生活案例:搬家数组

最近小Q跟本身的女友搬到新家,因为出小区的时候须要支付当月的物业费。安全

因而小Q跟本身的女友同时登陆了小区提供的物业缴费系统。并发

悲观并发控制mvc

假设小Q正在查当月须要缴纳的费用是多少进行支付的时候,此时小Q查询的这条数据是已经被锁定的。编辑器

那么小Q女友是没法访问该数据的,直至小Q支付完成或者退出系统将悲观锁释放,小Q的女友才能够查询到数据。学习

悲观锁保证在同一时间只能有一个线程访问,默认数据在访问的时候会产生冲突,而后在整个过程都加上了锁。字体

这样的系统对于用户来讲就是毫无体验感,若是多我的同时须要访问一条信息,只能在一台设备上看喽!

乐观并发控制

在小Q查看物业费欠费状况,而且支付的同时,小Q的女友也能够访问到该数据。

乐观锁认为即便在并发环境下,也不会产生冲突问题,因此不会去作加锁操做。

而是在数据提交的时候进行检测,若是发现有冲突则返回冲突信息。

小结

Innodb的MVCC机制就是乐观锁的一种体现,读不加锁,读写不冲突,在不加锁的状况下能让多个事务进行并发读写,而且解决读写冲突问题,极大的提升系统的并发性

2、悲观锁、乐观锁

锁按照粒度分为表锁、行锁、页锁。

按照使用方式分为共享锁、排它锁。

根据思想分为乐观锁、悲观锁。

不管是乐观锁、悲观锁都只是一种思想而已,并非实际的锁机制,这点必定要清楚。

1. 悲观锁(悲观并发控制)

悲观锁实际为悲观并发控制,缩写PCC。

悲观锁持消极态度,认为每一次访问数据时,老是会发生冲突,所以,每次访问必须先锁住数据,完成访问后在释放锁。

保证在同一时间只有单个线程能够访问,实现数据的排它性。同时悲观锁使用数据库自身的锁机制实现,能够解决读-写,写-写的冲突。

那么在什么场景下可使用悲观锁呢!

悲观锁适用于在写多读少的并发环境下使用,虽然并发效率不高,可是保证了数据的安全性。

2. 乐观锁(乐观并发控制)

跟悲观锁同样,乐观锁实际为乐观并发控制,缩写为OCC。

乐观锁相对于悲观锁而言,认为即便在并发环境下,外界对数据的操做不会产生冲突,因此不会去加锁,而是会在提交更新的时候才会正式的对数据冲突与否进行检测。

若是发现冲突,要么再重试一次,要么切换为悲观的策略。

乐观并发控制要解决的是数据库并发场景下的写-写冲突,指用无锁的方式去解决

3、MVCC解决了哪些问题

在事务并发的状况下会产生如下问题。

  • 脏读:读取其它事务未提交的数据。
  • 不可重复读:一个事务在读取一条数据时,因为另外一个事务修改了这条数据而且提交事务,再次读取时致使数据不一致
  • 幻读:一个事务读取了某个范围的数据,同时另外一个事务新增了这个范围的数据,再次读取发现俩次获得的结果不一致。

MVCC在Innodb存储引擎的实现主要是为了提升数据库并发能力,用更好的方式去处理读--写冲突,同时作到不加锁、非阻塞并发读写。

mvcc能够解决脏读,不可重复读,mvcc使用快照读解决了部分幻读问题,可是在修改时仍是使用当前读,因此仍是存在幻读问题,幻读问题最终就是使用间隙锁解决。

4、当前读、快照读

在了解MVCC是如何解决事务并发带来的问题以前,须要先明白俩个概念,当前读、快照读。

1. 当前读

给读操做加上共享锁、排它锁,DML操做加上排它锁,这些操做就是当前读。

共享锁、排它锁也被称之为读锁、写锁。

共享锁与共享锁是共存的,可是要修改、添加、删除时,必须等到共享锁释放才可进行操做。

由于在Innodb存储引擎中,DML操做都会隐式添加排它锁。

因此说当前读所读取的记录就是最新的记录,读取数据时加上锁,保证其它事务不能修改当前记录。

2. 快照读

若是你看到这里就默认你对隔离级别有必定的了解哈!

快照读的前提是隔离级别不是串行级别,串行级别的快照读会退化成当前读。

快照读的出现旨在提升事务并发性,其实现基于本文的主角MVCC即多版本控制器。

MVCC能够认为是行锁的一个变种,可是它在不少状况下避免了加锁操做。

因此说快照读的数据有可能不是最新的,而是以前版本的数据。

为何要提到快照读呢!由于read-view就是经过快照读生成的,为了防止后文概念模糊,因此在这里进行说明。

3. 如何区分当前读、快照读

不加锁的简单的select都属于快照读。

select id name user where id = 1;

与之对应的则是当前读,给select加上共享锁、排它锁。

select id name from user where id = 1 lock in share mode;

select id name from user where id = 1 for update;

5、MVCC实现三大要素

终于来到本文最重要的部分,前边的叙述都是为了给原理这一块作铺垫。

在这以前须要知道MVCC只在REPEATABLE READ(可重复读) 和 READ COMMITTED(已读提交)这俩种隔离级别下适用。

MVCC实现原理是由俩个隐式字段、undo日志、Read view来实现的。

1. 隐式字段

在Innodb存储引擎中,在有聚簇索引的状况下每一行记录中都会隐藏俩个字段,若是没有聚簇索引则还有一个6byte的隐藏主键。

这俩个隐藏列一个记录的是什么时候被建立的,一个记录的是何时被删除。

这里不要理解为是记录的是时间,存储的是事务ID。

俩个隐式字段为DB_TRX_ID,DB_ROLL_PTR,没有聚簇索引还会有DB_ROW_ID这个字段。

  • DB_TRX_ID:记录建立这条数据上次修改它的事务 ID
  • DB_ROLL_PTR:回滚指针,指向这条记录的上一个版本

隐式字段实际还有一个delete flag字段,即记录被更新或删除,这里的删除并不表明真的删除,而是将这条记录的delete flag改成true(这里埋下一个伏笔,数据库的删除是真的删除吗?)

2. undo log(回滚日志)

以前对undo log的做用只提到了回滚操做实现原子性,如今须要知道的另外一个做用就是实现MVCC多版本控制器。

undo log细分为俩种,insert时产生的undo log、update,delete时产生的undo log

在Innodb中insert产生的undo log在提交事务以后就会被删除,由于新插入的数据没有历史版本,因此无需维护undo log。

update和delete操做产生的undo log都属于一种类型,在事务回滚时须要,并且在快照读时也须要,则须要维护多个版本信息。只有在快照读和事务回滚不涉及该日志时,对应的日志才会被purge线程统一删除。

purge线程会清理undo log的历史版本,一样也会清理del flag标记的记录。

undo log在mvcc中的做用

写到这里关于undo log在mvcc中的做用估计仍是蒙圈的。

undo log保存的是一个版本链,也就是使用DB_ROLL_PTR这个字段来链接的。

当数据库执行一个select语句时会产生一致性视图read view

那么这个read view是由查询时全部未提交事务ID组成的数组,数组中最小的事务ID为min_id和已建立的最大事务ID为max_id组成,查询的数据结果须要跟read-view作比较从而获得快照结果。

因此说undo log在mvcc中的做用就是为了根据存储的事务ID和一致性视图作对比,从而获得快照结果。

3. undo log底层实现

假设一开始的数据为下图

开始数据
开始数据

此时执行了一条更新的SQL语句update user set name = 'niuniu where id = 1',那么undo log的记录就会发生变化

也就是说当执行一条更新语句时会把以前的原有数据拷贝到undo log日志中。

同时你能够看见最新的一条记录在末尾处链接了一条线,也就是说DB_ROLL_PTR记录的就是存放在undo log日志的指针地址。

最终有可能须要经过指针来找到历史数据。

undo log变化
undo log变化

4. read-view

当执行SQL语句查询时会产生一致性视图,也就是read-view,它是由查询的那一时间全部未提交事务ID组成的数组,和已经建立的最大事务ID组成的。

在这个数组中最小的事务ID被称之为min_id,最大事务ID被称之为max_id,查询的数据结果要根据read-view作对比从而获得快照结果。

因而就产生了如下的对比规则,这个规则就是使用当前的记录的trx_id跟read-view进行对比,对比规则以下。

5. 版本链对比规则

若是落在trx_id<min_id,表示此版本是已经提交的事务生成的,因为事务已经提交因此数据是可见的

若是落在trx_id>max_id,表示此版本是由未来启动的事务生成的,是确定不可见的

若在min_id<=trx_id<=max_id时

  • 若是row的trx_id在数组中,表示此版本是由还没提交的事务生成的,不可见,可是当前本身的事务是可见的
  • 若是row的trx_id不在数组中,代表是提交的事务生成了该版本,可见

在这里还有一个特殊状况那就是对于已经删除的数据,在以前的undo log日志讲述时说了update和delete是同一种类型的undo log,一样也能够认为delete就是update的特殊状况。

当删除一条数据时会将版本链上最新的数据复制一份,而后将trx_id修改成删除时的trx_id,同时在该记录的头信息中存在一个delete flag标记,将这个标记写上true,用来表示当前记录已经删除。

在查询时按照版本链的规则查询到对应的记录,若是delete flag标记位为true,意味着数据已经被删除,则不返回数据。

若是你对这里的read-view的生成和版本链对比规则不懂,不要着急,也不要在这里浪费时间,请继续往下看,咔咔会使用一个简单的案例和一个复杂的案例给你们重现上述的规则。

6、MVCC底层原理

案例一

下图是准备的素材,这里应该都理解select 返回的结果为niuniu,即事务102修改后的结果

案例一
案例一

从上图中能够看到有三个事务正在进行。

事务ID为100、101是修改的其它表,只有事务ID为102修改的须要查询的这张表。

接下来看看select这一列查询返回的结果是否是就是事务ID为102修改的结果。

此时生成的read-view为[100,101],102

那么如今就能够返回去看一下read-view规则,在这里事务ID100就是min_id,事务ID102就是max_id。

这个 select语句返回结果确定是 niuniu。

那么接下来看一下在MVCC中是如何查找数据的。

当前版本链。

此时的版本链
此时的版本链

那么就会拿着trx_id 为102进行比对,会发现这个102就是max_id

而后你再看一下版本链的对比规则中第三种状况

若是落在min_id<=trx_id<=max_id会存在俩种状况

此时信息就已经很是明确了,事务ID102是没有在数组中的,因此表示这个版本是已经提交的事务生成的,那么就是可见的呗!

毫无疑问查询会返回niuniu这个值

先经过这个简单的案例让你对版本链有一个简单的理解,接下来将使用一个比较繁琐的案例再来跟你们演示一遍。

案例二

本例要求知道 select的第二个查询结果。深黑色字体。

一样是在kaka那一条记录的基础上。

案例二
案例二

当事务ID100两次更新后,版本链也会改变,如今的版本链以下图,红色部分为最新数据,蓝色数据为undo log的版本链数据。

此时的版本链
此时的版本链

对于此时生成的read-view你会有什么疑问,在RR级别也就是可重复读的隔离级别下。

当在一个事务下执行查询时,全部的read-view都是沿用的第一条查询语句生成的。

那此时的read-view也就是[100,101],102

看一下底层查找步骤

  • 目前数据的事务ID为100
  • 根据规则会落在min_id<=trx_id<=max_id这个区间
  • 而且当前行的事务ID100是在read-view的数组中的,表示此时事务尚未提交则不可见
  • 继续在版本链中往下寻找,此时找到的事务ID仍是100,跟上述流程一致
  • 经过查找版本链,将发现事务 ID为102
  • 102是read-view的max_id,一样也会落在min_id<=trx_id<=max_id这个区间,可是跟以前不一样的是事务102是没有在数组中的,表示这个版本事务已经提交了因此是可见的
  • 最后返回的是 niuniu

案例三

为了让你们体验一下可重复读级别生成的read-view是根据在同一事务中第一条快照读产生的,再来看一个案例

此时的事务ID101也再对数据更新两次,而后在进行查询看一下会返回什么值

案例三
案例三

通过案例1、案例二的熟悉如今对undo log的版本链和对比规则已经有了必定的了解了吧!

案例三就不在那么详细的说明了。

此时的版本链以下

案例三的版本链
案例三的版本链

此时的read-view依然为[100,101],102。

那么首先会根据事务101去版本链对比,事务101和事务100都会落在min_id<=trx_id<=max_id这个区间,而且还都在数组中,因此数据是不可见的。

那么继续往版本链中寻找就会遇到事务102,这个是最大的事务ID而且不在数组中,因此是可见的。

因而最终的返回结果仍是niuniu

案例四

能够看到个案例三的图不一样的是新增了一个查询语句,那么假设这俩条语句执行的时间都是一致的,它们返回的结果会相同吗?

案例三查询到的值为niuniu

案例四
案例四

其实如今版本链跟案例三也是一致的

版本链
版本链

那么来梳理一下寻找过程

  • 首先这里的read-view发生了变化,此时的read-view为 [101],102
  • 拿着当前的事务ID101跟版本链规则进行对比,落盘在min_id<=trx_id<=max_id,而且在数组中,则数据不可见
  • 而后进入版本链,找到下一个数据的事务 ID,仍是101,与上一个一致
  • 接下来是事务ID100
  • 事务ID100是落在trx_id<min_id,表示此版本是已经提交的事务生成的,因为事务已经提交因此数据是可见的
  • 因此最终返回结果为 niuniu2

小结

在同一个事务中进行查询,会沿用第一次查询语句生成的read-view(前提是隔离级别是在可重复读)

经过以上的四个案例,在版本链寻找过程当中,能够总结出一个小技巧

技巧图
技巧图

根据这个小技巧你能够很快的得知此版本是否可见。

  • 若是当前的事务ID在绿色部分,是已经提交事务,则数据可见
  • 若是当前的事务ID在蓝色部分,会有俩种状况,若是当前事务ID在read-view数组内,是没有提交的事务不可见,若是不在数组内数据可见
  • 若是落在红色部分,则不考虑,对于将来的事情不去想便可。

7、总结

阅读本文后,在面试过程当中极大可能会遇到的问题就是聊聊你对mvcc的认识

本文内容从浅到深,从什么是mvcc到mvcc的底层实现,一步一步地陈述了mvcc的实现原理。

本文简单总结

  • mvcc在不加锁的状况下解决了脏读、不可重复读和快照读下的幻读问题,必定不要认为幻读彻底是mvcc解决的
  • 对当前读、快照读理解,简单点说加锁就是当前读,不加锁的就是快照读。
  • mvcc实现的三大要素俩个隐式字段、回滚日志、read-view
  • 俩个隐式字段:DB_TRX_ID:记录建立这条记录最后一次修改该记录的事务ID,DB_ROLL_PTR:回滚指针,指向这条记录的上一个版本
  • undo log在更新数据时会产生版本链,是read-view获取数据的前提
  • read-view当SQL执行查询语句时产生的,是由为提交的事务ID组成的数组和建立的最大事务ID组成的
  • 版本链规则看第六节的小结便可

坚持学习、坚持写做、坚持分享是咔咔从业以来一直所秉持的信念。但愿在偌大互联网中咔咔的文章能带给你一丝丝帮助。我是咔咔,下期见。

相关文章
相关标签/搜索