zoukankan      html  css  js  c++  java
  • MVCC原理解析

    MVCC原理分析

    1:什么是MVCC

    MVCC是英文名称Multi Version Concurrency Control 的简称,就是多版本并发控制。

    MVCC可以说实现,读不加锁,读写不冲突。这个可以大大的提高Mysql的性能。

    2:MVCC解决了什么问题

    多事务的并发进行一般会造成以下几个问题:

    脏读: A事务读取到了B事务未提交的内容,而B事务后面进行了回滚.

    不可重复读: 当设置A事务只能读取B事务已经提交的部分,会造成在A事务内的两次查询,结果竟然不一样,因为在此期间B事务进行了提交操作.

    幻读: A事务读取了一个范围的内容,而同时B事务在此期间插入了一条数据.造成"幻觉".

    MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读

    在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能

    同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题

    3: Mysql的事务隔离级别

    我们先了解一下数据的隔离级别。

    1:RU:读未提交,就是可以读取到其他未提交事务中的数据,存在脏读。

    2:RC:读已提交,只可以读取到已提交的数据,存在幻读。

    3:RR:不可重复读,不可以重复读。

    4:SERIALIZABLE:串行读取,没有脏读,幻读,不可重复读。每次操作都是加锁。性能较低

    MVCC只在REPEATABLE READ和READ COMMITTED两个隔离级别下工作。

    REPEATABLE READ读取之前系统版本号的记录,保证同一个事务中多次读取结果一致。

    REPEATABLE READ隔离级别下,MVCC具体操作:

    SELECT操作,InnoDB会根据以下两个条件检查每行记录:

    a. InnoDB只查找创建版本号早于或等于当前系统版本号的数据行,这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。

    b. 行的删除版本号要么未定义,要么大于当前的系统版本号(在当前事务开始之后删除的)。这可以确保事务读取到的行,在事务开始之前未被删除。

    READ COMMITTED读取最新的版本号记录,就是所有事务最新提交的结果。

    其他两个隔离级别和MVCC不兼容。READ UNCOMMITTED总是读取最新的数据行,而不是符合当前事务版本的数据行。SERIALIZABLE会对所有读取的行都加锁。

    3:MVCC的实现原理

    InnoDB的存储引擎,的每个记录都会存在三个隐藏的键,分别是:DATA_TRX_ID、DATA_ROLL_PTR、DB_ROW_ID。

    • DATA_TRX_ID:记录当前记录的事务ID,大小为6个字节。
    • DATA_ROLL_PTR:表示指向该行回滚段(rollback segment)的指针,大小为 7 个字节,InnoDB 便是通过这个指针找到之前版本的数据。该行记录上所有旧版本,在 undo 中都通过链表的形式组织。
    • DB_ROW_ID:行标识(隐藏单调自增 ID),大小为 6 字节,如果表没有主键,InnoDB 会自动生成一个隐藏主键(row_id并不是必要的,我们创建的表中有主键或者非NULL唯一键时都不会包含row_id列)

    再undo.log中形成一个这样的版本链。

    理论解释:

    这里开创了三个事务,

    假设我们最初的数据的事务Id是80,那他的数据结构如图所示:

    假设之后两个id分别为100200的事务对这条记录进行UPDATE操作,操作流程如下:

    每次对记录进行改动,都会记录一条undo日志,每条undo日志也都有一个roll_pointer属性(INSERT操作对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些undo日志都连起来,串成一个链表,所以现在的情况就像下图一样:

    ReadView

    对于使用READ UNCOMMITTED隔离级别的事务来说,直接读取记录的最新版本就好了,对于使用SERIALIZABLE隔离级别的事务来说,使用加锁的方式来访问记录。对于使用READ COMMITTEDREPEATABLE READ隔离级别的事务来说,就需要用到我们上边所说的版本链了,核心问题就是:需要判断一下版本链中的哪个版本是当前事务可见的。所以设计InnoDB的大叔提出了一个ReadView的概念,这个ReadView中主要包含当前系统中还有哪些活跃的读写事务,把它们的事务id放到一个列表中,我们把这个列表命名为为m_ids。这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见:

    • 如果被访问版本的trx_id属性值小于m_ids列表中最小的事务id,表明生成该版本的事务在生成ReadView前已经提交,所以该版本可以被当前事务访问。

    • 如果被访问版本的trx_id属性值大于m_ids列表中最大的事务id,表明生成该版本的事务在生成ReadView后才生成,所以该版本不可以被当前事务访问。

    • 如果被访问版本的trx_id属性值在m_ids列表中最大的事务id和最小事务id之间,那就需要判断一下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。

    如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本,如果最后一个版本也不可见的话,那么就意味着该条记录对该事务不可见,查询结果就不包含该记录。

    MySQL中,READ COMMITTEDREPEATABLE READ隔离级别的的一个非常大的区别就是它们生成ReadView的时机不同,我们来看一下。

    4:实践验证RR,RC隔离级别下的MVCC

    RC模式下:

    现在通过两个事务来处理一个数据,解读一下MVCC的实现原理。这里事务提交方式改为手动,事务隔离级别改为RC

    1: 查询结果为:刘备

    2:执行事务100,不提交

    我们可以得到再undo.log中的一个这样的版本链

    这个时候我们再执行,查询,可以看到查询结果

    可以看到结果还是刘备。

    分析:

    • 在执行SELECT语句时会先生成一个ReadViewReadViewm_ids列表的内容就是[100]

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

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

    • 下一个版本的列c的内容是'刘备',该版本的trx_id值为80,小于m_ids列表中最小的事务id100,所以这个版本是符合要求的,最后返回给用户的版本就是这条列c'刘备'的记录。

    接下来我们执行操作:提交事务100的操作,同时执行事务200的操作,但是不提交,然后执行查询操作。

    我们可以看到查询结果,为张飞

    分析:这个时候的版本链是这样的

    • 在执行SELECT语句时会先生成一个ReadViewReadViewm_ids列表的内容就是[200](事务id为100的那个事务已经提交了,所以生成快照时就没有它了)。

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

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

    • 下一个版本的列c的内容是'张飞',该版本的trx_id值为100,比m_ids列表中最小的事务id200还要小,所以这个版本是符合要求的,最后返回给用户的版本就是这条列c'张飞'的记录。

    以此类推,如果之后事务id为200的记录也提交了,再此在使用READ COMMITTED隔离级别的事务中查询表tid值为1的记录时,得到的结果就是'诸葛亮'了,具体流程我们就不分析了。总结一下就是:使用READ COMMITTED隔离级别的事务在每次查询开始时都会生成一个独立的ReadView。

    RR模式下的MVCC

    对于使用REPEATABLE READ隔离级别的事务来说,只会在第一次执行查询语句时生成一个ReadView,之后的查询就不会重复生成了。我们还是用例子看一下是什么效果。

    同样,按照上面的操作,重新操作。

    事务100,操作,但是不提交,事务200 不进行操作,然后查询,

    结果就是这样,原因和之前的一样,可以分析。这里就不在分析了。

    接下来我们执行查询,但是查询事务不提交,然后提交事务100;同时执行事务200,但是不提交;然后再次执行查询

    分析:

    上面执行可以得到undo.log的版本链

    • 因为之前已经生成过ReadView了,所以此时直接复用之前的ReadView,之前的ReadView中的m_ids列表就是[100, 200]

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

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

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

    • 下一个版本的列c的内容是'刘备',该版本的trx_id值为8080小于m_ids列表中最小的事务id100,所以这个版本是符合要求的,最后返回给用户的版本就是这条列c'刘备'的记录。

    也就是说两次SELECT查询得到的结果是重复的,记录的列c值都是'刘备',这就是可重复读的含义。如果我们之后再把事务id为200的记录提交了,之后再到刚才使用REPEATABLE READ隔离级别的事务中继续查找这个id为1的记录,得到的结果还是'刘备',具体执行过程大家可以自己分析一下。

    总结

    从上边的描述中我们可以看出来,所谓的MVCC(Multi-Version Concurrency Control ,多版本并发控制)指的就是在使用READ COMMITTDREPEATABLE READ这两种隔离级别的事务在执行普通的SEELCT操作时访问记录的版本链的过程,这样子可以使不同事务的读-写写-读操作并发执行,从而提升系统性能。READ COMMITTDREPEATABLE READ这两个隔离级别的一个很大不同就是生成ReadView的时机不同,READ COMMITTD在每一次进行普通SELECT操作前都会生成一个ReadView,而REPEATABLE READ只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复这个ReadView就好了。

  • 相关阅读:
    Palindrome Partitioning
    triangle
    Populating Next Right Pointers in Each Node(I and II)
    分苹果(网易)
    Flatten Binary Tree to Linked List
    Construct Binary Tree from Inorder and Postorder Traversal(根据中序遍历和后序遍历构建二叉树)
    iOS系统navigationBar背景色,文字颜色处理
    登录,注销
    ios 文字上下滚动效果Demo
    经常崩溃就是数组字典引起的
  • 原文地址:https://www.cnblogs.com/simple-flw/p/14724238.html
Copyright © 2011-2022 走看看