MySql MVCC 详解
作者:mmseoamin日期:2023-12-14

注意以下操作都是以InnoDB引擎为操作基准。

一,前置知识准备

1,MVCC简介

MVCC 是多版本并发控制(Multiversion Concurrency Control)的缩写。它是一种数据事务管理技术,用于解决并发访问数据库的问题。MVCC 通过创建多个版本的同一数据,每个版本与一个事务关联,来实现并发控制。

数据库在执行更新操作时,会保留之前版本的数据,以便其他正在执行事务的用户可以访问这些数据。每个事务都能看到一个稳定的数据快照,并且仅接触到他们自己的版本,这意味着每个事务可以独立地读取和写入数据,而不会干扰其它事务。

MVCC 在数据库的可伸缩性和性能方面具有重要作用,尤其是对于高并发的应用程序,如电子商务网站和社交媒体应用。

2,MySQL的逻辑架构

  • 第一层:处理客户端连接、授权认证,安全校验等。

  • 第二层:服务器server层,负责对SQL解释、分析、优化、执行操作引擎等。

  • 第三层:存储引擎,负责MySQL中数据的存储和提取。

    MySql MVCC 详解,第1张

    3,事务的四大特性

    ACID是指数据库事务所必须具备的四个特性,包括:

    1. 原子性(Atomicity):事务是一个不可分割的原子操作,要么全部执行成功,要么全部失败回滚,不允许出现部分执行成功或失败的情况。【undolog】

    2. 一致性(Consistency):事务执行前和执行后,数据库的完整性约束没有被破坏,也就是说,在事务完成后,数据库从一个一致性状态转变为另一个一致性状态。

    3. 隔离性(Isolation):事务之间是相互隔离的,一个事务的执行不能被其他事务干扰。多个事务并发执行时,它们之间的执行是独立的,每个事务感觉就像是在独占数据库。【锁,mvcc】

    4. 持久性(Durability):事务完成后,对于数据的修改是永久性的,即使系统故障也不会导致数据的丢失。因此,数据库中所有的数据修改都需要记录到日志中,以便在系统故障恢复时进行重做。【redolog】

    4,事务的四大隔离级别

    事务的隔离级别是指多个事务同时操作一个数据库时,数据库系统对这些事务分配的隔离程度。在不同的隔离级别下,事务之间的隔离程度也不同。常见的隔离级别包括:

    1. 读未提交(Read Uncommitted):允许一个事务读取另一个事务未提交的数据。这种隔离级别对数据的完整性和一致性影响较大。【存在脏读】

    2. 读已提交(Read Committed):一个事务只能读取已经提交的数据,其他未提交的事务所做的修改对它不可见。这种隔离级别对数据完整性和一致性的保护较好。【解决脏读,存在不可重复读】【mysql InnoDB 通过 mvcc 解决 】

    3. 可重复读(Repeatable Read):一个事务在执行期间看到的数据是固定的,不受其他并发事务的影响。即使其他事务已经提交了对数据的修改,当前事务也只能看到自己在读取数据时的版本。这种隔离级别对数据的完整性和一致性有一定保护。【解决不可重复读,存在幻读】【mysql InnoDB 通过 mvcc 解决 】

    4. 串行化(Serializable):最高的隔离级别,确保事务之间的操作是完全独立的,一个事务的操作必须等另一个事务结束后才能开始。这种隔离级别对数据完整性和一致性的保护最好,但并发性能较低。【解决脏读,解决不可重复读,解决幻读】【mysql InnoDB 通过 加锁解决 】

    在实际应用中,应根据业务需求和系统性能等因素选择合适的隔离级别。

    5,mysql 常用的日志

    server 层

    1. 错误日志(Error Log): 记录 MySQL 服务器启动、运行过程中出现的重要错误和警告信息。
    2. 慢查询日志(Slow Query Log): 记录执行时间超过预设时间的查询语句,通常用于优化查询性能。
    3. 二进制日志(Binary Log): 记录数据库更新操作,用于主从复制和数据恢复。
    4. 中继日志(Relay Log): 只在主从复制时使用,记录从服务器复制主服务器二进制日志的过程。

    引擎层

    1. 重做日志(Redo log): 是一种用于恢复数据库中未提交和已提交事务的机制。其包含了所有已经被写入到磁盘上的事务,通常被存储在磁盘上的一组文件中。当数据库系统宕机或者发生崩溃时,可以通过redo log来恢复数据库并且保证ACID属性。
    2. 回滚日志(Undo log): 则用于撤销已经提交或者未提交的事务。它记录了事务执行前的数据,以便当出现错误时可以将数据回滚到事务执行前的状态。在数据库中,undo log通常被用于避免脏读、不可重复读和幻读等问题。

    二,mvcc 原理

    1,当前读和快照读

    在学习 MVCC 多版本并发控制之前,我们必须先了解一下,什么是 MySQL InnoDB 下的当前读和快照读。

    • 当前读,像 select lock in share mode (共享锁), select for update; update;insert; delete (排他锁)这些操作都是一种当前读,为什么叫当前读 ? 就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。
    • 快照读,像不加锁的 select操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读,之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即MVCC.可以认为 MVCC 是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。

      MySql MVCC 详解,第2张在InnoDB中的每一条记录实际都会存在三个隐藏列

      • DB_TRX_ID:事务 ID,是根据事务产生时间顺序自动递增的,是独一无二的。如果某个事务执行过程中对该记录执行了增、删、改操作,那么InnoDB存储引擎就会记录下该条事务的 id。

      • DB_ROLL_PTR:回滚指针,本质上就是一个指向记录对应的undo log的一个指针,InnoDB 通过这个指针找到之前版本的数据

      • DB_ROW_ID:主键,如果有自定义主键,那么该值就是主键;如果没有主键,那么就会使用定义的第一个唯一索引;如果没有唯一索引,那么就会默认生成一个隐藏列作为主键。

        MySql MVCC 详解,第3张

        2,undolog

        这些数据快照都是存储在undolog 中的,这些数据分为两类

        • Insert undo log :insert生成的日志,仅在事务回滚中需要,并且可以在事务提交后立即丢弃。

        • Update undo log:update/delete生成的日志,除了用于事务回滚,还用于一致性读取,只有不存在innodb为其分配快照的事务之后才能丢弃它们,在一致读取中可能需要update undo log中的信息来构建数据库行的早期版本。

          删除操作实际上不会直接删除,而只是标记为删除,最终的删除操作是purge线程完成的

          purge线程作用,一是清理undo log,二是清除page里面带有Delete_Bit标识的数据行。在InnoDB中,事务中的Delete操作实际上并不是真正的删除掉数据行,而是一种Delete Mark操作,在记录上标识删除,真正的删除工作需要后台purge线程去完成。

          3,Read View(读视图)

          事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照。

          记录并维护系统当前活跃事务的ID(trx_id)(没有commit,当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以越新的事务,ID值越大),是系统中当前不应该被本事务看到的其他事务id列表。

          Read View主要是用来做可见性判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。

          Read View几个属性 

          MySql MVCC 详解,第4张

          MVCC 只在 Read Commited(读已提交) 和 Repeatable Read(可重读读) 两种隔离级别下工作。

          • 在RC隔离级别下,是每个快照读都会生成并获取最新的Read View,这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因。

          • 在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View,从而做到可重复读。

            4,可见性算法

            1、首先比较DB_TRX_ID < up_limit_id,如果小于,则当前事务能看到DB_TRX_ID所在的记录,如果大于等于进入下一个判断。

            2、接下来判断DB_TRX_ID >= low_limit_id,如果大于等于则代表DB_TRX_ID所在的记录在Read View生成后才出现的,那么对于当前事务肯定不可见,如果小于,则进入下一步判断。

            3、判断DB_TRX_ID是否在活跃事务中,如果在,则代表在Read View生成时刻,这个事务还是活跃状态,还没有commit,修改的数据,当前事务也是看不到,如果不在,则说明这个事务在Read View生成之前就已经开始commit,那么修改的结果是能够看见的。

            4,mvcc能否解决幻读

            在一次事务里面,多次查询之后,结果集的个数不一致的情况叫做幻读。而多出来或者少的哪一行被叫做幻行。

            • 在快照读读情况下,mysql通过mvcc来避免幻读。

            • 在当前读读情况下,mysql通过next-key来避免幻读。

              不能把快照读和当前读得到的结果不一样这种情况认为是幻读,这是两种不同的使用。所以mysql的RR级别是解决了幻读的。

              三,mvcc 场景验证

              所谓光说不练假把式,光练不说傻把式,又练又说真把式,上面说的只是结论,下面进行实际的操作演示。

              mysql 版本 5.7

              创建测试表

              CREATE TABLE `user` (
                `id` bigint(20) NOT NULL AUTO_INCREMENT,
                `name` varchar(20) DEFAULT NULL,
                `age` tinyint(3) DEFAULT NULL,
                `gender` tinyint(1) DEFAULT NULL,
                PRIMARY KEY (`id`),
                KEY `idx_abc` (`name`,`age`,`gender`) USING BTREE
              ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8

              (开始之前将事务自动提交关闭) 

              1,场景一

              当前的事务隔离级别为,可重复读(Repeatable Read)

              MySql MVCC 详解,第5张

              步骤一,t1时刻同时开启事务

              步骤二,t2时刻事务2更新并提交

              步骤三,t3时刻事务1查询

              问题,事务1在t3时刻是否能查询t2时刻事务2提交的数据。

              预期结果能

              因为执行的是select所以为快照读,但是由于Read View是在快照读的时候才产生,并且可重复读(Repeatable Read)隔离级别下,只产生一个Read View。事务2更新并提交完成时候,由于此时事务1还没进行快照读。所以此时事务1查询的时候是可以查询到事务2更新并提交后的数据的。

              sql 执行验证 

              MySql MVCC 详解,第6张

              MySql MVCC 详解,第7张

              查询结果符合预期

              通过算法核对

              trx_list:1,3 【当前活跃事务为 1,3,因为2已经提交了】

              low_limit_id:4【当前系统最大事务版本号+1,因为已经开启了3个事务,所以下一个应该是4】

              up_limit_id:1 【创建当前read view 时“系统正处于活跃事务最小版本号,活跃的只有1,3,最小的是1】

              MySql MVCC 详解,第8张

              算法验证:验证当前事务1是否能看到事务2提交的数据。

              (1)DB_TRX_ID < up_limit_id 【2<1】不成立继续判断

              (2)DB_TRX_ID >= low_limit_id 【2>=4】不成立继续判断

              (3)判断DB_TRX_ID是否在活跃事务中,不在,则说明这个事务在Read View生成之前就已经开始commit,那么修改的结果是能够看见的。

              2,场景二

              当前的事务隔离级别为,可重复读(Repeatable Read)

              MySql MVCC 详解,第9张

              步骤一,t1时刻同时开启事务

              步骤二,t2时刻事务1进行快照读

              步骤三,t3时刻事务2修改并提交

              步骤四,t4时刻事务1进行快照读

              问题,事务1在t4时刻是否能查询t3时刻事务2提交的数据。

              预期结果不能

              事务1在t2时刻执行的是select所以为快照读,此时Read View产生,并且可重复读(Repeatable Read)隔离级别下,只产生一个Read View。事务2在t3更新并提交完成时候,由于此时事务1已经产生了Read View,再次进行快照读。所以此时事务1查询的时候是可以查询不到事务2更新并提交后的数据的,因为读取的是快照中的数据。

              sql 执行验证  MySql MVCC 详解,第10张

               查询结果符合预期

              通过算法核对

              第一次查询

              trx_ids:1,2,3 【当前活跃事务为 1,2,3,此时事务2还没开始修改提交】

              low_limit_id:4【当前系统最大事务版本号+1,因为已经开启了3个事务,所以下一个应该是4】

              up_limit_id:1 【创建当前read view 时“系统正处于活跃事务最小版本号,活跃的只有1,3,最小的是1】

              第二次查询

              可重复读(Repeatable Read)事务隔离级别下只产生一个read view

              trx_ids:1,3 【当前活跃事务为 1,3,此时事务2已经修改提交】

              low_limit_id:4【当前系统最大事务版本号+1,因为已经开启了3个事务,所以下一个应该是4】

              up_limit_id:1 【创建当前read view 时“系统正处于活跃事务最小版本号,活跃的只有1,3,最小的是1】

              MySql MVCC 详解,第11张

              第二次快照读的时候,当前数据的DB_TRX_ID为2

              (1)DB_TRX_ID < up_limit_id 【2<1】不成立继续判断

              (2)DB_TRX_ID >= low_limit_id 【2>=4】不成立继续判断

              (3)判断DB_TRX_ID是否在活跃事务中,在,则代表在Read View生成时刻,这个事务还是活跃状态,还没有commit,修改的数据,当前事务也是看不到。

              3,场景三

              当前的事务隔离级别为,可重复读(Repeatable Read)

              如果事务中全部都是快照读,不会产生幻读问题,但是当快照读和当前读一起使用的时候就会产生幻读问题。

              Mysql的解决方案是加锁,想要解决这个问题,必须要保证当前读和快照读的数据必须要一致,只能去阻止其他事务进行插入操作,所以只能加锁。

              sql 执行验证  

              1,如果事务中全部都是快照读,不会产生幻读问题

              MySql MVCC 详解,第12张

              2,当快照读和当前读一起使用的时候就会产生幻读问题

              MySql MVCC 详解,第13张

              3,如果事务中全部都是当前读,不会产生幻读问题【加锁实现的】

              MySql MVCC 详解,第14张

               提交后立刻执行

              MySql MVCC 详解,第15张

               4,场景四

              当前的事务隔离级别为,读已提交(Read Committed)

              验证在此隔离级别,每一次快照读都会产生一次新的read view

              MySql MVCC 详解,第16张步骤一,t1时刻同时开启事务

              步骤二,t2时刻事务1进行快照读

              步骤三,t3时刻事务2修改并提交

              步骤四,t4时刻事务1进行快照读

              问题,事务1在t4时刻是否能查询t3时刻事务2提交的数据。

              预期结果能

              事务1在t2时刻执行的是select所以为快照读,此时Read View产生,读已提交(Read Committed)隔离级别下,产生多个Read View。所以事务2在t3更新并提交完成时候,事务1在t4时刻再次进行快照读。此时又产生了新的Read View。新的Read View是在t3时刻事务2修改并提交产生的所以可以查看到。

              sql 验证

              MySql MVCC 详解,第17张 MySql MVCC 详解,第18张

              符合预期结果

              通过算法核对

              第一次查询

              trx_ids:1,2,3 【当前活跃事务为 1,2,3,此时事务2还没开始修改提交】

              low_limit_id:4【当前系统最大事务版本号+1,因为已经开启了3个事务,所以下一个应该是4】

              up_limit_id:1 【创建当前read view 时“系统正处于活跃事务最小版本号,活跃的只有1,3,最小的是1】

              第二次查询

              可重复读(Repeatable Read)事务隔离级别下只产生一个read view

              trx_ids:1,3 【当前活跃事务为 1,3,此时事务2已经修改提交】

              low_limit_id:4【当前系统最大事务版本号+1,因为已经开启了3个事务,所以下一个应该是4】

              up_limit_id:1 【创建当前read view 时“系统正处于活跃事务最小版本号,活跃的只有1,3,最小的是1】

              MySql MVCC 详解,第19张

               第二次快照读的时候,当前数据的DB_TRX_ID为2

              (1)DB_TRX_ID < up_limit_id 【2<1】不成立继续判断

              (2)DB_TRX_ID >= low_limit_id 【2>=4】不成立继续判断

              (3)判断DB_TRX_ID是否在活跃事务中,不在,则说明这个事务在Read View生成之前就已经开始commit,那么修改的结果是能够看见的。