【MySQL】MySQL事务(三)

发布于:2025-05-14 ⋅ 阅读:(17) ⋅ 点赞:(0)

        前言:本节内容是事务里面最难的一部分, 就是理解mvcc快照读和read view。这两个部分需要了解隔离性里面的四种隔离级别。 博主之前讲过,但是担心友友们不了解, 所以这里开头进行了复习。 下面开始我们的学习吧!

        ps:本节内容很难, 希望友友们沉下心认真学习哦!

目录

一致性

三种并发方式的安全隐患

三个隐藏字段

Undo日志

mvcc多版本控制

read view

 RC VS RR的本质区别


我们先来回忆一下上接内容讲解的隔离性的四种隔离级别, 以及并发产生的问题:

  • 脏读:一个事务在执行中,读到另一个事务的更新但是还没有commit的数据, 这种情况叫做脏读。
  • 不可重复读:一个事务提交前和提交后, 另一个事务select查到的数据结果是不一样的。 也就是说一个事务一直select, 结果某一次select的时候, 数据突然变了。
  • 幻读:读着读着可能多出来了一条数据,就类似于出现了幻觉。 
  • 读未提交: 会导致脏读,不可重复读, 幻读现象。
  • 读提交 :解决了脏读, 但是有不可重复读, 幻读。
  • 可重复单独: 解决了脏读, 不可重复读, 但是有些数据库的insert会有幻读现象(mysql解决了幻读)
  • 串行化:加锁, 不进行并发执行, 没有上面的问题。

一致性

        事务执行的结果,必须使数据库从一个一致性状态,变到另一个一致性状态。当数据库只包含事务成功提交的结果时,数据库处于一种一致性状态。如果系统运行发生中断,某个事务尚未完成而被迫中断,而改变未完成的事务对数据库所做的修改已被写入数据库,此时数据库就处于一种不正确的状态。因此一致性是通过原子性保证的。

三种并发方式的安全隐患

  • 读读并发:不存在任何问题,不需要并发控制。
  • 读写并发:有线程安全问题,可能造成事务隔离性问题可能遇到脏读,幻读,不可重复读
  • 写写并发:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失 

        读写并发,也就是一个事务在写,一个事务在读。要保证读到的是原来的,和另一个事务写的不一样。
        这个使用的是一种无锁的并发控制MVCC。

  • 1、每个事务都要有自己的事务ID,可以根据事务的大小,来决定事务到来的先后顺序。
  • 2、mysqld可能会面临处理多个事务的情况。事务在使用者的角度看来是原子的,但是本质上是有过程的。所以就证明事务也有自己的生命周期。一》mysqld要对事务进行管理:先描述,再组织。事务在我看来,mysqld中一定是对应的一个或者一套结构体对象/类对象
  • 3、所以,事务也要有自己的结构体。 

下面我们都在讲解mvcc以及版本链, 很重要, 很难理解, 需要反复观看。

三个隐藏字段

其实我们之前在建表的时候, mysql也会给我们添加三个默认的,隐藏的字段:

  • DB_TRX_ID:6byte,每一条数据后面都要有一个属性,就是DB_TRX_ID,无论是单sql,还是我们手动begin起的事务。都算是事务,都有自己的事务ID,而DB_TRX_ID,就是最后操作这条数据的事务ID。
  • DB_ROLL_PTR:在MySQL中,对于某一行数据来说,我们要修改他,并不是直接就修改了。 mysql要先将数据保存一份,然后再改 这样的话,我们改完之后,我们也知道未来这个字段历史是谁。所以,为了保证能够找到这个最后的历史信息,就有了这个DB_ROLL_PTR。
  • DB_ROW_ID:隐藏的自增ID,如果数据库表中没有主键,InnoDB会自动以DB_ROW_ID产生一个聚族索引。大小6byte

Undo日志

        上面的红框框就是操作系统, 然后上层就是用户层。里面有我们的mysql的一个buffer pool , 然后undo日志就是在这个buffer pool里面。undo log其实就是mysql里面的一段内存缓冲区。作用就是用来记录一些mysql里面事务的回滚操作等。

        假如现在有一行数据如下:

        这个数据我们知道是在b+树的叶子节点上面。(这个叶子节点此时被加载到了Buffer Poll里面),现在有一个事务10, 想要对这行数据进行修改, 将名字张三改成李四。       

        因为要修改, 所以先将这条记录加锁, 然后将这条数据记录复制一份到undo log里面:

        然后就开始修改, 现存数据name改成李四, 事务ID改成10, 然后回滚指针指向刚刚拷贝到undo log里面的数据。这就叫做历史版本。

        最后commit, 释放锁。 就行了。 (注意,这里示例是写写并发, 串行化, 加锁。)

        然后现在又来了一个事务11,也要进行修改, 要将这个age修改成28. 那么就和上面一样, 先拷贝一份数据到undo log里面。

        然后修改记录:

        所以, 我们就有了一个历史版本链, 如果未来我们不想修改了, 后悔了, 就可以把undo log里面的历史数据拿出来。 

mvcc多版本控制

        我们把上面的版本链, 这种多版本控制就叫做mvcc多版本控制。而里面的一个一个的版本的数据, 我们就叫做快照。 

        另外, undo log是一个临时缓冲区。 他管理的是一个事务内所形成的版本链条。 什么意思? 就是说, 一个事务里面, 我们多次修改,多次update,才会形成版本链; 同样的, delete数据也能形成版本链, 因为delete的本质是把改行数据的"flag“置为删除。 所以也可以形成版本链。

        那么,insert可以形成版本链吗?——其实,insert可以理解为是没有版本链的,但是insert对应的数据也要放到undo log里面。对于insert来说, 如果一旦commit, 这个insert对应的版本链也就可以被清空了。 而对于undate和delete来说就不一定, 因为可能也有其他的事务在访问这一条记录。 

        我们上面说的是update和delete, 以及insert。 那么select呢?对于前两者,update和delete的对象一定是最新数据, 历史数据他们没有权限修改。对于后者,select我们怎么知道我们读取的是最新数据还是历史数据呢? 所以就有了当前读和快照读的概念。 

  •         当前读:读取最新的记录, 就是当前读, 增删改,都叫做当前读。select也可能当前读。
  •         快照读:读取历史版本,就叫做快照读。

        那么现在再来看我们之前的读写操作。 我们做过实验, 看到的是两个事务, 一个写一个读。 然后两个事务看到的数据是不同的数据。——就是因为我们的读取, 读取的是历史的数据。 而删和改,删和改的是当前的数据。 读和删改事务在访问不同的位置(访问不同的版本), 就不需要进行加锁, 所以就不会发生并发的问题, 我们就可以并发进行读写操作。

        所以, 隔离性, 本质上是数据上的隔离, 也是版本上面的隔离。而不同的隔离级别就让我们看到不同的版本。

        所以, 隔离, 隔离性本质上使用mvcc实现的,我们的事务回滚利用的也是利用mvcc版本控制的。

read view

        read view我们讨论的是rc和rr这两种隔离级别。read view就是事务进行快照读操作的时候生产的读视图,在该事务执行快照读的那一刻,会生成数据库当前的一个快照,记录并维护系统当前活跃事务的ID。(当每个事务开启时,都会被分配给一个ID,这个ID是递增的。所以最新的事务的ID更大)

        read view是一个类,与事务的关系就类似于进程地址空间和PCB。两个数据结构之间使用指针连接起来。

        当我们进行快照读的时候,对该记录创建一个read view读视图。然后对它填充数据,作为条件。 用来判断当前事务能够看到哪个版本的数据,既可能是当前数据, 也可能是undo log里面的历史数据。下面看一下这个read view结构体:

class Readview
{
private:
    trx_id_t m_low_limit_id;  //高水位,大于等于这个ID的事务均不可见。
    trx_id_t m_up_limit_id;  //低水位, 小于这个ID的事务均可见。
    trx_id_t m_creator_trx_id;     //创建该Read view的事务ID。    
    ids_t m_ids; //创建视图时的活跃事务id列表。
    trx_id_t m_low_limit_no; 
    bool m_closed;  //视图是否关闭
    //....省略       
};

        上面重要的是两个水位线 , 事务id。m_ids就是当前正在打开的事务id列表。

现在一张图我们来理解read view:

        首先, 生成一个read view, read view里面的m_ids保存了快照读的一瞬间还在进行的事务的ID。 m_ids里面的最小的事务ID作为read view里面的低水位线up_limit_id,m_ids里面的最大的事务ID作为read view里面的高水位线low_limit_id。然后如果当前事务查看某个版本的数据的时候, 如果这个数据的ID小于低水位线, 就说明操作这个数据的事务是比当前最早活跃的事务还要早, 那么肯定可以看到; 如果这个数据的ID大于高水位线, 就说明操作这个数据的事务是比当前最晚活跃的事务还要晚,那么就不应该看到; 如果这个数据的ID在两个水位线之间, 此时如果这个ID不是活跃的事务ID, 说明在快照读的时候这个事务已经被提交了, 也应该看到,反之就不应该看到。   

        另外,我们还要注意的是!read view是事务可见性的一个类,不是事务创建出来, 就有read view, 而是这个已经存在的事务, 首次进行快照读的时候,mysql形成read view!

 RC VS RR

        rc 和rr的区别就在于, rc在每一次select 查询的时候, 都生成一次read view, 即每次select 都是快照读。 而rr只有在第一次select的时候, 才会生成一次read view, 之后每次select 服用这个read view, 即只有第一次才会生成快照读。

        有了上面的read view的知识点之后, 我们就能谈RC和RR的本质区别了。我们谈论RC和RR的区别, 需要使用一个例子, 现在我们做几个实验。 
        首先第一个实验:

        这是account表。 并且此时的隔离级别是可重复读。然后我们两边都开启事务:

然后两边都进行快照读, 记录当前的read view:

接下来我们左边修改数据,然后commit:

然后我们查一下右边:

很显然, 因为是可重复读, 所以数据没有变化。 这也符合我们的预期。

现在我们来看一下实验二:

        同样是隔离级别为可重复读。 但是这次右边不进行快照读。 而是等到左边commit之后,在进行快照读:

查右边:

我们会发现, id = 4这个字段被修改了。 也就是说我们的右边看到了左边的修改。 

        这里为什么会发生这种情况, 是因为我们的read view是在快照读的时候就生成, 然后RR隔离界别的read view只会生成一次。 就是第一次快照读的时候。 所以在第一个实验中, 我们一开始就使用了快照读。 此时m_ids显示左右两个事务都是活跃事务, 那么我们进行判断的时候, 右边的事务对于左边事务修改的版本就看不见。 

        实验二的时候, 我们一开始没有快照读, 而是等到左边事务commit后才进行快照读。那么右边事务的m_ids里面就一定没有左边事务的ID, 并且,因为左边事务启动在右边事务快照读之前所以左边事务的事务ID也不可能比右边事务的事务的low_limt_id大。 那么这个时候左边事务的事务ID只有两种情况, 一种情况是比右边事务的up_limit_id还要小。或者是在up_limit_id和low_limit_id之间但是没在m_ids里面。 这两种情况下右边事务都可以看到左边事务的修改版本!!!所以右边事务就能看到左边事务的修改数据版本!!!

        上面是RR。RC就好说了, RR是只能生成一次read view, 只能进行一次快照。 后面的事务无论做什么修改, 都要根据这一个快照进行判断能不能够看到这些版本数据。 但是RC每次进行快照读都能生成快照, 每次快照读都生成快照, 那么low_limit_id就永远是当前系统下最大的事务ID。 所以就能保证只要其他事务commit了, 其他事务不是活跃事务了, 那么就只剩下了在up_limit_id和low_limit_id之间但是没在m_ids里面这两种情况。 也就一定能看到了。 所以这就是RC的形成原因。  这就是RR和RC的根本区别!

——————以上就是本节全部内容哦, 如果对友友们有帮助的话可以关注博主, 方便学习更多知识哦!!!