mysql----MVCC

一、前言

  我们都知道mysql在近些年新版本中默认是innodb引擎,隔离级别为RR,即可重复读。而RR的实现又是基于MVCC的.。大多数时候我们看到MVCC这几个字母也只是能道出这是版本并发控制的意思。其特点就是在同一时间,不同事务可以读取到不同版本的数据,从而去解决脏读和不可重复读,最重要的核心便是解决了读写直接不阻塞的问题,提高了事务之间的并发性。再往深的说就一知半解了,这篇文章就一起来深入探索下MVCC底层的实现原理。

二、当前读与快照度

   常规情况下同一行数据平时发生读写请求时,会上锁阻塞住。但mvcc用更好的方式去处理读—写请求,做到在发生读—写请求冲突时不用加锁。这个读是指的快照读,而不是当前读,当前读是一种加锁操作,是悲观锁。那它到底是怎么做到读—写不用加锁的,快照读和当前读又是什么鬼,下面我们就一起来认识下设么是快照度与当前读。

2.1 当前读:

   它读取的数据库记录,都是当前最新的版本,会对当前读的数据进行加锁,防止其他事物修改数据。数悲观锁的一种操作。如下操作都属于当前读:

  • insert
  • delete
  • update
  • select for update
  • select lock in share mode

2.2 快照读:

快照读的实现是基于多版本并发控制,即MVCC,既然是多版本,那么快照读读到的数据不一定是当前最新的数据,有可能是之前历史版本的数据。如下操作属于快照读:

不加锁的select操作(注:事务级别不是串行化)

快照读,即普通的select语句。当前读,即相当于一种悲观锁,操作时需要进行加锁。MVCC都是基于快照读,目的是为了提高读写时候避免锁的竞争从而提升性能。

三、事务特性

我们都知道事务的实现需要保证四个特性即ACID,那么它们又是靠什么实现的呢?

  • atomicity—undolog
  • durability—redolog
  • isolation—锁+MVCC
  • consistency—一致性靠上面三点共同保证

3.1 数据库并发场景及隔离级别

  • 读读:不存在任何问题,也不需要并发控制

  • 读写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读

  • 写写:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失

四种隔离级别:

  • 读未提交(READ UNCOMMITTED):又称为脏读,一个事务可以读取到另一个事务未提交的数据。这种隔离级别岁最不安全的一种,因为未提交的事务是存在回滚的情况。

  • 读已提交(READ COMMITTED):一个事务提交之后,它做的变更才会被其他事务看到。又称为不可重复读,一个事务因为读取到另一个事务已提交的修改数据,导致在当前事务的不同时间读取同一条数据获取的结果不一致。
    举个例子,在下面的例子中就会发现SessionA在一个事务期间两次查询的数据不一样。原因就是在于当前隔离级别为 RC,SessionA的事务可以读取到SessionB提交的最新数据。
    在这里插入图片描述

  • 可重复读(REPEATABLE READ):又称为幻读,一个事物读可以读取到其他事务提交的数据,但是在RR隔离级别下,当前读取此条数据只可读取一次,在当前事务中,不论读取多少次,数据任然是第一次读取的值,不会因为在第一次读取之后,其他事务再修改提交此数据而产生改变。因此也成为幻读,因为读出来的数据并不一定就是最新的数据。举个例子:在SessionA中第一次读取数据时,后续其他事务修改提交数据,不会再影响到SessionA读取的数据值。此为可重复读。

在这里插入图片描述

  • 串行化(SERIALIZABLE):对于同一行记录,“写”会加“写锁”,“读”会加“读锁”,当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。

3.2 MVCC在保证隔离性的前提下到底解决了什么问题?

mvcc用来解决读—写冲突的无锁并发控制,就是为事务分配单向增长的时间戳。为每个数据修改保存一个版本,版本与事务时间戳相关联。

读操作只读取该事务开始前的数据库快照。

解决问题如下:

并发读-写时:可以做到读操作不阻塞写操作,同时写操作也不会阻塞读操作。

解决脏读、幻读、不可重复读等事务隔离问题,但不能解决上面的写-写 更新丢失问题。

因此有了下面提高并发性能的组合拳:

MVCC + 悲观锁:MVCC解决读写冲突,悲观锁解决写写冲突

MVCC + 乐观锁:MVCC解决读写冲突,乐观锁解决写写冲突

四、实现原理

4.1 MVCC基本构成

它的实现原理主要包含版本链,undolog ,Read View三大部分。

InnoDB表中有三个隐藏字段,这三个字段是MySQL默认帮助我们添加的,通过代码可以查看到。比如,
在这里插入图片描述
注:rowid只在特定情况下才能显示
1 、当表中有主键并且是数值型的时候才是显示的
2、当表中没有主键的时候,但是表中有唯一非空并且是数值型的时候才是显示的

   对于使用InnoDB存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列(row_id并不是必要的,我们创建的表中有主键或者非NULL唯一键时都不会包含row_id列):

  • db_row_id:6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以db_row_id产生一个聚簇索引。
  • trx_id:6byte,最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务ID。
  • roll_pointer:7byte,回滚指针,指向这条记录的上一个版本(存储于rollback segment里)

4.2 事务内部变化

我们抛开数据库中的ACID,这里就涉及一个问题,在修改数据后,数据库是怎么做到查询的结果还是之前的数据的呢?其实就是借助于数据库中的Undolog和MVCC来实现的:
在这里插入图片描述当插入一条数据时,在记录上对应的回滚段指针为NULL,上图所示。
在这里插入图片描述当更新记录时,将原记录放入Undo表空间中,我们查询看到的未修改的数据就是从Undo表空间中返回的,如果存在多个数据的版本就会构成一个链表。在MySQL中就是根据记录上的回滚段指针及事务ID判断记录是否可见的。具体的判断流程如下:在每个事务开始时,都会将当前系统中所有的活跃事务拷贝到一个列表(ReadView)中。当读取一行记录时,会根据行记录上的TRX_ID值与Read View中的最大TRX_ID值、最小TRX_ID值的比较来判断是否可见。比较TRX_ID值是否小于Read View中的最小TRX_ID值,如果是,则说明此事务早于Read View中的所有事务结束,可以输出返回;如果不是,则判断TRX_ID值是否大于Read View中的最大TRX_ID值。

  • 如果是,则根据行记录上的回滚段指针找到回滚段中的对应记录且取出TRX_ID值赋给当前行的TRX_ID,并重新执行比较操作(说明此行记录在事务开始之后发生了变化)。
  • 如果不是,则判断TRX_ID值是否在Read View中。如果在Read View中,则根据行记录上的回滚段指针找到回滚段中的对应记录且取出TRX_ID值(说明此行记录在事务开始时处于活跃状态);如果不是,则返回记录。
    在这里插入图片描述

4.3 undolog

Undo log 主要用于记录数据被修改之前的日志,在表信息修改之前先会把数据拷贝到undo log里。当事务进行回滚时可以通过undo log 里的日志进行数据还原。

4.3.1 Undo log 的用途

  • 保证事务进行rollback时的原子性和一致性,当事务进行回滚的时候可以用undo log的数据进行恢复。
  • 用于MVCC快照读的数据,在MVCC多版本控制中,通过读取undo log的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据版本。

4.3.2 undolog主要分为两种

  • insert undolog

代表事务在insert新记录时产生的undo log , 只在事务回滚时需要,并且在事务提交后可以被立即丢弃

  • update undolog(主要)

事务在进行update或delete时产生的undolog ; 不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除

4.4 Read View(读视图)

事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照。

记录并维护系统当前活跃事务的ID(没有commit,当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以越新的事务,ID值越大),是系统中当前不应该被本事务看到的其他事务id列表。

Read View主要是用来做可见性判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undolog里面的某个版本的数据。

4.4.1 Read View几个属性

在这里插入图片描述

4.4.2 Read View可见性判断条件

在这里插入图片描述

  • db_trx_id < up_limit_id || db_trx_id == creator_trx_id(显示)

如果数据事务ID小于read view中的最小活跃事务ID,则可以肯定该数据是在当前事务启之前就已经存在了的,所以可以显示。

或者数据的事务ID等于creator_trx_id ,那么说明这个数据就是当前事务自己生成的,自己生成的数据自己当然能看见,所以这种情况下此数据也是可以显示的。

  • db_trx_id >= low_limit_id(不显示)

如果数据事务ID大于read view 中的当前系统的最大事务ID,则说明该数据是在当前read view 创建之后才产生的,所以数据不显示。如果小于则进入下一个判断

  • db_trx_id是否在活跃事务(trx_ids)中

不存在:则说明read view产生的时候事务已经commit了,这种情况数据则可以显示。

已存在:则代表我Read View生成时刻,你这个事务还在活跃,还没有Commit,你修改的数据,我当前事务也是看不见的。

在这里插入图片描述

五、MVCC和事务隔离级别

上面所讲的Read View用于支持RC(Read Committed,读提交)和RR(Repeatable Read,可重复读)隔离级别的实现。

RR、RC生成时机
RC隔离级别下,是每个快照读都会生成并获取最新的Read View;

而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View,之后的查询就不会重复生成了,所以一个事务的查询结果每次都是一样的。

解决幻读问题
快照读:通过MVCC来进行控制的,不用加锁。按照MVCC中规定的“语法”进行增删改查等操作,以避免幻读。

当前读:通过next-key锁(行锁+gap锁)来解决问题的。

RC、RR级别下的InnoDB快照读区别
在RR级别下的某个事务的对某条记录的第一次快照读会创建一个快照及Read View, 将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,还是使用的是同一个Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见;这里需要注意,如果有需求在事务开始时就要创建Read View,则可在执行starttransaction语句时指定WITH CONSISTENT SNAPSHOT参数。其实在mysqldump执行时内部就是用这种方式开启事务的。

即RR级别下,快照读生成Read View时,Read View会记录此时所有其他活动事务的快照,这些事务的修改对于当前事务都是不可见的。而早于Read View创建的事务所做的修改均是可见

而在RC级别下的,事务中,每次快照读都会新生成一个快照和Read View, 这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因。

六、LBCC 解决数据丢失

在这里插入图片描述上面的两种情况就是对于一条数据,多个事务同时操作可能会产生的问题,会出现某个事务的操作被覆盖而导致数据丢失。

LBCC 解决数据丢失

LBCC,基于锁的并发控制,Lock Based Concurrency Control。

使用锁的机制,在当前事务需要对数据修改时,将当前事务加上锁,同一个时间只允许一条事务修改当前数据,其他事务必须等待锁释放之后才可以操作。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值