MVCC到底是什么?这一篇博客就够啦

MVCC简单理解

MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。
这是百度百科给的标准的回答

转化成自己的语言:

多版本的意思就是数据库中同时存在多个版本的数据,并不是整个数据库的多个版本,而是某一条记录的多个版本同时存在,在某个事务对其进行操作的时候,需要查看这一条记录的隐藏列事务版本id,比对事务id并根据事物隔离级别去判断读取哪个版本的数据。
这几个名词要记住,能通过博客把他们都搞懂就okok啦

  • 事务隔离级别
  • 一条完整的数据记录
  • undo日志
  • ReadView

MVCC的好处\作用

  1. MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读
  2. 我们都知道并发访问数据库造成的四种问题(脏写(修改丢失)、脏读、不可重复读、幻读),MVCC就是在尽量减少锁使用的情况下高效避免这些问题

数据库的四种隔离级别

隔离界别 脏读 不可重复读 幻读
READ UNCOMMITTED:未提交读 可能发生 可能发生 可能发生
READ COMMITTED:已提交读 解决 可能发生 可能发生
REPEATABLE READ:可重复读 解决 解决 可能发生
SERIALIZABLE:可串行化 解决 解决 解决

为什么没有脏写?
四种问题按照严重性排序:脏写 > 脏读 > 不可重复读 > 幻读
脏写这个问题太严重了,不论是哪种隔离级别,都不允许脏写的情况发生。

好!那下面就开始进入正题……

MVCC的实现原理

一、依赖于隐藏的两个列
我在之前的博客中提到过在InnoDB下的Compact行结构,有三个隐藏的列

列名 是否必须 描述
row_id 行ID,唯一标识一条记录(如果定义主键,它就没有啦)
transaction_id 事务ID
roll_pointer DB_ROLL_PTR是一个回滚指针,用于配合undo日志,指向上一个旧版本

二、版本链

假设初始添加一个数据。如图:

MVCC到底是什么?这一篇博客就够啦_第1张图片
有两个事务同时进行更新信息,事务执行流程:
MVCC到底是什么?这一篇博客就够啦_第2张图片

为什么两个事务执行的顺序有偏差?
很简单,如果能够同时交叉修改同一个数据,那不就是“修改丢失(脏写)”并发问题了吗,mysql在执行操作的时候,会对其加锁,另外一个事务就要暂时挂起

那更新了这么多次的数据,他就还是一条数据吗,不,他会在roll_pointer处记录就近一次的更次记录,然后以此指向前面的更新数据:
MVCC到底是什么?这一篇博客就够啦_第3张图片

对该记录每次更新后,都会将旧值放到一条undo日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被roll_pointer属性连接成一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值。另外,每个版本中还包含生成该版本时对应的事务id

三、ReadView

什么是Read View?
说白了Read View就是事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)

作用:
可以判断一下版本链中的哪个版本是当前事务可见的。那他是如何判断的呢?下面详细介绍。

其最重要的四个部分:

1、m_ids:表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。
2、min_trx_id:表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值。
3、max_trx_id:表示生成ReadView时系统中应该分配给下一个事务的id值。
4、creator_trx_id:表示生成该ReadView的事务的事务id。

用ReadView判断哪个版本的数据可读的过程:

  • 如果被访问版本的trx_id属性值与ReadView中的creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
  • 如果被访问版本的trx_id属性值小于ReadView中的min_trx_id值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。
  • 如果被访问版本的trx_id属性值大于或等于ReadView中的max_trx_id值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
  • 如果被访问版本的trx_id属性值在ReadView的min_trx_id和max_trx_id之间,那就需要判断一下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。

但是对于READ COMMITTED(读取已提交) 和REPEATABLE READ(可重复读)两种隔离界别来说产生ReadViem是不同的,下面我们来就这两个隔离级别说一下,如何判断是否可读某个历史版本记录

READ COMMITTED(读取已提交)— 每次读取数据前都生成一个ReadView

下面过程一定认真看(我第一遍粗略看没有挨着读,结果什么也不明白,后来认真读了一遍,尽可能明白每一个步骤所描述的,看下来之后就会有恍然大明白的感觉!!!)

1、比方说现在系统里有两个事务id分别为100、200的事务在执行:

Transaction 100
BEGIN;

UPDATE hero SET name = ‘关羽’ WHERE number = 1;

UPDATE hero SET name = ‘张飞’ WHERE number = 1;

Transaction 200
BEGIN;

更新了一些别的表的记录 …

注意此时两个事务都没有进行提交
2、此刻,表hero中number为1的记录得到的版本链表如下所示:
MVCC到底是什么?这一篇博客就够啦_第4张图片
3、假设现在有一个使用READ COMMITTED隔离级别的事务开始执行:

BEGIN;

SELECT1:Transaction 100、200未提交
SELECT * FROM hero WHERE number = 1;
得到的列name的值为’刘备’

那这个select的语句能都读取到的数据就是我们最关心的啦!
过程:

  • 在执行SELECT语句时会先生成一个ReadView,ReadView的m_ids列表的内容就是[100,
    200],min_trx_id为100,max_trx_id为201,creator_trx_id为0。

  • 然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列name的内容是’张飞’,该版本的trx_id值为100,在m_ids列表内,所以不符合可见性要求,根据roll_pointer跳到下一个版本。

  • 下一个版本的列name的内容是’关羽’,该版本的trx_id值也为100,也在m_ids列表内,所以也不符合要求,继续跳到下一个版本。

  • 下一个版本的列name的内容是’刘备’,该版本的trx_id值为80,小于ReadView中的min_trx_id值100,所以这个版本是符合要求的,最后返回给用户的版本就是这条列name为’刘备’的记录。

4、之后,我们把事务id为100的事务提交一下
5、然后再到事务id为200的事务中更新一下表hero中number为1的记录(只有事务一执行完之后事务2才能执行,这个原因上面已经说过啦)

Transaction 200
BEGIN;

更新了一些别的表的记录 …

UPDATE hero SET name = ‘赵云’ WHERE number = 1;

UPDATE hero SET name = ‘诸葛亮’ WHERE number = 1;

此刻版本链是这样的:
MVCC到底是什么?这一篇博客就够啦_第5张图片
6、然后再到刚才使用READ COMMITTED隔离级别的事务中继续查找这个number为1的记录(我们知道上面读取了一次,本次和上次的属于同一个事务不同次操作),如下

BEGIN;

SELECT1:Transaction 100、200均未提交
SELECT * FROM hero WHERE number = 1;
得到的列name的值为’刘备’

SELECT2:Transaction 100提交,Transaction 200未提交
SELECT * FROM hero WHERE
number = 1; # 得到的列name的值为’张飞’

7、这个SELECT2的执行过程如下:

  • 在执行SELECT语句时会又会单独生成一个ReadView,该ReadView的m_ids列表的内容就是[200](事务id为100的那个事务已经提交了,所以再次生成快照时就没有它了),min_trx_id为200,max_trx_id为201,creator_trx_id为0。
  • 然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列name的内容是’诸葛亮’,该版本的trx_id值为200,在m_ids列表内,所以不符合可见性要求,根据roll_pointer跳到下一个版本。
  • 下一个版本的列name的内容是’赵云’,该版本的trx_id值为200,也在m_ids列表内,所以也不符合要求,继续跳到下一个版本。
  • 下一个版本的列name的内容是’张飞’,该版本的trx_id值为100,小于ReadView中的min_trx_id值200,所以这个版本是符合要求的,最后返回给用户的版本就是这条列name为’张飞’的记录。

你认真看完了吗?如果明白了过程,你就记住这句话就行,以后遇到类似的场景就没有问题:
使用READ COMMITTED隔离级别的事务在每次查询开始时都会生成一个独立的ReadView。

REPEATABLE READ —— 在第一次读取数据时生成一个ReadView

我们还用上面的一样的场景看一下,进行对比,区别就显而易见啦

我们从事务100提交之后开始说(因为前面的操作是一样的,一样会读到“刘备”这一条数据,不过需要注意前面已经建立了一次ReadView

1、然后再到事务id为200的事务中更新一下表hero中number为1的记录:
此时版本链是这样的:
MVCC到底是什么?这一篇博客就够啦_第6张图片

2、然后再到刚才使用REPEATABLE READ隔离级别的事务中继续查找这个number为1的记录,如下:

使用REPEATABLE READ隔离级别的事务
BEGIN;

SELECT1:Transaction 100、200均未提交
SELECT * FROM hero WHERE number = 1;
得到的列name的值为’刘备’

SELECT2:Transaction 100提交,Transaction 200未提交
SELECT * FROM hero WHEREnumber = 1;
得到的列name的值仍为’刘备’

过程:

  • 因为当前事务的隔离级别为REPEATABLE READ,而之前在执行SELECT1时已经生成过ReadView了,所以此时直接复用之前的ReadView,之前的ReadView的m_ids列表的内容就是[100, 200],min_trx_id为100,max_trx_id为201,creator_trx_id为0。

  • 然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列name的内容是’诸葛亮’,该版本的trx_id值为200,在m_ids列表内,所以不符合可见性要求,根据roll_pointer跳到下一个版本。

  • 下一个版本的列name的内容是’赵云’,该版本的trx_id值为200,也在m_ids列表内,所以也不符合要求,继续跳到下一个版本。

  • 下一个版本的列name的内容是’张飞’,该版本的trx_id值为100,而m_ids列表中是包含值为100的事务id的,所以该版本也不符合要求,同理下一个列name的内容是’关羽’的版本也不符合要求。继续跳到下一个版本。

  • 下一个版本的列name的内容是’刘备’,该版本的trx_id值为80,小于ReadView中的min_trx_id值100,所以这个版本是符合要求的,最后返回给用户的版本就是这条列c为’刘备’的记录。

也就是说两次SELECT查询得到的结果是重复的

下面个人的理解,如有错误可以指正:
ReadView可以查到又名“快照读”,每次执行“快照读”,就好像给数据库拍了一个照片,你拍到了什么就可以读到什么,当然,如果你自己要在照片上画一个“小狗”,那这个“小狗”你也是可以看到了,但是如果你拍了一个杯子,在你拍完之后,别人往杯子里加了点水,你能在照片上看到吗?当然不能。ReadView中的m_ids事务id列表就是你拍照前在你面前跃跃欲试的人,所以你要记录好他们。所以就是利用事务id的先后顺序来判断能否读到某个版本信息。

上面信息和图片借鉴掘金小册《MySQL 是怎样运行的:从根儿上理解 MySQL》

你可能感兴趣的:(mysql,mysql,MVCC,多版本并发控制,java)