• MySQL 事务与InnoDB的MVCC实现机制


    MVCC全称Multi-Version Concurrency Control,即多版本并发控制。MVCC是为MySQL并发场景下无锁生成读视图进行读操作来进行多版本控制。
    MySQL中InnoDB中实现了事务(MVCC+锁),其中通过MVCC解决隔离性问题。具体而言,MVCC就是为了实现读-写冲突不加锁。

    事务的四大特性(ACID)

    ACID模型是一组数据库设计原则,强调对业务数据和关键任务应用程序的重要可靠性。

    如果您有额外的软件保护、超可靠的硬件或可以容忍少量数据丢失或不一致的应用程序,您可以调整MySQL设置牺牲部分ACID可靠性以获得更高的性能或吞吐量。

    • 原子性(Atomicity):一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚
    • 一致性(Consistency):数据库总是从一个一致性的状态转换到另一个一致性的状态
    • 隔离性(Isolation):通常来说,一个事务所做的修改在最终提交以前,对其他事务是不可见的
    • 持久性(Durability):一旦事务提交,则其所做的修改就会永久保存到数据库中

    事务隔离

    事务隔离是数据库处理的基础之一。当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题,为了解决这些问题,就有了“隔离级别”的概念。

    事务隔离级别

    隔离级别是在多个事务同时进行更改和执行查询时,有效平衡性能与可靠性、一致性和结果的可再现性的设置。

    在谈隔离级别之前,首先要知道,隔离得越严实,效率就会越低。因此很多时候,我们都要在二者之间寻找一个平衡点。

    SQL 标准的事务隔离级别包括:

    • 读未提交(Read Uncommitted):一个事务还没提交时,它做的变更就能被别的事务看到。
    • 读提交(Read Committed):一个事务提交之后,它做的变更才会被其他事务看到。
    • 可重复读(Repeatable Read):一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
    • 串行化(Serializable):对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
    1. 只有在隔离级别为读未提交(RU)时,会出现脏读
    2. RR隔离级别下需要利用间隙锁来解决幻读问题

    快照读与当前读

    MVCC(多版本并发控制)就是为了实现读-写冲突不加锁,而这个读指的就是快照读当前读实际上是一种加锁的操作,是悲观锁的实现

    • 当前读
      当前读读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。如共享锁select for share, 排他锁select for update,update,insert,delete等操作

    • 快照读
      快照读是不加锁的非阻塞读,如普通的select操作。之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销

      • 既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。

    快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读

    InnoDB快照读在RR与RC级别的区别

    • 在RC隔离级别下,是每个快照读都会生成并获取最新的Read View
    • 在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View

    RC

    session Asession B
    begin;begin;
    select nick from t where id=1; (快照读 nick=赵一)
    update t set nick=‘钱二’ where id=1;
    commit;
    select nick from t where id=1; (快照读 nick=钱二)
    select nick from t where id=1 for share; (当前读 nick=钱二)

    RR

    session Asession B
    begin;begin;
    select nick from t where id=1; (快照读 nick=赵一)
    update t set nick=‘钱二’ where id=1;
    commit;
    select nick from t where id=1; (快照读 nick=赵一)
    select nick from t where id=1 for share; (当前读 nick=钱二)

    在RR级别,事务中快照读的结果是非常依赖该事务首次出现快照读的地方,即某个事务中首次出现快照读的地方非常关键,它有决定该事务后续快照读结果的能力

    事务启动命令

    begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作 InnoDB 表的语句,事务才真正启动。如果你想要马上启动一个事务,可以使用 start transaction with consistent snapshot 这个命令。

    第一种启动方式,一致性视图是在第执行第一个快照读语句时创建的; 第二种启动方式,一致性视图是在执行 start transaction with consistent snapshot 时创建的。

    MVCC实现机制

    MVCC为多版本并发控制,目的是为了解决读写冲突。它的实现原理主要是依赖记录中的3个隐式字段undo logRead View来实现的。

    隐式字段

    MySQL行记录除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段

    • DB_ROW_ID 6byte, 隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引
    • DB_TRX_ID 6byte, 最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务ID
    • DB_ROLL_PTR 7byte, 回滚指针,指向这条记录的上一个版本(存储于rollback segment里)
    • DELETED_BIT 1byte, 记录被更新或删除并不代表真的删除,而是删除flag变了

    在这里插入图片描述

    如上图,DB_ROW_ID是数据库默认为该行记录生成的唯一隐式主键;DB_TRX_ID是当前操作该记录的事务ID; 而DB_ROLL_PTR是一个回滚指针,用于配合undo日志,指向上一个旧版本

    undo log

    此段原文修改自MySQL InnoDB的MVCC实现机制
    著作权归https://pdai.tech所有。

    undo log是一种用于撤销回退的日志,在事务没提交之前,MySQL会先记录更新前的数据到 undo log日志文件里面,当事务回滚时或者数据库崩溃时,可以利用 undo log来进行回退。

    undo log主要分为3种:

    • Insert undo log:插入一条记录时,至少要把这条记录的主键值记下来,之后回滚的时候只需要把这个主键值对应的记录删掉
    • Update undo log:修改一条记录时,至少要把修改这条记录前的旧值都记录下来,这样之后回滚时再把这条记录更新为旧值
    • Delete undo log:删除一条记录时,至少要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中
      • 删除操作都只是设置一下记录的DELETED_BIT,并不真正将过时的记录删除。
      • 为了节省磁盘空间,InnoDB有专门的purge线程来清理DELETED_BIT为true的记录。为了不影响MVCC的正常工作,purge线程自己也维护了一个read view(这个read view相当于系统中最老活跃事务的read view);如果某个记录的DELETED_BIT为true,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录一定是可以被安全清除的。

    undo log的存储由InnoDB存储引擎实现,数据保存在InnoDB的数据文件中。在InnoDB存储引擎中,undo log是采用分段(segment)的方式进行存储的。rollback segment称为回滚段,每个回滚段中有1024个undo log segment。

    在MySQL5.5之前,只支持1个rollback segment,也就是只能记录1024个undo操作。在MySQL5.5之后,可以支持128个rollback segment

    undo log例子

    undo log实际上就是存在rollback segment中旧记录链,对MVCC有帮助的实质是update undo log,它的执行流程如下:

    1. 比如一个有个事务插入persion表插入了一条新记录,记录如下,name为Jerry, age为24岁,隐式主键是1,事务ID和回滚指针,我们假设为NULL
      在这里插入图片描述

    2. 现在来了一个事务1对该记录的name做出了修改,改为Tom

      1. 在事务1修改该行(记录)数据时,数据库会先对该行加排他锁
      2. 然后把该行数据拷贝到undo log中,作为旧记录,既在undo log中有当前行的拷贝副本
      3. 拷贝完毕后,修改该行name为Tom,并且修改隐藏字段的事务ID为当前事务1的ID, 我们默认从1开始,之后递增,回滚指针指向拷贝到undo log的副本记录,既表示我的上一个版本就是它
      4. 事务提交后,释放锁

    在这里插入图片描述

    1. 又来了个事务2修改person表的同一个记录,将age修改为30岁
      1. 在事务2修改该行数据时,数据库也先为该行加锁
      2. 然后把该行数据拷贝到undo log中,作为旧记录,发现该行记录已经有undo log了,那么最新的旧数据作为链表的表头,插在该行记录的undo log最前面
      3. 修改该行age为30岁,并且修改隐藏字段的事务ID为当前事务2的ID, 那就是2,回滚指针指向刚刚拷贝到undo log的副本记录
      4. 事务提交,释放锁

    在这里插入图片描述

    从上面,我们就可以看出,不同事务或者相同事务的对同一记录的修改,会导致该记录的undo log成为一条记录版本线性表,既链表,undo log的链首就是最新的旧记录,链尾就是最早的旧记录

    Read View

    此段原文修改自MySQL 实战 45 讲

    InnoDB 在实现 MVCC 时用到的一致性读视图,即 consistent read view,用于支持 RC(Read Committed,读提交)和 RR(Repeatable Read,可重复读)隔离级别的实现。

    Read View没有物理结构,作用是事务执行期间用来定义“我能看到什么数据”

    Read View概念

    按照可重复读的定义,一个事务启动的时候,能够看到所有已经提交的事务结果。但是之后,这个事务执行期间,其他事务的更新对它不可见。

    在实现上, InnoDB 为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务 ID。“活跃”指的就是,启动了但还没提交。

    InnoDB 里面每个事务有一个唯一的事务ID,叫作 transaction id。它是在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的。每次事务更新数据的时候,都会生成一个新的数据版本,并且把 transaction id 赋值给这个数据版本的事务ID(DB_TRX_ID)

    • 未提交事务ID数组 用来维护Read View生成时刻系统正活跃的事务ID
    • 低水位 未提交事务ID数组里面事务ID的最小值
    • 高水位 当前系统里面已经创建过的事务ID的最大值加1

    这个视图数组和高水位,就组成了当前事务的一致性视图(read-view)

    数据版本可见性规则

    Read View可见性规则

    数据版本的可见性规则,就是基于数据的DB_TRX_ID和这个一致性视图的对比结果得到的

    对于当前事务的启动瞬间来说,一个数据版本的DB_TRX_ID,根据上图对比可知,有以下几种可能:

    • 如果落在已提交事务部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的;
    • 如果落在未开始事务部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的;
    • 如果落在未提交事务集合部分,那就包括两种情况
      • a. 若DB_TRX_ID在数组中,表示这个版本是由还没提交的事务生成的,不可见;
      • b. 若DB_TRX_ID不在数组中,表示这个版本是已经提交了的事务生成的,可见。

    InnoDB 利用了“所有数据都有多个版本”的这个特性,实现了“秒级创建快照”的能力。

    Read View示例

    接下来,我们继续看一下图中的三个事务,分析下事务 A 的语句返回的结果,为什么是 k=1。

    事务A事务B事务C
    start transaction with consistent snapshot;
    start transaction with consistent snapshot;
    update t set k=k+1 where id=1;
    update t set k=k+1 where id=1;
    select k from t where id=1;
    select k from t where id=1;
    commit;
    commit;

    这里,我们不妨做如下假设:

    1. 事务 A 开始前,系统里面只有一个活跃事务 ID 是 99;
    2. 事务 A、B、C 的版本号分别是 100、101、102,且当前系统里只有这四个事务;
    3. 三个事务开始前,(1,1)这一行数据的 row trx_id 是 90。

    这样,事务 A 的视图数组就是 [99,100], 事务 B 的视图数组是 [99,100,101], 事务 C 的视图数组是 [99,100,101,102]。

    为了简化分析,我先把其他干扰语句去掉,只画出跟事务 A 查询逻辑有关的操作:
    事务 A 查询数据逻辑图

    从图中可以看到,第一个有效更新是事务 C,把数据从 (1,1) 改成了 (1,2)。这时候,这个数据的最新版本的DB_TRX_ID是 102,而 90 这个版本已经成为了历史版本。

    第二个有效更新是事务 B,把数据从 (1,2) 改成了 (1,3)。这时候,这个数据的最新版本(即DB_TRX_ID)是 101,而 102 又成为了历史版本。

    你可能注意到了,在事务 A 查询的时候,其实事务 B 还没有提交,但是它生成的 (1,3) 这个版本已经变成当前版本了。但这个版本对事务 A 必须是不可见的,否则就变成脏读了。

    好,现在事务 A 要来读数据了,它的视图数组是 [99,100]。当然了,读数据都是从当前版本读起的。所以,事务 A 查询语句的读数据流程是这样的:

    • 找到 (1,3) 的时候,判断出DB_TRX_ID=101,比高水位大,处于红色区域,不可见;
    • 接着,找到上一个历史版本,一看DB_TRX_ID=102,比高水位大,处于红色区域,不可见;
    • 再往前找,终于找到了(1,1),它的DB_TRX_ID=90,比低水位小,处于绿色区域,可见。

    这样执行下来,虽然期间这一行数据被修改过,但是事务 A 不论在什么时候查询,看到这行数据的结果都是一致的,所以我们称之为一致性读。

    这个判断规则是从代码逻辑直接转译过来的,但是正如你所见,用于人肉分析可见性很麻烦。

    所以,我来给你翻译一下。一个数据版本,对于一个事务视图来说,除了自己的更新总是可见以外,有三种情况:

    1. 版本未提交,不可见;
    2. 版本已提交,但是是在视图创建后提交的,不可见;
    3. 版本已提交,而且是在视图创建前提交的,可见。

    现在,我们用这个规则来判断图 4 中的查询结果,事务 A 的查询语句的视图数组是在事务 A 启动的时候生成的,这时候:

    • (1,3) 还没提交,属于情况 1,不可见;
    • (1,2) 虽然提交了,但是是在视图数组创建之后提交的,属于情况 2,不可见;
    • (1,1) 是在视图数组创建之前提交的,可见。

    不同隔离级别下Read View表现

    • RU:直接返回记录上的最新值,没有视图概念
    • RC:视图是在每次SELECT时生成的。(违背了事务ACID里的隔离性)
    • RR:视图是在第一次SELECT时创建的,事务读取期间的SELECT都用这个视图
    • Serializable:直接用加锁的方式来避免并行访问,没有视图概念

    参考资料:

    1. InnoDB 事务模型
    2. 《MySQL技术内幕 InnoDB存储引擎》
    3. MySQL InnoDB的MVCC实现机制
    4. MySQL 实战 45 讲
    5. MySQL事务与MVCC如何实现的隔离级别
  • 相关阅读:
    Unity API学习之消息机制理论与应用
    6.2、Flink数据写入到Kafka
    Matlab论文插图绘制模板第56期—曲面图(Surf)
    探讨C#、C++和Java这三门语言在嵌入式的地位
    联通面试题
    三年经验只会点点点(功能测试),辞职后你可能连工作都找不到了。
    基于 Spring boot + MyBatis 的在线音乐播放系统
    DJango 学习(2)—— django引入:借助于wsgiref模块(web服务网关接口)搭建简易 web 框架
    每天一个面试题之类加载机制、spirngboot的启动机制
    python基础知识
  • 原文地址:https://blog.csdn.net/why_still_confused/article/details/126311886