我对 MySQL 锁、事务、MVCC 的一些认识( 二 )

  • MVCC (后面讲)
  • 此外还利用了Next-Key锁 在一定程度上解决了幻读的问题 。关于这个我们后面再说 。
    Serializable在该隔离级别下事务都是串行顺序执行的 。如果禁用了自动提交 , 则 InnoDB 会将所有普通的 SELECT 语句隐式转换为 SELECT ... LOCK IN SHARE MODE 。即给读操作隐式加一把读共享锁 , 从而避免了脏读、不可重读复读和幻读问题 。
    MVCC

    Multiversion concurrency control (MCC or MVCC), is a concurrency control method commonly used by database management systems to provide concurrent access to the database and in programming languages to implement transactional memory

    翻译过来就是:多版本并发控制(MCC或MVCC)是一种并发控制方法 , 通常被数据库管理系统用来提供对数据库的并发访问 , 并以编程语言来实现事务存储 。
    简单来说就是数据库用来控制并发的一种方法 。每个数据库对于 MVCC 的实现可能不一样 。
    以我们常用的 MySQL 来说 , MySQL 的 InnoDB 引擎实现了 MVCC。
    MVCC 能解决什么问题从上面的定义我们能看出 , MVCC 主要解决事务并发时数据一致性的问题
    InnoDB 是如何实现的 MVCC下面这个图来自《高性能MySQL》(第3版)
    我对 MySQL 锁、事务、MVCC 的一些认识

    文章插图
     
    这本书写的很好 , 翻译的也不错 , 我对于 MySQL 最初的系统性认识也是因为读了这本书 , 然而在对于 MVCC 是如何实现的讲述上 , 个人认为是有些问题的 。
    来看下哪里有问题
    • 首先看下 MySQL 的官方文档 , 我对比了 5.1、5.6、5.7 三个版本的 文档[1]  , 对 MVCC 这部分的描述 , 几乎是相同的 。
    根据文档很明显是在每条数据增加三个隐藏列:
    • 6字节的 DB_TRX_ID 字段 , 表示最近一次插入或者更新该记录的事务ID 。
    • 7字节的 DB_ROLL_PTR 字段 , 指向该记录的 rollback segment 的 undo log 记录 。
    • 6字节的 DB_ROW_ID , 当有新数据插入的时候会自动递增 。当表上没有用户主键的时候 , InnoDB会自动产生聚集索引 , 包含DB_ROW_ID字段 。
    这里我补充一张包含 rollback segment 的 MySQL 内部结构图
    我对 MySQL 锁、事务、MVCC 的一些认识

    文章插图
     
    版本链
    之前我们讲过 undo_log 的概念 , 每条 undo日志都有一个 roll_pointer 属性 , 那么所有的版本都会被 roll_pointer 属性连接成一个链表 , 我们把这个链表称之为版本链 , 版本链的头节点就是当前记录最新的值 。
    ReadView
    通过隐藏列和版本链 , MySQL 可以将数据恢复到指定版本;但是具体要恢复到哪个版本 , 则需要根据 ReadView 来确定 。所谓 ReadView , 是指事务(记作事务A)在某一时刻给整个事务系统(trx_sys)打快照 , 之后再进行读操作时 , 会将读取到的数据中的事务 id 与 trx_sys 快照比较 , 从而判断数据对该 ReadView 是否可见 , 即对事务A是否可见 。(参考[2])
    至此我们发现 MVCC 就是基于隐藏字段、undo_log 链和 ReadView 来实现的 。
    Read committed 中的 MVCC前面我们讲过 Read committed 隔离级别中使用 MVCC 解决脏读问题 。这里我参考了两篇文章:
    • https://cloud.tencent.com/developer/article/1150633
    • https://cloud.tencent.com/developer/article/1150630
    InnoDB只会查找版本早于当前事务版本的数据行(也就是 , 行的版本号小于或是等于事务的系统版本号) , 这样可以确保数据读取的行 , 要么是在事务开始前已经存在的 , 要么是事务自身插入或修改过的 。因此不会产生脏读 。
    Read committed 隔离级别下出现不可重复读是由于 read view 的生成机制造成的 。在 Read committed 级别下 , 只要当前语句执行前已经提交的数据都是可见的 。在每次语句执行的过程中 , 都关闭 read view, 重新创建当前的一份 read view 。这样就可以根据当前的全局事务链表创建 read view 的事务区间 。简单说就是在 Read committed 隔离级别下 , MVCC 在每次 select 时生成一个快照版本 , 所以每次 select 都会读到不同的版本数据 , 所以会产生不可重复读 。
    Repeatable read 中的 MVCCRepeatable read 隔离级别解决了不可重复读的问题 , 一个事务中多次读取不会出现不同的结果 , 保证了可重复读 。前文中我们说 Repeatable read 有两种实现方式 , 一种是悲观锁的方式 , 相对的 MVCC 就是乐观锁的方式 。


    推荐阅读