• MySQL 之多版本并发控制 MVCC


           MVCC(多版本并发控制) 没有正式的标准,在不同的 DBMS 中MVCC的实现方式可能不同,本文中讲解的是 InnoDB 中 MVCC 的实现机制(MySQL 其它的存储引擎并不支持 MVCC).

     

    1、MVCC 中的两种读取方式

     
           MVCC 在 MySQL InnoDB 中的实现主要是为了提高数据库并发性能,用更好的方式去处理 读-写冲突,做到即使有读写冲突,也能做到 不加锁,非阻塞并发读,而这个读指的就是 快照读,而非当前读。
           当前读实际上是一种加锁的操作,是悲观锁的实现。而MVCC本质是采用乐观锁思想的一种方式。
           快照读和当前读是 MVCC 中的两种读取方式。MVCC 通过快照读和当前读两种方式,在保持事务隔离性的同时,提高了数据库的并发性能

     

    1.1、快照读

     
    快照读(又叫一致性读),读取的是快照数据,即不加锁的非阻塞读。不加锁的简单 select 都属于快照读。快照读的实现是基于 MVCC(多版本并发控制),在很多情况下,快照读避免了加锁操作,降低了开销。

    • 由于快照读基于 MVCC(多版本并发控制),所以快照读可能读到的并不一定是数据的最新版本,很有可能读的是之前的历史版本数据。
    • 快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读。

     

    1.2、当前读

     
           当前读读取的记录是最新版本的数据,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。加锁的 SELECT,或者对数据进行增删改都会进行当前读。比如:

    # 共享锁
    select * from tb_table lock in share mode;
    # 排它锁
    select * from tb_table for update;
    # 排它锁
    insert into tb_table values# 排它锁
    delete from tb_table where# 排他锁
    update tb_table set
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

     

    2、MVCC实现原理之 ReadView

     
    MVCC 的实现依赖于:隐藏字段、undo log、ReadView.

     

    2.1、隐藏字段

     
    对于InnoDB引擎来说,每个行记录除了记录本身的数据之外,还存在几个隐藏的列。

    • DB_ROW_ID :如果没有为表显式的定义主键,并且表中也没有定义唯一索引,那么InnoDB会自动为表添加一个 row_id 的隐藏列作为主键。
    • DB_TRX_ID :每个事务都会分配一个事务ID,当对某条记录发生变更时,就会将这个事务的事务ID写入 trx_id 中。
    • DB_ROLL_PTR :回滚指针,本质上就是指向 undo log 的指针。
      在这里插入图片描述

    2.2、ReadView

     
           在 MVCC 机制中,多个事务对同一行记录A,进行更新会产生多个历史快照,这个历史快照保存在 undo log 中。当一个事务想要查询 记录A 这行数据时,需要读取哪个版本的记录将是一个问题。ReadView 就可以解决以上 行可见性问题。

    在读未提交的隔离级别下事务,查询的数据都是最新版本,所以不需要MVCC。在串行化隔离级别下的事务,InnoDB 规定使用加锁的方式来访问记录,也不需要MVCC。

    ReadView 就是事务在使用 MVCC 机制进行快照读操作时产生的读视图。ReadView 主要包含了以下4个重要内容:

    • creator_trx_id,创建该 ReadView 的事务ID.

      只要在对表中的记录做改动时(执行 INSERT、DELETE、UPDATE 这些语句时)才会为事务分配事务id,否则在一个只读事务中的事务id只都默认为0.

    • trx_ids,表示生成 ReadView 时,当前系统中活跃的读写事务的 事务id列表

    • up_limit_id,活跃的事务中最小的事务ID.

    • low_limit_id,表示生成 ReadView 时系统中应该分配给下一个事务的 id 值。low_limit_id 是系统最大的事务ID值.

      low_limit_id 并不是trx_ids 中的最大值,事务ID是递增分配的。比如:现在有ID为1、2、3这三个事务,之后ID为3的事务提交了,那么一个新的读事务在生成 ReadView 时,trx_ids 就包括1 和2,up_limit_id 的值就是1,low_limit_id的值就是4.

    ReadView 的规则:在访问某条记录时,需要按照以下步骤(ReadView 的规则)判断记录的某个版本是否可见

    • 如果被访问版本的 trx_id(db_trx_id) 属性值与 ReadView 中的 creator_trx_id 值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
    • 如果被访问版本的 trx_id(db_trx_id) 属性值小于 ReadView 中的 up_limit_id 值,表明生成该版本的事务在当前事务生成 ReadView 前已经提交,所以该版本可以被当前事务访问。
    • 如果被访问版本的 trx_id(db_trx_id) 属性值大于或等于 ReadView 中的 low_limit_id 值,表明生成该版本的事务在当前事务生成 ReadView 后才开启,所以该版本不可以被当前事务访问。
    • 如果被访问版本的 trx_id(db_trx_id) 属性值在 ReadView 的 up_limit_id 和 low_limit_id 之间,就需要判断一下 trx_id 属性值是否在 trx_ids 列表中。
    • 如果在,说明创建 ReadView 时,生成该版本的事务还是活跃的,该版本不可以被访问。
    • 如果不在,说明创建 ReadView时,生成该版本的事务已经被提交,该版本可以被访问。

    MVCC 整体操作流程:

    查询一条记录时,系统通过MVCC机制查找记录的流程:

    • ① 首先获取事务自己的版本号,也就是事务ID.
    • ② 获取 ReadView.
    • ③ 查询得到的数据,然后与 ReadView 中的事务版本号进行比较。
    • ④ 如果不符合 ReadView 规则,就需要从 undo log 中获取历史快照。
    • ⑤ 最后返回符合规则的数据。

           如果某个版本的数据对当前事务不可见,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。

    InnoDB 中,MVCC 是通过 Undo Log + ReadView 进行数据读取, Undo Log 保存了历史快照,而 Read View 规则帮我们判断当前版本的数据是否可见。

     

    2.3、读已提交和可重复读隔离级别下,产生 ReadView 时机的区别

     

    • 在隔离级别为读已提交(Read Commit)时,一个事务中的每一次 SELECT 查询都会重新获取一次 ReadView.

      步骤事务说明
      1begin;
      2select * from tb_table where id > 2;获取一次 ReadView
      3……
      4select * from tb_table where id > 2;获取一次 ReadView
      5commit;
      • 步骤2和步骤4,一样的查询语句,在读已提交的隔离级别下,会分别获取一次 ReadView,如果两次的ReadView 不同,就可能产生不可重复读或者幻读的情况。
    • 当隔离级别为可重复读的时候,一个事务只在第一次 SELECT 时会获取一次 ReadView,而后面所有的 SELECT 都会复用这个 ReadView,所以可以避免不可重复读。
      在这里插入图片描述

     

    3、MVCC 解决幻读

     
           使用例子说明 InnoDB 如何解决幻读,以下示例是在InnoDB引擎的可重复读隔离级别下讨论。现在 tb_table 表中已存在一条 主键id = 1 的数据,该条记录隐藏的 db_trx_id = 10。

    db_trx_id = 10数据:id = 1,name = 张三NULL

    假设现有 事务A 和 事务B 并发执行,事务A 的事务id 为 20,事务B 的事务id 为 30.

    • 步骤 1:事务A 开始第一次查询数据。

      – 事务A 开始第一次查询数据

      select * from tb_table where id >=1;
       
      ① --> 在开始查询前,MySQL 会为事务A产生一个 ReadView,此时 ReadView 的内容如下:trx_ids = [20,30],up_limit_id = 20,low_limit_id = 31,creator_trx_id = 20.
       
      ② --> 执行select * from tb_table where id >=1;查出id=1,db_trx_id= 10的数据,按ReadView机制判断该行数据是否可见。
       
      ③ --> 按 ReadView 机制 该查询出的数据 db_trx_id = 10 ,小于事务A的ReadView 的up_limit_id,故该数据在事务A 开启之前,其他事务已提交,所以该行数据可见。
       
      结论:事务A 的第一次查询,能读到一条数据,id=1,name= 张三。

    • 步骤2:接着事务B(事务id=30),往tb_table 插入两条数据,并提交事务(假设不考虑 gap lock)。

      insert into student(id,name) values (2,'李四')insert into student(id,name) values (3,'王五')
      • 1
      • 2

      此时 tb_table 中就要三条数据了,对应的 undo log 如下。
      在这里插入图片描述

    • 步骤3:接着事务A开启第二次查询,由于在可重复读隔离级别下,事务A不会再次生成ReadView.

      – 事务A 开始第二次查询数据

      select * from tb_table where id >=1;
       
      ① --> 此时tb_table中的3条数据都满足 id>=1 的条件,因此会先查出来,然后根据 ReadView 机制,判断每条数据是不是都可以被事务A可见。
       
      ② --> 首先 id = 1 这条数据同步骤1,对事务A可见。
       
      ③ --> 然后是 id = 2 的数据,它的 trx_id = 30,介于 ReadView 的 up_limit_id = 20 和 low_limit_id = 31 之间,需要再次判断 trx_id = 30 是否处于 ReadView 的 trx_ids=[20,30] 数值内。结果在trx_ids数组内,表示 id= 2 这条数据是与事务A 在同一时刻启动的其他事务提交的,所以这条数据不能被事务A可见。
       
      ④ --> 同理,id = 3 这条数据,trx_id 也为 30,因此也不能被事务A 可见。
       
      结论:最终事务A 的第二次查询,只能查询出 id = 1 的这条数据,这和事务A 的第一次查询的结果一样,因此没有出现不可重复读现象

      在这里插入图片描述

    注意只靠MVCC不能完全解决幻读问题。虽然MVCC可以提供事务的一致性视图,并保持数据的一致性版本,但它本身并不能防止其他事务插入新的数据。通过结合 next-key locking 和 MVCC,InnoDB 在可重复读隔离级别下解决了幻读问题,确保了事务在多次读取相同范围数据时的一致性

          间隙锁是在 innodb的 可重复读级别才会生效,且 Next-Key Locks 实际上是由间隙锁加行锁实现的MySQL 在 InnoDB 存储引擎下,可重复读的隔离级别下,不存在幻读问题
     

    4、总结

     
    MVCC(多版本并发控制)的核心点在于 ReadView 的原理,READ COMMITTED、REPEATABLE READ 这两个隔离级别的一个很大不同就是生成 ReadView 的时机不同:

    • READ COMMITTED每一次进行普通 select 操作前,都会生成一个 ReadView.
    • REPEATABLE READ 只在第一次进行普通 select 操作前生成一个 ReadView,之后的查询操作都重复使用这个 ReadView 就好了。

     

    MVCC(多版本并发控制)解决的问题

    • 读写之间阻塞的问题。通过 MVCC 可以让读写相互不阻塞,即读不阻塞写,写不阻塞读,可以提升事务并发处理能力。
    • 降低了死锁的概率。因为 MVCC 采用了乐观锁的方式,读取数据时并不需要加锁,对于写操作,也只锁定必要的行。
    • 解决快照读的问题。当查询数据库在某个时间点的快照时,只能看到这个时间点之前事务提交更新的结果,而不能看到这个时间点之后事务提交的更新结果。

     

     

  • 相关阅读:
    腾讯云我的世界mc服务器多少钱一年?
    105AspectRatio调整宽高比组件_flutter
    基于SVM+Webdriver的智能NBA常规赛与季后赛结果预测系统——机器学习算法应用(含python、ipynb工程源码)+所有数据集(四)
    ArcGIS:如何简单地制作一幅专题地图?
    学习MongoTemplate操作MongoDB增删改查
    二十二、W5100S/W5500+RP2040树莓派Pico<SMTP发送邮件>
    自动化测试中,该如何高效管理测试数据?
    Django中的缓存
    只开源36小时!GitHub标星139K从Java开发到软件架构师进阶笔记
    物联网终端算法
  • 原文地址:https://blog.csdn.net/weixin_41922349/article/details/134503927