zoukankan      html  css  js  c++  java
  • MySQL学习(五)事务隔离与MVCC

    文章部分图片和总结来自参考资料,半原创

    概述

    MySQL的可重复读隔离级别,事务T启动的时候会创建一个视图read-view,之后事务T执行期间,即使有其他事务修改了数据,事务T看到的仍然跟在启动时看到的一样。也就是说,一个在可重复读隔离级别下执行的事务,好像与世无争,不受外界影响。

    可重复读的含义

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

    MVCC

    MVCC 是什么

    多版本控制(Multiversion Concurrency Control): 指的是一种提高并发的技术。MySQL中, MVCC在 Read Committed 和 Repeatable Read两个隔离级别下工作。

    MySQL的InnoDB存储引擎默认事务隔离级别是RR(可重复读),是通过 "行级锁+MVCC"一起实现的,正常读的时候不加锁,写的时候加锁。而 MVCC 的实现依赖:隐藏字段、Read View、Undo log。

    隐藏字段

    InnoDB存储引擎在每行数据的后面添加了三个隐藏字段:

    1. DB_TRX_ID(6字节):表示最近一次对本记录行作修改(insert | update)的事务ID。至于delete操作,InnoDB认为是一个update操作,不过会更新一个另外的删除位,将行表示为deleted。并非真正删除。

    2. DB_ROLL_PTR(7字节):回滚指针,指向当前记录行的undo log信息

    3. DB_ROW_ID(6字节):随着新行插入而单调递增的行ID。理解:当表没有主键或唯一非空索引时,innodb就会使用这个行ID自动产生聚簇索引。如果表有主键或唯一非空索引,聚簇索引就不会包含这个行ID了。这个DB_ROW_ID跟MVCC关系不大。

    聊完隐藏字段我们再来看一下 read-view .

    Read View

    read view 创建的时机

    read view 是在SQL语句执行之前申请的,其中

    • RC隔离级别是每个SELECT都会申请
    • RR隔离级别的read view是事务开始之后的第一个SQL申请,之后事务内的其他SQL都使用该read view

    read view 的构成

    read view 有几个和 mvcc 相关的字段

    • low_limit_id:目前出现过的最大的事务ID+1,即下一个将被分配的事务ID。
    • up_limit_id:活跃事务列表trx_ids中最小的事务ID,如果trx_ids为空,则up_limit_id 为 low_limit_id。
    • trx_ids:Read View创建时其他未提交的活跃事务ID列表,不包括当前事务自己和已提交的事务

    Undo log

    Undo log中存储的是老版本数据,当一个事务需要读取记录行时,如果当前记录行不可见,可以顺着undo log链找到满足其可见性条件的记录行版本。

    大多数对数据的变更操作包括 insert/update/delete,在InnoDB里,undo log分为如下两类:

    • insert undo log : 事务对insert新记录时产生的undo log, 只在事务回滚时需要, 并且在事务提交后就可以立即丢弃。

    • update undo log : 事务对记录进行delete和update操作时产生的undo log,不仅在事务回滚时需要,快照读也需要,只有当数据库所使用的快照中不涉及该日志记录,对应的回滚日志才会被purge线程删除。

        Purge线程:为了实现InnoDB的MVCC机制,更新或者删除操作都只是设置一下旧记录的deleted_bit,并不真正将旧记录删除。
        为了节省磁盘空间,InnoDB有专门的purge线程来清理deleted_bit为true的记录。purge线程自己也维护了一个read view,如果某个记录的deleted_bit为true,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录一定是可以被安全清除的。
      

    记录行修改的具体流程

    下面的例子和描述来自 : https://blog.csdn.net/Waves___/article/details/105295060#1.2、Read View 结构,非原创

    假设有一条记录行如下,字段有Name和Honor,值分别为"curry"和"mvp",最新修改这条记录的事务ID为1。
    1297993-20210908122809703-49931198.png

    (1)现在事务A(事务ID为2)对该记录的Honor做出了修改,将Honor改为"fmvp":

    • 事务A先对该行加排它锁
    • 然后把该行数据拷贝到undo log中,作为旧版本
    • 拷贝完毕后,修改该行的Honor为"fmvp",并且修改DB_TRX_ID为2(事务A的ID), 回滚指针指向拷贝到undo log的旧版本。(然后还会将修改后的最新数据写入redo log)
    • 事务提交,释放排他锁

    1297993-20210908122841987-1946285119.png

    (2)接着事务B(事务ID为3)修改同一个记录行,将Name修改为"iguodala":

    • 事务B先对该行加排它锁
    • 然后把该行数据拷贝到undo log中,作为旧版本
    • 拷贝完毕后,修改该行Name为"iguodala",并且修改DB_TRX_ID为3(事务B的ID), 回滚指针指向拷贝到undo log最新的旧版本。
    • 事务提交,释放排他锁

    1297993-20210908122841987-1946285119.png

    从上面可以看出,不同事务或者相同事务的对同一记录行的修改,会使该记录行的undo log成为一条链表,当某个事务进来后会看到最新的 tx_id 对应的记录 ,然后根据可见性规则进行判断当前这条记录是否是自己这个事务可见的, 假如是不可见的, 那么将会沿着回滚指针往下查找, 找到自己事务可以见到的记录. 通过这个过程我们可以知道 ,某个记录要是被多个事务并发更新 ,必定会增长回滚记录链 ,这又给我们优化提供了一个思路.

    可见性规则

    下面的例子和描述来自 : https://blog.csdn.net/Waves___/article/details/105295060#1.2、Read View 结构,非原创

    在innodb中,创建一个新事务后,执行第一个select语句的时候,innodb会创建一个快照(read view),快照中会保存系统当前不应该被本事务看到的其他活跃事务id列表(即trx_ids)。当用户在这个事务中要读取某个记录行的时候,innodb会将该记录行的DB_TRX_ID与该Read View中的一些变量进行比较,判断是否满足可见性条件。

    假设当前事务要读取某一个记录行,该记录行的DB_TRX_ID(即最新修改该行的事务ID)为trx_id,Read View的活跃事务列表trx_ids中最早的事务ID为up_limit_id,将在生成这个Read Vew时系统出现过的最大的事务ID+1记为low_limit_id(即还未分配的事务ID). 规则如下 :

    1. 如果 trx_id < up_limit_id, 那么表明“最新修改该行的事务”在“当前事务”创建快照之前就提交了,所以该记录行的值对当前事务是可见的。跳到步骤5。

    2. 如果 trx_id >= low_limit_id, 那么表明“最新修改该行的事务”在“当前事务”创建快照之后才修改该行,所以该记录行的值对当前事务不可见。跳到步骤4。

    3. 如果 up_limit_id <= trx_id < low_limit_id, 表明“最新修改该行的事务”在“当前事务”创建快照的时候可能处于“活动状态”或者“已提交状态”;所以就要对活跃事务列表trx_ids进行查找(源码中是用的二分查找,因为是有序的):
      (1) 如果在活跃事务列表trx_ids中能找到 id 为 trx_id 的事务,表明①在“当前事务”创建快照前,“该记录行的值”被“id为trx_id的事务”修改了,但没有提交;或者②在“当前事务”创建快照后,“该记录行的值”被“id为trx_id的事务”修改了(不管有无提交);这些情况下,这个记录行的值对当前事务都是不可见的,跳到步骤4;
      (2)在活跃事务列表中找不到,则表明“id为trx_id的事务”在修改“该记录行的值”后,在“当前事务”创建快照前就已经提交了,所以记录行对当前事务可见,跳到步骤5。

    4. 在该记录行的 DB_ROLL_PTR 指针所指向的undo log回滚段中,取出最新的的旧事务号DB_TRX_ID, 将它赋给trx_id,然后跳到步骤1重新开始判断。

    5. 将该可见行的值返回。

    也就是说 up_limit_idlow_limit_id 这两个是水线 , 而中间这段区域则需要结合活跃事务列表来判断.

    总结 :

    • 版本未提交,不可见;

    • 版本已提交,但是是在视图创建后提交的,不可见;

    • 版本已提交,而且是在视图创建前提交的,可见。

    例子一

    1297993-20210908142027215-1710278964.png

    1297993-20210908141946515-1816467941.png

    例子二

        看上面的例子我们还是很懵到底可重复读是如何实现的?实际上,(以下是通俗的理解,具体的需要查看源码实现)

    • row : 维护一个链表
    • 事务 :维护一个有时间排序的活跃的数组(活跃的指的是当前开启事务的)
          row上的链表保存的是对该行修改已经提交的事务;每个事务的保存的活跃数组则是当前该表相关的事务数组,按时间排序,是有序的。
          例如以下的例子。

    1297993-20200117145805364-1870027912.png

    1297993-20200117150506081-2096922834.png

        可以看到,当A去查询某行记录的时候,它顺着该row 已经提交的链表查询,一直查到自己的一个txn_id ,而其他的事务不会被读到。
    当B更新的时候,则是先读后更新的,这个读,只能读当前的值,而不需要顺着链表确定自己读取的值是多少,成为当前读(current read )

        事务去row 链表确定自己读取的值,有这样的规则,

    1297993-20200117151045507-571653008.png

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

    • 找到(1,3)的时候,判断出row trx_id=101,比高水位大,处于红色区域,不可见;
    • 接着,找到上一个历史版本,一看row trx_id=102,比高水位大,处于红色区域,不可见;
    • 再往前找,终于找到了(1,1),它的row trx_id=90,比低水位小,处于绿色区域,可见。
      这样执行下来,虽然期间这一行数据被修改过,但是事务A不论在什么时候查询,看到这行数据的结果都是一致的,所以我们称之为一致性读。

        假如C事务被换成了 C' 的执行逻辑

    1297993-20200117151227649-1657960339.png

    那么当B在更新数据的时候,C'还没有提交,B就会一直阻塞在那里,因为该行的写锁被获取了。

    例子三(MySQL45讲课程的例子)

        假如有多个事务,每个事务都生成一个数据的全量拷贝下来,那么数据量是吓人的,MySQL 肯定不是这种方案。
        InnoDB里面每个事务有一个唯一的事务ID,叫作transaction id。它是在事务开始的时候向InnoDB的事务系统申请的,是按申请顺序严格递增的。
        而每行数据也都是有多个版本的。每次事务更新数据的时候,都会生成一个新的数据版本,并且把transaction id赋值给这个数据版本的事务ID,记为row trx_id。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它。也就是说,数据表中的一行记录,其实可能有多个版本(row),每个版本有自己的row trx_id。

    如图2所示,就是一个记录被多个事务连续更新后的状态。

    1297993-20200115171531116-1154645035.png

    如上图要是有3个事务开启,transaction_id 分别为 15,17,25 那么看到的值分别是 10,11,22 ,可以想象有这样的场景,transaction_id = 15 开启事务看到k 为 10 ,然后transaction_id 为 16也开启了事务,让k更新为 11 ,commit ,transaction_id 为17的开启了事务过来看热闹,那么它拿到的k 为 11 ,后面的 transaction_id 为 25也是同样的道理 ,而为了记录每个事务对该行的操作,k分别从初始值10被set 成不同的值而不是直接记录k 的具体数值。

    set k = k+1 
    set k = k*2 
    
    

        你可能会问,前面的文章不是说,语句更新会生成undo log(回滚日志)吗?那么,undo log在哪呢?
        实际上,图2中的三个虚线箭头,就是undo log;而V1、V2、V3并不是物理上真实存在的,而是每次需要的时候根据当前版本和undo log计算出来的。比如,需要V2的时候,就是通过V4依次执行U3、U2算出来。
        到了这里我们可以知道 undo-log 就是来保存多个版本某行数据的变化逻辑,并且并不是直接储存数值,而是通过逻辑推算出来的。

    例子四

    https://tech.youzan.com/-ci-da-liang-shan-chu-dao-zhi-mysqlman-cha-de-fen-xi/ 有赞的慢查询,推荐一看

    查看和设置事务隔离级别

    SET [GLOBAL | SESSION ] TRANSACTION ISOLATION LEVEL 
    {
    	READ UNCOMMITTED 
        | READ COMMITTD 
        | REPEATABLE READ 
        | SERIALIZABLE   
    }
    
    
    //查看当前会话隔离级别 
    SELECT @@tx_isolationG;
    
    //查看全局隔离级别  
    SELECT @@GLOBAL.tx_isolationG;
    
    
    

    参考资料

  • 相关阅读:
    【CF1029A】Many Equal Substrings(模拟)
    【CF1028C】Rectangles(线段树)
    【CF1028B】Unnatural Conditions(构造)
    【CF1028A】Find Square(签到)
    【CF1025C】Plasticine zebra(模拟)
    【CF1025A】Doggo Recoloring(签到)
    167.数据传送指令
    166.寻址方式
    165.基础
    164.多媒体操作系统
  • 原文地址:https://www.cnblogs.com/Benjious/p/15242838.html
Copyright © 2011-2022 走看看