Mysql 专栏 – MVCC机制


Mysql 专栏 – MVCC机制

前言

mvcc机制是mysql解决事务问题一项重要机制,通过这个机制,mysql解决了关于事务的问题:脏写、脏读、重复读的问题,但是默认的不可重复读的情况下还是会出现幻读的问题。

概述:

  1. undo log的版本链条和read view的实现
  2. Undo log以及read view如何解决常见的事务问题
  3. 简单介绍关于独占锁和共享锁的内容

mysql事务问题:

事务的问题无非下面两种:

  • 多个事务并发执行的时候,可能会同时对缓存页里的一行数据进行更新,这个冲突怎么处理?是否要加锁?
  • 可能有的事务在对一行数据做更新,有的事务在查询这行数据,这里的冲突怎么处理?

undo log事务回滚的实现

基本介绍:

首先我们需要了解undo log中存在两个重要的属性,一个是「trx_id」,一个是「roll_pointer」,这个trx_id就 是最近一次更新这条数据的事务id,roll_pointer 就是指向你了你更新这个事务之前生成的undo log, 关于undo log之前都讲过了这里不再过多的介绍。

Undo log 版本链

介绍mvcc机制之前,我们需要先了解关于undo log的版本链条的结构 ,很明显这个机制的引入是为了mvcc的机制铺路准备的,下面我们来一一讲解undo log回滚机制

是什么?

undo log 版本链条是通过链表的方式,当同一条记录存在多个事务提交的时候,为了保证事务可以正常回滚,会通过roll_pointer 以及 txd_id 实现多个事务版本的串联,同时串联多个版本的值。

比如下面这个undo log版本链条如下所示:

  1. 首先,事务A写入了一条数据并且写入的值为A,此时事务A id为50,所以在undo log的链条里面存储的也是trx_id
  2. 事务B往这一行记录更新一个值B,此时会更新trx_id 为58,并且roll_point 会指向trx_id 为 50的记录
  3. 如果此时出现事务C更新数据,按照同样的道理也会出现一个链表的节点并且将事务C指向trx_id 为69的记录

至此,一个undo log多版本控制链已经实现,他的结构如下面的形式:

MySQL,MVCC机制,MySQL事务,Undo log,ReadView

这种设计有什么作用?

这种设计的作用是,保证多个事务提交的时候一旦需要回滚操作,可以保证同一个事务只能读到比当前版本更早提交的值,不能看到更晚提交的值。

Read view

是什么?

read view是mvcc机制的实现一个关键组件,是mysql基于undo log多版本链条实现的,在一个事务开启的时候,默认会为当前事务生生成一个read view表,这个表在事务开启的时候所有的参数都确定,并且在事务结束的时候才会销毁。

一个read view当中里面包含了下面的内容,包含4个重要的基本字段,其中最重要的是m_ids以及max_trx_id 以及 min_trx_id这三个字段。

  • 一个是m_ids,这个就是说此时有哪些事务在MySQL里执行还没提交的;
  • 一个是min_trx_id,就是m_ids里最小的值;
  • 一个是max_trx_id,这是说mysql「下一个要生成的事务id」,就是最大事务id;
  • 一个是creator_trx_id,就是你这个事务的id

注意max_trx_id是下一个要生成的事务id,之所以这样设计是方便事务id的判断❞

按照逻辑的简单理解,他的存储结构如下所示,具体的参数的含义会在下文根据实际的案例进行解释,这里只是作为展示数据的存储展示理解即可。

MySQL,MVCC机制,MySQL事务,Undo log,ReadView

检查和读取步骤

了解了上面的基本结构图之后,下面我们来了解一下如何读取和检索来实现一个undo log的回滚操作,为了更好的理解我们需要根据上面的图加入一些模拟的操作来进行解释:

「假设事务A需要读取数据,而事务B需要更新数据,事务A的id为50,事务B为58」,按照read view的设计,最终会出现一副下面的情况:

MySQL,MVCC机制,MySQL事务,Undo log,ReadView

如果不加以任何限制,这里会出现脏读的情况,也就是一个事务可能会读到一个没有提交的值。❞

但是实际上肯定不是这样的,按照上面的undo log链的介绍,事务A需要查询值但是在查询的过程中突然被事务B插了一脚把这个值更新了,此时需要生成一个undo log的记录,并且让其值更新为事务B提交的值,所以实际上结构图会是下面的样子,这里读者可以先暂停一下思考事务A要怎么读,又是怎么去判断的:

MySQL,MVCC机制,MySQL事务,Undo log,ReadView

「关键:此时事务A去读取的时候,自己的id是50,但是发现trx_id却是58,也就是事务的id要比自己操作的记录id要新,但是它是如何知道的?其实就是用了这个read view,此时事务A查询min_trx_id 发现他的min_trx_id(50)是小于当前记录的trx_id(58)的,说明此时很有可能在事务A开启的时候出现了其他差不多时间开启的事务在操作这个数据,于是会看一下m_ids 列表,发现果然有其他事务在进行干扰」

于是事务A就知道这条数据不是他改的,所以它要根据roll_point找到下一条数据(此时可以理解这条数据为事务A操作的快照)并且同样检查他的trx_id是否大于min_trx_id,通过对比发现是和他相等的(都是50),所以可以确定这条数据是事务A修改的数据。最终他的判断结构图如下所示:

MySQL,MVCC机制,MySQL事务,Undo log,ReadView

通过上面的步骤讲解,我们简单了解undo log版本链和read view是如何配合实现mvcc这个机制的:在每一个事务开启的时候,会维护一个read view链表,当多个事务同时操作一个记录行的时候,就会根据undo log版本链构建一个链表结构的记录链条串,通过这两个结构的配合,实现了基本的mvcc的操作。

解决脏读、幻读、重复读问题

有了上面的Read View和undo log链条之后,下面我们来看下如何解决脏读,幻读以及重复读的问题的。其实道理都是互通的这里简单说明一下关键的部分:

脏读:

脏读就是读到一个未提交的数据,根据上面的内容介绍如果事务A读到一个没有提交的值,会根据undo log链找到事务A提交的值,然后只要事务A读取的是自己开启事务的时候看到的值,就没有问题。

不可重复读:

重复读和脏读类似,不过会稍微的绕一下,假设事务A需要读取一个有可能被事务B修改的值,会有两种情况:

1. 读取事务B修改之后的值,2. 读取事务B修改了但是没有提交事务的值。

关于第二点我们可以通过undo log的方式回溯找到事务A之前读取的值并且进行操作即可,这样事务A操作结果就不是脏读的。事务B其实更加好理解,因为事务B已经把事务提交了,「当事务A再次查询会生成一份新的Read View,此时事务B已经不再m_ids里面」,也就是说,虽然事务A发现undo log中的trx_id并不是自己修改的值,但是又发现m_ids事务没有这个未知的“篡改者”,所以可以认定这个事务已经被修改了,这时候操作是没有影响的。

幻读:

mysql防止幻读的操作其实就是在事务B操作提交完成事务之后,事务A发现这个ID是大于当前的max_trx_id的是不能够进行读取的,也就是说它只能读到小于这个max_trx_id的数据,mysql就是通过这种方式来避免幻读问题的产生的。

MySQL,MVCC机制,MySQL事务,Undo log,ReadView

多个事务如何避免脏写?

了解了mvcc机制之后,我们来了解多事务执行的时候如何避免脏写这个问题,也就是如何避免一个事务读到一个未提交的内容:

MySQL,MVCC机制,MySQL事务,Undo log,ReadView

这里我们直接根据一个图进行讲解,当第一个事务访问的时候,此时这个事务就会创建一个锁,里面包含了自己的trx_id和等待状态,然后把锁跟这行数据关联在一 起,同时锁是在内存里面完成操作的,因为操作数据在缓冲区完成的而不是磁盘文件完成。第二个事务过来的时候,发现数据被锁了,所以很无奈,只能进行等待,此时会生成一个锁,并且把等待状态设置为true,表示自己也在等待。关键的一步来了:**事务A执行完成之后会解除锁并且会去找找有没有事务也对这条数据加了锁,结果发现第二条事务也加了锁,于是会去释放这个锁,然后把B唤醒去干活。**这种双锁的设计保证了多线程访问的情况下对于一行数据的访问。

事务隔离级别解决的事务问题

在讲述下一个内容之前,我们先回顾一下事务隔离级别是如何解决事务问题的。

隔离级别脏读不可重复读幻读
读未提交(read uncommitted RU)
读提交(read committed RC)×
可重复读(repeatable read RR)××
串行化(serializable )×××

独占锁和共享锁

讲完了事务我们接下来来简单聊聊关于独占锁和共享锁的特点。

「多个事务运行」的时候mysql加入的是「独占锁」,但是因为使用的了Mvcc的机制,所以又分为了「读锁和写锁」,写锁的优先级是高于读锁的,但是mysqL通过mvcc实现了读写锁的分离操作,也就是一条数据更新的时候,并不影响另一个事务读取数据,这样也保证了互斥锁造成事务阻塞的问题。

共享锁和独占锁是互斥的,你只能加其中的一个锁。最后我们来看看共享锁和独占锁的互斥问题

MySQL,MVCC机制,MySQL事务,Undo log,ReadView

「执行查询操作就是想要加锁怎么办?」

MySQL首先支持一种共享锁,就是S锁,这个共享锁的语法如下:select * from table lock in share mode,你在一个查询语句后面加上lock in share mode,意思就是查询的时候对一行数据加共享锁。

上面提到的锁都是行锁的特性,在多个事务并发更新数据的时候,都是要在行级别加独占锁的,这就是行锁,独占锁都是互斥的,所以不可能发生脏写问题,一个事务提交了才会释放自己的独占锁,唤醒下一个事务执行

所以如果更新数据,会出现下面的情况:

  • 第一种是基于mvcc的事务隔离机制
  • 第二种是基于特殊语法的独占锁和共享锁

需要注意的是dll语句和增删改的操作是互斥的❞

行锁和表锁的加锁规则

如何加表锁

加表锁通常使用下面两条语句,但是实际上这个加锁的操作

LOCK TABLES xxx [READ:这是加表级共享锁](READ:这是加表级共享锁)

LOCK TABLES xxx WRITE:这是加表级独占锁

意向锁

关于意向锁的内容,我们需要了解的是在增删改的时候会进行意向的独占锁,而查询的时候会加入意向的共享锁,什么是意向锁呢?假设,事务A获取了某一行的排它锁,尚未提交,此时事务B想要获取表锁时,必须要确认表的每一行都不存在排他锁,很明显效率会很低,引入意向锁之后,效率就会大为改善:

  1. 如果事务A获取了某一行的排它锁,实际此表存在两种锁,表中某一行的排他锁和表上的意向排他锁。
  2. 如果事务B试图在该表级别上加锁时,则受到上一个意向锁的阻塞,它在锁定该表前不必检查各个页或行锁,而只需检查表上的意向锁。

下面是整个加锁的总结:

MySQL,MVCC机制,MySQL事务,Undo log,ReadView

总结

MySQL实现MVCC机制的时候,是基于undo log多版本链条+ReadView机制来做的,默认的Read uncommit隔离级别,就是基于这套机制来实现的,依托这套机制实现了RR(「repeatable read」)级别,避免脏写、脏读、不可重复读这三个问题,但是无法避免幻读的问题。

在后续的内容中我们介绍了关于事务的隔离级别问题,以及脏写的问题如何避免,最后我们讲述关于加锁的规则和内容,以及简单了解意向锁是什么东西。

写在最后

以上就是关于mvcc的简单了解内容,只要深刻了解undo log和read view这两个组件结合的机制相信mvcc的机制也能很快的理解。