MVCC详解


参考:

https://blog.csdn.net/SnailMann/article/details/94724197

https://blog.csdn.net/DILIGENT203/article/details/100751755

https://blog.csdn.net/whoamiyang/article/details/51901888

https://techlog.cn/article/list/10183403

正确的理解MySQL的MVCC及实现原理

!首先声明,MySQL的测试环境是5.7

  • 前提概要
    • 什么是MVCC
    • 什么是当前读和快照读?
    • 当前读,快照读和MVCC的关系
  • MVCC实现原理
    • 隐式字段
    • undo日志
    • Read View(读视图)
    • 整体流程
  • MVCC相关问题
    • RR是如何在RC级的基础上解决不可重复读的?
    • RC,RR级别下的InnoDB快照读有什么不同?

mvcc - @百度百科

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

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


@呵呵一笑百媚生一张源码图,如上,它是一段MySQL判断可见性的一段源码,即changes_visible方法(不完全哈,但能看出大致逻辑),该方法展示了我们拿DB_TRX_ID去跟Read View某些属性进行怎么样的比较

在展示之前,我先简化一下Read View,我们可以把Read View简单的理解成有三个全局属性

  • trx_list(名字我随便取的)
    一个数值列表,用来维护Read View生成时刻系统正活跃的事务ID
  • up_limit_id
    记录trx_list列表中事务ID最小的ID
  • low_limit_id
    ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1
  • 首先比较DB_TRX_ID < up_limit_id, 如果小于,则当前事务能看到DB_TRX_ID 所在的记录,如果大于等于进入下一个判断
  • 接下来判断 DB_TRX_ID 大于等于 low_limit_id , 如果大于等于则代表DB_TRX_ID 所在的记录在Read View生成后才出现的,那对当前事务肯定不可见,如果小于则进入下一个判断
  • 判断DB_TRX_ID 是否在活跃事务之中,trx_list.contains(DB_TRX_ID),如果在,则代表我Read View生成时刻,你这个事务还在活跃,还没有Commit,你修改的数据,我当前事务也是看不见的;如果不在,则说明,你这个事务在Read View生成之前就已经Commit了,你修改的结果,我当前事务是能看见的

>>>资料传送门 | 呵呵一笑百媚生的回答) ;所以在这里例子中up_limit_id就是1,low_limit_id就是4 + 1 = 5,trx_list集合的值是1,3,Read View如下图
  • 我们的例子中,只有事务4修改过该行记录,并在事务2执行快照读前,就提交了事务,所以当前该行当前数据的undo log如下图所示;我们的事务2在快照读该行记录的时候,就会拿该行记录的DB_TRX_ID去跟up_limit_id,low_limit_id活跃事务ID列表(trx_list)进行比较,判断当前事务2能看到该记录的版本是哪个。
  • 所以先拿该记录DB_TRX_ID字段记录的事务ID 4去跟Read View的的up_limit_id比较,看4是否小于up_limit_id(1),所以不符合条件,继续判断 4 是否大于等于 low_limit_id(5),也不符合条件,最后判断4是否处于trx_list中的活跃事务, 最后发现事务ID为4的事务不在当前活跃事务列表中, 符合可见性条件,所以事务4修改后提交的最新结果对事务2快照读时是可见的,所以事务2能读到的最新数据记录是事务4所提交的版本,而事务4提交的版本也是全局角度上最新的版本

在这里插入图片描述

  • 也正是Read View生成时机的不同,从而造成RC,RR级别下快照读的结果的不同

【MySQL笔记】正确的理解MySQL的乐观锁与悲观锁,MVCC
  • 【MySQL笔记】正确的理解MySQL的MVCC及实现原理
  • 【MySQL笔记】正确的理解MySQL的事务和隔离级别

  • InnoDB多版本(MVCC)实现简要分析 - @作者:何登成
  • MySQL InnoDB MVCC深度分析 - @作者:stevenczp
  • InnoDB存储引擎MVCC的工作原理 - @作者:秋风醉了
  • MySQL 在 RC 隔离级别下是如何实现读不阻塞的? - @作者:知乎
  • MVCC read view的问题 - @作者:PHP中文网
  • MySQL数据库事务各隔离级别加锁情况–read committed && MVCC - @作者:mark_fork
  • 乐观锁与CAS,MVCC - @作者:shuff1e
  • 悲观锁,乐观锁以及MVCC - @作者:wezheng
  • 【数据库】悲观锁与乐观锁与MySQL的MVCC实现简述 - @作者:Nick Huang
  •                    

    一文讲透 MVCC 实现原理

    1. 引言

    上一篇文章中,我们介绍了 mysql 的 crash safe 机制,也是 ACID 中原子性的实现 – redolog 的原理和配置方法
    mysql 异常情况下的事务安全 – 详解 mysql redolog

    本文,我们来介绍 mysql 在可重复读隔离级别下事务的实现方式 – MVCC,以及他的实现原理 – undologmysql 锁机制与四种隔离级别

    MVCC 全称是 multiversion concurrency control,即多版本并发控制,是 innodb 实现事务并发与回滚的重要功能。
    具体的实现是,在数据库的每一行中,添加额外的三个字段:

    1. DB_TRX_ID – 记录插入或更新该行的最后一个事务的事务 ID
    2. DB_ROLL_PTR – 指向改行对应的 undolog 的指针
    3. DB_ROW_ID – 单调递增的行 ID,他就是 AUTO_INCREMENT 的主键 ID

    # 此处有图片 3

    怎么避免从删库到跑路 -- 详解 mysql binlog 的配置与使用

    无论我们使用的是什么存储引擎,只要通过配置开启,mysql 都会记录 binlog

    在工程存储项目中,有一个重要的概念,那就是 crash safe,即当服务器突然断电或宕机,需要保证已提交的数据或修改不会丢失,未提交的数据能够自动回滚,这就是 mysql ACID 特性中的一个十分重要的特性 -- Atomicity 原子性

    根据我们上一篇文章中的讲解,依靠 binlog 是无法保证 crash safe 的,因为 binlog 是事务提交时写入的,如果在 binlog 缓存中的数据持久化到硬盘之前宕机或断电

    在服务器恢复工作后,由于 binlog 缺失一部分已提交的操作数据,而主数据库中实际上这部分操作已经存在,从数据库因此无法同步这部分操作,从而造成主从数据库数据不一致,这是很严重的

    但实际上,innodb 存储引擎是拥有 crash safe 能力的,那么他是用什么机制来实现呢?本文我们就来详细说明

    mysql 的执行过程

    无论使用任何存储引擎,只要开启相应配置,mysql 都会记录 binlog

    但 MyISAM 引擎并没有提供 crash safe 能力,而 InnoDB 则提供了灾后恢复能力,这是为什么呢?

    这和 mysql 整体的分层有关,我们需要首先了解一下一条 sql 语句是如何执行的

    mysql 主要分为两层,与客户端直接交互的是 server 层,包括连接的简历和管理、词法分析、语法分析、执行计划与具体 sql 的选择都是在 server 层中进行的,binlog 就是在 server 层中由 mysql server 实现的

    而 innodb 作为具体的一个存储引擎,他通过 redolog 实现了 crash safe 的支持

    redolog 的写入

    mysql 有一个基本的技术理念,那就是 WAL,即  Write-Ahead Logging,先写日志,再写磁盘,从而保证每一次操作都有据可查,这里所说的“先写日志”中的日志就包括 innodb 的 redolog

    redolog 与持续向后添加的 binlog 不同,他只占用预先分配的一块固定大小的磁盘空间,在这片空间中,redolog 采用循环写入的方式写入新的数据

    同时,binlog 是以每条操作语句为单位进行记录的,而 redolog 则是以数据页来进行记录的,他记录了每个页上的修改,所以一个事务中可能分多次多条写入 redolog

    crash safe 与两阶段提交

    每条 redolog 都有两个状态 -- prepare 与 commit 状态

    例如对于一张 mysql 表(CREATE TABLE `A` (`ID` int(10) unsigned NOT NULL AUTO_INCREMENT, `C` int(10) NOT NULL DEFAULT 0, PRIMARY KEY (`ID`)) ENGINE=InnoDB),我们执行一条 SQL 语句:

    UPDATE A set C=C+1 WHERE ID=2

    实际上,mysql 数据库会进行以下操作(下图中深色的是 mysql server 层所做的操作,浅色部分则是 innodb 存储引擎进行的操作)::

    可以看到,在写入 binlog 及事务提交前,innodb 先记录了 redolog,并标记为 prepare 状态,在事务提交后,innodb 会将 redolog 更新为 commit 状态,这样在异常发生时,就可以按照下面两条策略来处理:

      1. 当异常情况发生时,如果第一次写入 redolog 成功,写入 binlog 失败,MySQL 会当做事务失败直接回滚,保证了后续 redolog 和 binlog 的准确性
      1. 如果第一次写入 redolog 成功,binlog 也写入成功,当第二次写入 redolog 时候失败了,那数据恢复的过程中,MySQL 判断 redolog 状态为 prepare,且存在对应的 binlog 记录,则会重放事务提交,数据库中会进行相应的修改操作

    整个过程是一个典型的两阶段提交过程,由 binlog 充当了协调者的角色,针对每一次日志写入,innodb 都会随之记录一个 8 字节序列号 -- LSN(日志逻辑序列号 log sequence number),他会随着日志写入不断单调递增

    binlog、DB 中的数据、redolog 三者就是通过 LSN 关联到一起的,因为数据页上记录了 LSN、日志开始与结束均记录了 LSN、刷盘节点 checkpoint 也记录了 LSN,因此 LSN 成为了整套系统中的全局版本信息

    当异常发生并重新启动后,innodb 会根据出在 prepare 状态的 redo log 记录去查找相同 LSN 的 binlog、数据记录,从而实现异常后的恢复

    redo log 的组织

    redo log 是以“块”为单位进行存储的,称之为“redo log block”,每个块的大小是 512 字节

    以块为单位存储的原因是他和磁盘扇区的大小是相同的,从而保证在异常情况发生时不会出现部分写入成功产生的脏数据

    相关配置

    innodb_log_file_size

    redo log 磁盘空间大小,默认为 5M

    innodb_log_buffer_size

    redo log 缓存大小,默认为 8M

    innodb_flush_log_at_trx_commit

    此前我们曾经介绍过,操作系统为了减少了磁盘的读写次数,提升系统的 IO 性能,会在内存空间中分配一个缓冲区,这就是页面高速缓冲,虽然高速缓冲让 IO 性能得以大幅提升,但在宕机等异常发生时,这部分在高速缓冲区中的数据就会丢失,因此 unix 提供了系统调用 fsync来让我们手动执行高速缓冲到磁盘的刷新工作

    对于 redolog 来说,由于他的存在就是为了避免异常情况造成的已提交事务的丢失,所以高速缓冲引起的未刷盘数据丢失是不能容忍的,innodb_flush_log_at_trx_commit 配置项就是指定具体的刷盘策略的

    他有以下值可以选择:

      1. 0 -- 以固定间隔将缓存中的数据写入系统高速缓存并调用一次 fsync 强制刷新高速缓冲,系统崩溃可能丢失最大1秒的数据
      1. 1 -- 默认值,每次事务提交时调用 fsync,这种方式即使系统崩溃也不会丢失任何数据,但是因为每次提交都写入磁盘,IO的性能较差
      1. 2 -- 每次事务提交都将数据写入系统高速缓存,但仅在固定间隔调用一次 fsync 强制刷新高速缓冲,安全性高于配置为 0

    通常,为了绝对的安全性,我们会配置为 1,但在追求最高的写入性能时,我们通常配置为 2,因为设置为 2 与设置为 0 在性能上差异不大,但配置为 2 却在安全性上高于配置为 0

    同时为了保证 binlog 的安全性,我们同时要配置 sync_binlog 为 1,保证每次 binlog 都直接写入磁盘,而不进行缓存

    innodb_flush_log_at_timeout

    上面提到了刷新告诉缓存的固定间隔,这个“固定间隔”就是通过 innodb_flush_log_at_timeout 配置项指定的,默认是 1 秒

    但实际上,如果 redo log 的缓存占用超过一半,也会立即触发缓冲的刷新