• 【2. MVCC-多版本并发控制技术】


    快照读与当前读

    快照读(普通select语句)通过MVCC方式解决幻读

    • MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理 读-写冲突 ,做到即使有读写冲突时,也能做到 不加锁 , 非阻塞并发读 ,而这个读指的就是 快照读(属于乐观锁) , 而非 当前读 。

    当前读(select...for update等语句)通过next-key lock(记录锁+间隙锁)

    • 当前读实际上是一种加锁的操作,是悲观锁的实现。而MVCC本质是采用乐观锁思想的一种方式。
    • 当前读读取的是记录的最新版本(最新数据,而不是历史版本的数据),读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁

    隔离级别

    MySQL通过MVCC解决幻读,MVCC多版本并发控制结束,多版本通过undolog实现,管理通过ReadView实现

    • 事务有四个隔离级别,可能存在三种并发问题:

    在这里插入图片描述

    • 但是在MySQL中,默认的隔离级别是可重复读,可以解决脏读和不可重复读,如果从定义来看,它不能解决幻读,只能采取可串行化
    • 但是MVCC可以不采用锁机制,而是通过乐观锁方式解决幻读,所以MySQL在可重复读也解决了幻读问题。
      在这里插入图片描述

    这四种隔离级别具体是如何实现的呢?

    • 对于「读未提交」隔离级别的事务来说,因为可以读到未提交事务修改的数据,所以直接读取最新的数据就好了;
    • 对于「串行化」隔离级别的事务来说,通过加读写锁的方式来避免并行访问;
    • 对于「读提交」和「可重复读」隔离级别的事务来说,它们是通过 Read View 来实现的,它们的区别在于创建 Read View 的时机不同,大家可以把 Read View 理解成一个数据快照,就像相机拍照那样,定格某一时刻的风景。「读提交」隔离级别是在「每个语句执行前」都会重新生成一个 Read View,而「可重复读」隔离级别是「启动事务时」生成一个 Read View,然后整个事务期间都在用这个 Read View。

    注意,执行「开始事务」命令,并不意味着启动了事务。在 MySQL 有两种开启事务的命令,分别是:

    • 第一种:begin/start transaction 命令;
    • 第二种:start transaction with consistent snapshot 命令;

    这两种开启事务的命令,事务的启动时机是不同的:

    • 执行了 begin/start transaction 命令后,并不代表事务启动了。只有在执行这个命令后,执行了增删查改操作的 SQL 语句,才是事务真正启动的时机;
    • 执行了 start transaction with consistent snapshot 命令,就会马上启动事务。

    总结:

    • MVCC读的是快照,也就是乐观锁的实现方式,幻读不会出现,相当于解决了幻读,但是并不是编程串行化了,串行化读是通过加锁,一个一个的进行解决。

    隐藏字段、Undo log版本链

    • 针对每一条记录(行格式),有三个隐藏字段
      • UUID:如果没有主键也没有唯一性索引,默认提供一个UUID隐藏字段
      • trx_id:记录最近一个更新事务的ID(当一个事务对某条聚簇索引记录进行改动时,就会把该事务的事务 id 记录在 trx_id 隐藏列里)
      • roll_pointer:回滚指针,是通过和undo log日志,旧版本写入到undo日志中,旧版本通过链表指针相连接,roll_point指向最近的undo log记录
        在这里插入图片描述

    ReadView

    • 使用 READ COMMITTEDREPEATABLE READ 隔离级别的事务,都必须保证读到 已经提交了的 事务修改过的记录。假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的
    • 核心问题就是需要判断一下版本链中的哪个版本是当前事务可见的,这是ReadView要解决的主要问题
    1. creator_trx_id ,创建这个 Read View 的事务 ID。
    2. m_ids ,表示在生成ReadView时当前系统中「活跃且未提交」的 事务id列表 (“活跃事务”指的就是,启动了但还没提交的事务。)
    3. min_trx_id ,创建 Read View 时,当前数据库中「活跃事务且未提交的」事务中最小事务的事务 id
    4. max_trx_id ,表示生成ReadView时系统中应该分配给下一个事务的 id 值。max_trx_id 是系统最大的事务id值,这里要注意是系统中的事务id,需要区别于正在活跃的事务ID。
      在这里插入图片描述
      在这里插入图片描述

    在这里插入图片描述

    MVCC整体操作流程/如何解决幻读(重点)

    1. 首先获取事务自己的版本号,也就是事务 ID;
    2. 获取 ReadView;(在第一个查询语句后,会创建一个Read View后续的查询语句利用这个 Read View)
    3. 查询得到的数据,然后与 ReadView 中的事务版本号进行比较;
    4. 如果不符合 ReadView 规则,就需要从 Undo Log 中获取历史快照;
    5. 最后返回符合规则的数据。

    总结

    1. MVCC 在 READ COMMITTDREPEATABLE READ 这俩个隔离级别才有效,因为这俩个情况下考虑快照(读旧数据),而剩下的读为提交和串行化,读的都是最新数据
    2. 核心点在于 ReadView 的原理, READ COMMITTDREPEATABLE READ 这两个隔离级别的一个很大不同
      就是生成ReadView的时机不同:
      • READ COMMITTD 在每一次进行普通SELECT操作前都会生成一个ReadView
      • REPEATABLE READ 只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复
        使用这个ReadView就好了。

    在这里插入图片描述

    当前读是如何避免幻读的?

    • MySQL 里除了普通查询是快照读,其他都是当前读,比如 update、insert、delete,这些语句执行前都会查询最新版本的数据,然后再做进一步的操作。
    • 假设你要 update 一个记录,另一个事务已经 delete 这条记录并且提交事务了,这样不是会产生冲突吗,所以 update 的时候肯定要知道最新的数据。
    • 使用间隙锁,对范围加锁使得插入或者删除或者更新无法操作

    在这里插入图片描述

    幻读没有被完全解决?

    场景一:

    • 在可重复读隔离级别下,事务 A 第一次执行普通的 select 语句时生成了一个 ReadView,之后事务 B 向表中新插入了一条 id = 5 的记录并提交。接着,事务 A 对 id = 5 这条记录进行了更新操作,在这个时刻,这条新记录的 trx_id 隐藏列的值就变成了事务 A 的事务 id,之后事务 A 再使用普通 select 语句去查询这条记录时就可以看到这条记录了,于是就发生了幻读。

    场景二:

    • T1 时刻:事务 A 先执行「快照读语句」:select * from t_test where id > 100 得到了 3 条记录。
    • T2 时刻:事务 B 往插入一个 id= 200 的记录并提交;
    • T3 时刻:事务 A 再执行「当前读语句」 select * from t_test where id > 100 for update 就会得到 4 条记录,此时也发生了幻读现象。

    要避免这类特殊场景下发生幻读的现象的话,就是尽量在开启事务之后,马上执行 select … for update 这类当前读的语句,因为它会对记录加 next-key lock,从而避免其他事务插入一条新记录。

  • 相关阅读:
    docker基础命令操作---镜像操作
    【计算机网络】MTU和MSS
    stm32-SPI协议
    微信小程序 | 酷炫时钟样式整理【附源码】
    MVC4自带的JS、CSS优化功能
    Gateway 网关
    【python二级-练习题】
    java基础-集合-HashMap(JDK20)学习
    mysql
    kubernetes 通过HostAliases属性配置域名解析
  • 原文地址:https://blog.csdn.net/weixin_45043334/article/details/127707186