MySQL-InnoDB-MVCC多版本并发控制

(Multiversion Concurrency Control)html

前言

最近正在啃《高性能MySQL》这本书, 当看到事务相关知识时,决定对该知识点稍微深刻一下, 《高性能MySQL》中在介绍事务相关知识点时, 显然不是特别深刻, 不少比较底层的知识点并无太多的深刻, 固然此处并非要对本书作什么评判,言归正传, 这里主要先说一下本人在啃相关知识点时的曲折之路:mysql

  1. 首先是事务相关ACID特性, 以前已经有相关笔记进行过介绍, 这里再也不重复;
  2. 接下来是高并发事务相关的问题, 像是 脏读, 不可重复读, 幻读, 更新丢失等问题以前也有相关笔记;
  3. 再下来就是MySQL应对高并发事务是如何给出解决方案的(其中包含各个隔离级别的简介);
  4. 而后就是各个隔离级别的具体介绍及与锁的关系, 也就是在这部分知识点, 发现了以前并无过多关心的知识点 MVCC多版本并发控制, 而后一发不可收拾了...

入题

下面先引用一些前辈们比较优秀的文章:git

阿里数据库内核'2017/12'月报中对MVCC的解释是:
多版本控制: 指的是一种提升并发的技术。最先的数据库系统,只有读读之间能够并发,读写,写读,写写都要阻塞。引入多版本以后,只有写写之间相互阻塞,其余三种操做均可以并行,这样大幅度提升了InnoDB的并发度。在内部实现中,与Postgres在数据行上实现多版本不一样,InnoDB是在undolog中实现的,经过undolog能够找回数据的历史版本。找回的数据历史版本能够提供给用户读(按照隔离级别的定义,有些读请求只能看到比较老的数据版本),也能够在回滚的时候覆盖数据页上的数据。在InnoDB内部中,会记录一个全局的活跃读写事务数组,其主要用来判断事务的可见性。

<高性能MySQL>中对MVCC的部分介绍github

  • MySQL的大多数事务型存储引擎实现的其实都不是简单的行级锁。基于提高并发性能的考虑, 它们通常都同时实现了多版本并发控制(MVCC)。不只是MySQL, 包括Oracle,PostgreSQL等其余数据库系统也都实现了MVCC, 但各自的实现机制不尽相同, 由于MVCC没有一个统一的实现标准。
  • 能够认为MVCC是行级锁的一个变种, 可是它在不少状况下避免了加锁操做, 所以开销更低。虽然实现机制有所不一样, 但大都实现了非阻塞的读操做,写操做也只锁定必要的行。
  • MVCC的实现方式有多种, 典型的有乐观(optimistic)并发控制 和 悲观(pessimistic)并发控制。
  • MVCC只在 READ COMMITTEDREPEATABLE READ 两个隔离级别下工做。其余两个隔离级别够和MVCC不兼容, 由于 READ UNCOMMITTED 老是读取最新的数据行, 而不是符合当前事务版本的数据行。而 SERIALIZABLE 则会对全部读取的行都加锁。

从书中能够了解到:算法

  • MVCC是被Mysql中 事务型存储引擎InnoDB 所支持的;
  • 应对高并发事务, MVCC比单纯的加锁更高效;
  • MVCC只在 READ COMMITTEDREPEATABLE READ 两个隔离级别下工做;
  • MVCC可使用 乐观(optimistic)锁悲观(pessimistic)锁来实现;
  • 各数据库中MVCC实现并不统一
  • 可是书中提到 "InnoDB的MVCC是经过在每行记录后面保存两个隐藏的列来实现的"(网上也有不少此类观点), 但其实并不许确, 能够参考MySQL官方文档, 能够看到, InnoDB存储引擎在数据库每行数据的后面添加了三个字段, 不是两个!!

相关概念

1.read view, 快照snapshotsql

淘宝数据库内核月报/2017/10/01/
此文虽然是以PostgreSQL进行的说明, 但并不影响理解, 在"事务快照的实现"该部分有细节须要注意:
事务快照是用来存储数据库的事务运行状况。一个事务快照的建立过程能够归纳为:
查看当前全部的未提交并活跃的事务,存储在数组中
选取未提交并活跃的事务中最小的XID,记录在快照的xmin中
选取全部已提交事务中最大的XID,加1后记录在xmax中

注意: 上文中在PostgreSQL中snapshot的概念, 对应MySQL中, 其实就是你在网上看到的read view,快照这些概念;
好比何登成就有关于Read view的介绍;
此文 却还是使用快照来介绍;数据库

2.read view 主要是用来作可见性判断的, 比较广泛的解释即是"本事务不可见的当前其余活跃事务", 但正是该解释, 可能会形成一节理解上的误区, 因此此处提供两个参考, 供给你们避开理解误区:数组

read view中的`高水位low_limit_id`能够参考 https://github.com/zhangyachen/zhangyachen.github.io/issues/68, https://www.zhihu.com/question/66320138
其实上面第1点中加粗部分也是相关高水位的介绍( 注意进行了+1 )

3.另外, 对于read view快照的生成时机, 也很是关键, 正是由于生成时机的不一样, 形成了RC,RR两种隔离级别的不一样可见性;并发

  • 在innodb中(默认repeatable read级别), 事务在begin/start transaction以后的第一条select读操做后, 会建立一个快照(read view), 将当前系统中活跃的其余事务记录记录起来;
  • 在innodb中(默认repeatable committed级别), 事务中每条select语句都会建立一个快照(read view);
  • 参考
With REPEATABLE READ isolation level, the snapshot is based on the time when the first read operation is performed.
 使用REPEATABLE READ隔离级别,快照是基于执行第一个读操做的时间。
With READ COMMITTED isolation level, the snapshot is reset to the time of each consistent read operation.
使用READ COMMITTED隔离级别,快照被重置为每一个一致的读取操做的时间。

4.undo-log高并发

  • Undo log是InnoDB MVCC事务特性的重要组成部分。当咱们对记录作了变动操做时就会产生undo记录,Undo记录默认被记录到系统表空间(ibdata)中,但从5.6开始,也可使用独立的Undo 表空间。
  • Undo记录中存储的是老版本数据,当一个旧的事务须要读取数据时,为了能读取到老版本的数据,须要顺着undo链找到知足其可见性的记录。当版本链很长时,一般能够认为这是个比较耗时的操做(例如bug#69812)。
  • 大多数对数据的变动操做包括INSERT/DELETE/UPDATE,其中INSERT操做在事务提交前只对当前事务可见,所以产生的Undo日志能够在事务提交后直接删除(谁会对刚插入的数据有可见性需求呢!!),而对于UPDATE/DELETE则须要维护多版本信息,在InnoDB里,UPDATE和DELETE操做产生的Undo日志被归成一类,即update_undo
  • 另外, 在回滚段中的undo logs分为: insert undo logupdate undo log

    • insert undo log : 事务对insert新记录时产生的undolog, 只在事务回滚时须要, 而且在事务提交后就能够当即丢弃。
    • update undo log : 事务对记录进行delete和update操做时产生的undo log, 不只在事务回滚时须要, 一致性读也须要,因此不能随便删除,只有当数据库所使用的快照中不涉及该日志记录,对应的回滚日志才会被purge线程删除。

5.InnoDB存储引擎在数据库每行数据的后面添加了三个字段

  • 6字节的事务ID(DB_TRX_ID)字段: 用来标识最近一次对本行记录作修改(insert|update)的事务的标识符, 即最后一次修改(insert|update)本行记录的事务id。
    至于delete操做,在innodb看来也不过是一次update操做,更新行中的一个特殊位将行表示为deleted, 并不是真正删除
  • 7字节的回滚指针(DB_ROLL_PTR)字段: 指写入回滚段(rollback segment)的 undo log record (撤销日志记录记录)。
    若是一行记录被更新, 则 undo log record 包含 '重建该行记录被更新以前内容' 所必须的信息。
  • 6字节的DB_ROW_ID字段: 包含一个随着新行插入而单调递增的行ID, 当由innodb自动产生汇集索引时,汇集索引会包括这个行ID的值,不然这个行ID不会出如今任何索引中。
    结合聚簇索引的相关知识点, 个人理解是, 若是咱们的表中没有主键或合适的惟一索引, 也就是没法生成聚簇索引的时候, InnoDB会帮咱们自动生成汇集索引, 但聚簇索引会使用DB_ROW_ID的值来做为主键; 若是咱们有本身的主键或者合适的惟一索引, 那么聚簇索引中也就不会包含 DB_ROW_ID 了 。
    关于聚簇索引, 《高性能MySQL》中的篇幅对我来讲已经够用了, 稍后会整理一下之前的学习笔记, 而后更新上来。

6.可见性比较算法(这里每一个比较算法后面的描述是创建在rr级别下,rc级别也是使用该比较算法,此处未作描述)
设要读取的行的最后提交事务id(即当前数据行的稳定事务id)为 trx_id_current
当前新开事务id为 new_id
当前新开事务建立的快照read view 中最先的事务id为up_limit_id, 最迟的事务id为low_limit_id(注意这个low_limit_id=未开启的事务id=当前最大事务id+1)
比较:

  • 1.trx_id_current < up_limit_id, 这种状况比较好理解, 表示, 新事务在读取该行记录时, 该行记录的稳定事务ID是小于, 系统当前全部活跃的事务, 因此当前行稳定数据对新事务可见, 跳到步骤5.
  • 2.trx_id_current >= trx_id_last, 这种状况也比较好理解, 表示, 该行记录的稳定事务id是在本次新事务建立以后才开启的, 可是却在本次新事务执行第二个select前就commit了,因此该行记录的当前值不可见, 跳到步骤4。
  • 3.trx_id_current <= trx_id_current <= trx_id_last, 表示: 该行记录所在事务在本次新事务建立的时候处于活动状态,从up_limit_id到low_limit_id进行遍历,若是trx_id_current等于他们之中的某个事务id的话,那么不可见, 调到步骤4,不然表示可见。
  • 4.从该行记录的 DB_ROLL_PTR 指针所指向的回滚段中取出最新的undo-log的版本号, 将它赋值该 trx_id_current,而后跳到步骤1从新开始判断。
  • 5.将该可见行的值返回。

案例分析

  1. 下面是一个很是简版的演示事务对某行记录的更新过程, 固然, InnoDB引擎在内部要作的工做很是多:
    clipboard.png
  2. 下面是一套比较算法的应用过程, 比较长
    比较算法

当前读和快照读

1.MySQL的InnoDB存储引擎默认事务隔离级别是RR(可重复读), 是经过 "行排他锁+MVCC" 一块儿实现的, 不只能够保证可重复读, 还能够部分防止幻读, 而非彻底防止;

2.为何是部分防止幻读, 而不是彻底防止?

  • 效果: 在若是事务B在事务A执行中, insert了一条数据并提交, 事务A再次查询, 虽然读取的是undo中的旧版本数据(防止了部分幻读), 可是事务A中执行update或者delete都是能够成功的!!
  • 由于在innodb中的操做能够分为当前读(current read)快照读(snapshot read):

3.快照读(snapshot read)

简单的select操做(固然不包括 select ... lock in share mode, select ... for update)

4.当前读(current read) 官网文档 Locking Reads

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

在RR级别下,快照读是经过MVVC(多版本控制)和undo log来实现的,当前读是经过加record lock(记录锁)和gap lock(间隙锁)来实现的。
innodb在快照读的状况下并无真正的避免幻读, 可是在当前读的状况下避免了不可重复读和幻读!!!

小结

  1. 通常咱们认为MVCC有下面几个特色:

    • 每行数据都存在一个版本,每次数据更新时都更新该版本
    • 修改时Copy出当前版本, 而后随意修改,各个事务之间无干扰
    • 保存时比较版本号,若是成功(commit),则覆盖原记录, 失败则放弃copy(rollback)
    • 就是每行都有版本号,保存时根据版本号决定是否成功,听起来含有乐观锁的味道, 由于这看起来正是,在提交的时候才能知道到底可否提交成功
  2. 而InnoDB实现MVCC的方式是:

    • 事务以排他锁的形式修改原始数据
    • 把修改前的数据存放于undo log,经过回滚指针与主数据关联
    • 修改为功(commit)啥都不作,失败则恢复undo log中的数据(rollback)
  3. 两者最本质的区别是: 当修改数据时是否要排他锁定,若是锁定了还算不算是MVCC?
  • Innodb的实现真算不上MVCC, 由于并无实现核心的多版本共存, undo log 中的内容只是串行化的结果, 记录了多个事务的过程, 不属于多版本共存。但理想的MVCC是难以实现的, 当事务仅修改一行记录使用理想的MVCC模式是没有问题的, 能够经过比较版本号进行回滚, 但当事务影响到多行数据时, 理想的MVCC就无能为力了。
  • 好比, 若是事务A执行理想的MVCC, 修改Row1成功, 而修改Row2失败, 此时须要回滚Row1, 但由于Row1没有被锁定, 其数据可能又被事务B所修改, 若是此时回滚Row1的内容,则会破坏事务B的修改结果,致使事务B违反ACID。 这也正是所谓的 第一类更新丢失 的状况。
  • 也正是由于InnoDB使用的MVCC中结合了排他锁, 不是纯的MVCC, 因此第一类更新丢失是不会出现了, 通常说更新丢失都是指第二类丢失更新。
相关文章
相关标签/搜索