mysql进阶之锁、INNODB引擎详解

miloyang
0 评论
/ /
753 阅读
/
20372 字
27 2023-09

MySQL的基础内容如下,看完后你就是合格的增删改查程序员了。通过 itcast课程进行整理。

分类 全称 说明 链接
DDL Data Definition Language 数据定义语言,用来定义数据库对象,如:数据库、表、字段等等 DDL
DML Data Manipulation Language 数据操作语言,用来对数据表中的数据进行增、删、改 DML
DQL Data Query Language 数据查询语言,用来查询 数据库中表的记录 DQL
DCL Data Control Language 数据控制语言,用来创建数据库用户、控制数据库的访问权限 DCL

本章节主要介绍mysql的锁相关以及INNODB引擎及事务原理

锁是计算机协调多个进程或线程并发访问某一资源的机制。在数据库中,除传统的计算资源(CPU,RAM,I/O)的争用以外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素,因此从这个角度出发,锁对数据库而言显得尤其重要,也更加复杂。
在mysql中的锁,按照锁的粒度分为以下三类:

  • 全局锁:锁定数据库中的所有表。
  • 表级锁:每次操作锁住整张表。
  • 行级锁:每次操作锁住对应的行数据。

全局锁

就是对整个数据库实例加锁,加锁后整个实例就处于只读状态,后续的DML的写语句,DDL语句,已经更新操作的事务提交语句都将被阻塞。
典型的使用场景就是做全库的逻辑备份,对所有的表进行锁定,从而获取一致性视图,保证数据的完整性。假设在订单系统中需要备份整个库,假设不加全局锁的话,在备份的过程中有新订单的产生则会继续写入到库中,但备份文件中并未有新的数据,此时会造成数据的不一致性,如果需要回滚则新数据丢失。当然咯,现在可能不会采取这种方式进行备份了。

beifencaozuosuo

如上图的步骤:

  • 使用 flush tables with read lock;进行加锁。
  • 使用 mysqldump -uroot -pxxxx test>test.sql进行备份。
  • 此时DML、DDL语言都加锁,但DQL可以操作。也就是其他的客户端只能读取不能更新数据库数据。
  • test.sql文件备份成功后,使用 unlock tables;进行解锁。

下面真实的步骤:

mysql> use milo_blog;
Database changed
mysql> flush tables with read lock;
Query OK, 0 rows affected (0.00 sec)
[root@VM-4-9-centos ~]# mysqldump -h127.0.0.1 -uroot -pxxxxx milo_blog > /www/wwwroot/backup/202309272243/milo.blog.sql
mysql> unlock tables;
Query OK, 0 rows affected (0.01 sec)

特点

数据库中加全局锁,是一个比较重的操作,存在以下问题:

  • 如果在主库上备份,那么在备份期间都不能执行更新,业务基本就得停摆。
  • 如果在从库备份,那么在备份期间从库不能执行主库同步过来的二进制日志(binlog),导致主从延迟。

当然在INNODB引擎中,我们可以备份时加上参数 --single-transaction 参数来完成不加锁的一致性备份。
mysqldump --single-transaction -h127.0.0.1 -uroot -pxxxxx milo_blog > /www/wwwroot/backup/202309272243/milo.blog.sql
实际上是在INNODB引擎的底层,通过快照读来实现的。后续会详细说,已经列入学习计划了。

表级锁

顾名思义,每次操作锁住整张表,锁定粒度大,发生锁冲突的概率最高,并发度最低,MyIsam、INNODB等存储引擎都支持。
对于表级锁,分为三类:

  • 表锁,分为表共享读锁(read lock),一个是表独占写锁(write lock)。
  • 元数据锁
  • 意向锁

表共享锁(读锁)

语法为:

  • 加 锁 : lock tables 表名... read
  • 释放锁 : unlock tables或者是客户端断开链接自动释放。

如下图:
客户端1进行加锁后,DQL可以操作,但是DDL/DML不能操作,且client2也是只能操作DQL,但是不能操作DDL/DML,只能通过client1 使用解锁后才算完成。

biaojijiasuoliucheng

直接上操作如下:
左侧开启读锁后,可以读操作,不能更新操作。右侧客户端也可以进行读操作,但是更新操作的时候卡住了,锁到什么时候呢?直到左侧解锁后才更新。

readsuodetedian_01

下图可知:右侧更新name为c,但是左侧为锁定状态,直到左侧解锁后,右侧更新成功,此时name已经修改为c。

readsuodingdian_02

表共享锁(写锁)

语法为:

  • 加 锁 : lock tables 表名... write
  • 释放锁 : unlock tables或者是客户端断开链接自动释放。 如下图:
    客户端1进行加锁后,客户端1可以进行DQL也可以进行DDL/DML,但是客户端2不能进行DQL和DDL以及DML,直到客户端1进行释放

biaoxiesuoliucheng

也直接上操作:

writesuodecaozuo01

  • 左侧客户端执行了写锁,左侧可以查和更新操作。
  • 右侧客户端不能读操作,使用ctrl+c后,再进行update操作,依旧卡住锁死状态。
  • 左侧客户端释放锁。
  • 右侧客户端立马写入成功。

表锁-元数据锁(meta data lock,MDL)

MDL加锁过程是系统自动控制的,无需显式使用,在访问一张表的时候会自动加上。MDL锁主要作用是维护元数据的数据一致性,在表上有活动事务的时候,不可以对元数据进行写入操作,这是为了避免DML和DDL冲突,保证读写的正确性
mysql5.5后引入了MDL,当对一张表进行增删改查的时候,加MDL读锁,当对表结构进行变更操作的时候,加MDL写锁。
也就是说,client1 开启一个事务开启后,还未提交,此时在client2中去修改当前表结构,会触发元数据锁,卡主等待。需要等到client1事务提交后,client2的修改才可以继续。

表锁-意向锁

先来一个现象:客户端client1开启一个事务,对根据id对某行数据进行update操作,会默认把对应id加锁(行锁)。此时另外一个客户端client2,使用表锁,读、写锁来锁这张表。那这个行锁和表锁已经冲突了。之前的机制,client2在上表锁的时候,会判定当前表中是否有其他行锁,从第一行记录开始搜索检查,一直到最后,当然这个性能比较低了。

在现在的INNODB当中,为了解决这个性能问题,避免DML在执行时候,加到行锁和表锁冲突,引用了意向锁,使得表锁不用检查每行数据是否加锁,而是使用意向锁来减少表锁的检查。
步骤如下:

  • client1在开启事务针对某一行进行update的时候,此时会默认自动加上意向锁。
  • client2针对于当前表进行表锁的时候,首先会检查当前表是否有意向锁,如果有而且意向表类型与表锁冲突,则卡住等待。
  • 等到client1提交事务后,意向锁默认释放。client2获得表锁。

意向锁以及兼容分为下面两种:

  • 1:意向共享锁(IS):由语句select ... lock in share mode添加。它与表锁共享锁(读锁)兼容,与表锁排他锁(写锁)互斥。
  • 2:意向排他锁(IX):由insert、update、delete、select ... from update 添加。它与表锁共享锁(读锁)以及排他锁(写锁)都互斥。
  • 3:意向锁之间不会互斥。

行锁

行级锁,每次操作锁住对应的行数据。锁定粒度最小,发生锁冲突的概率最低,并发度最高。应用在INNODB引擎中。

INNODB的数据是基于索引组织的,行锁是通过对索引上的索引项(一般是聚集索引)加锁来实现的,而不是对记录加的锁。对于行级锁,主要分为:

  • 行锁(record lock):锁定单个行记录的锁,防止其他事务对此行进行update和delete。在Read committed和Repeatable Read隔离级别下都支持。

    如下图,锁住了34这行记录,在聚集索引上。 hangsuoyanshitu

  • 间隙锁(gap lock):锁定索引记录间隙(不含该记录),确保索引记录间隙不变,防止其他事务在这个间隙进行insert,产生幻读。在Repeatable Read隔离级别下都支持。

    如下图,锁住了6-12、12-16、16-18、18-29、29-34之间的间隙,但是不包括他当前的记录,防止在这个中间插入数据。 hangsuojianxisuotu

  • 临键锁(next-key lock):行锁和间隙锁的组合,同时锁住数据,并锁住数据前面的间隙Gap,在Repeatable Read隔离级别下支持。

    如下图,除了间隙锁之外,还把34这条记录锁住了,可以理解为两个行锁和间隙锁的组合。 linjiansuojibietu

行锁

INNODB实现了以下两种类型的行锁:

  • 共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排它锁。
  • 排他锁(X):允许获取排他锁的事务更新数据,阻止其他事务获得相同数据集的共享锁和排他锁。
SQL 行锁类型 说明
insert ... 排他锁 自动加锁
update ... 排他锁 自动加锁
delete ... 排他锁 自动加锁
select ... 不加任何锁
select ...lock in share mode 共享锁 需要手动在select之后加lock in share mode
select ... for update 排他锁 需要手动在select之后加for update

在默认情况下,INNODB在repeatable read事务隔离级别下运行,INNODB使用 next-key锁进行搜索和索引扫描,以防止幻读。

  • 针对唯一索引进行检索时,对已存在的记录进行等值匹配时,将自动优化为行锁。
  • INNODB的行锁是针对索引加的锁,不通过索引条件检索数据,那么INNODB将对表中的所有记录加锁,此时就会升级为表锁。
演示查询加lock inshare mode

如下图: gongxiangpaitasuo

  • 左侧客户端在查询的时候,加了共享锁。
  • 右侧客户端可以查询,但是在更新该条数据的时候被锁住了。需要等左侧事务提交后才可以修改成功,但是可以更新其他非id为1的数据。
演示更新时行锁

hangsuogengxinshihsuo.png

  • 左侧开启事务,针对id为1的进行更新操作。事务还未提交。
  • 右侧也开启事务,同样针对id为1的进行更新操作,此时id为1的这行被锁定。需要等到左侧事务提交后,排他锁释放才可以修改。
  • 右侧可以更新非id为1的行。
演示不通过索引条件检索数据,升级为表锁

bujiasuoshengjibiaosuo.png

  • phone字段是没有索引的。左侧客户端开启事务,根据where条件为phone的去更新数据。并未提交事务。
  • 右侧客户端开启事务,此时select是可以的,但是一旦更新tb_user表,则触发了表锁。

如果phone有索引的话,则只会触发行锁(当修改同一行数据时,会触发更新时行锁。不同行的数据则不会加锁)。

间隙锁(临键锁)

默认情况下,INNODB在repeatable read事务隔离级别下运行,INNODB使用next-key锁进行搜索和索引扫描,以防止幻读。

  • 1:索引上的等值查询(唯一索引),给不存在的记录加锁时,优化为间隙锁。

    如下图,左侧更新了一个不存在的id为5,事务未提交的情况下,右侧插入一条id为4的,已经插入不成功的。因为左侧已经把[4,6]区间的间隙都锁住了。当然,如果插入id为9的,时可以插入成功。间隙锁是对两条记录的间隙加锁。

    charuweitijiaojianxisuo

  • 2:索引上的等值查询(普通索引),向右遍历时最后一个值不满足查询需求时,next-key lock退化为间隙锁。

  • 3:索引上的范围查询(唯一索引)--会访问到不满足条件的第一个值为止。

    如下图:id为唯一索引,左侧查询了<7的数据,手动加锁。右侧插入小于7的数据,是被间隙锁锁住的,直到左侧提交事务。但是右侧可以插入>7的第一条数据,比如id为9的

    weiyisuoyingjiasuo.png

间隙锁唯一目的,就是防止其他事务插入间隙。间隙锁可以共存,一个事务采用的间隙锁不会阻止另外一个事务在同一个间隙上采用间隙锁

INNODB引擎

作为mysql默认的存储引擎,很有必要去深入了解下。其他的引擎之前也强调了,已经被其他的存储方式可以替代的,在实际工作中使用的较少,就不介绍了,感兴趣的可以自行Google。

逻辑存储结构

cunchujiegoutubiao.png

上图可以看出,结构包含了五个部分:

  • 表空间:tablespace,这个是最外层的结构。表空间包含了很多个段。

    ibd文件,所有的索引、数据都存放在表空间中,

  • 段:segment,包含了很多个区(extent)

    分为数据段、索引段、回滚段,INNODB是索引组织表,数据段就是B+树的叶子节点,索引段就是B+树的非叶子节点,段用来管理多个区。

  • 区:extent,区中间包含了很多个页。

    表空间的单元结构,每个区的大小为1M。默认情况下,INNODB存储引擎页大小为16k,也就是每一个区中有64个连续的页。

  • 页:page,页中间包含了很多个行。

    是INNODB存储引擎磁盘管理的最小单元,每个页的大小默认为16K,为了保证页的连续性,INNODB存储引擎每次从磁盘申请 4-5个区。我们表结构存储的记录和索引,都是在页当中存储的。

  • 行:row,行当中,包含了事务的id、回滚指针、列等等。

    INNODB存储引擎数据是按行进行存放的。trx_id和roll_pointer是隐藏字段。
    trx_id:每次对某条记录进行改动时,都会把对应的事务id赋值给trx_id隐藏列。
    roll_pointer:每次对某条引起记录改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列相当于一个指针,可以通过它来找到该记录修改前的信息。

架构

mysql5.5开始,默认使用INNODB存储引擎,它擅长事务处理,具有崩溃恢复特性,在日常开发的过程中使用的非常广泛。
以下是INNODB架构图,左侧是buffer缓冲区内存结构,右侧为磁盘结构,有一些表空间、redo log等等。该图乍一看很复杂,其实一点也不简单。

jiagoutuxiangxie

INNODB缓冲区内存结构

如果有一台专门的sql服务器,几乎80%的内存会分配给缓冲区来提高mysql的执行效率。

  • buffer pool:缓冲池是主内存中的一个区域,里面可以缓存磁盘上经常操作的真实数据。

    在执行增删改查操作的时候,先操作缓冲池的数据,若缓冲池没有数据,则从磁盘加载并缓存,然后再以一定频率刷新到磁盘,从而减少磁盘IO,加快处理速度。假设没有缓冲区,数据直接从磁盘拿,且大部分的数据都是随机的,这样会频繁IO,有了缓冲区,就可以直接来缓冲区拿,速度也快了很多。
    缓冲次以page页为单位,底层采用链表数据结构管理page,根据状态,将page分为三种类型,好比上图的小方块颜色不同,就是状态不一样。

    • free page:空闲page,数据没有被修改过。
    • clean page:被使用page,数据没有被修改过。
    • dirty page:脏页,被使用page,数据被修改过,页中数据与磁盘的数据产生了不一致。
  • change buffer:更改缓冲区,针对非唯一的二级索引页。

    在执行DML语句时,如果这些数据page没有在buffer pool中,不会直接操作磁盘,而会将数据更存在缓冲区 change buffer中,在未来数据被读取时,再将数据合并恢复到buffer pool中,再合并后的数据刷新到磁盘中

  • adaptive hash index:自适应hash索引,用于优化对buffer pool数据的查询。

    INNODB存储引擎会监控对表上各索引页的查询,如果观察hash索引可以提升速度,则建立hash索引,称之为自适应hash索引。
    自适应hash索引,无需人工干预,是系统根据情况自动完成。

  • log buffer:日志缓冲区,用来保存要写入磁盘中的log日志数据(redo log,undo log),默认大小为16MB,日志缓冲区的日志会定时刷新到磁盘中。

    如果需要更新、插入、删除许多行的事务,增加日志缓冲区的大小可以节省磁盘I/O。
    参数:innodb_log_buffer_size:缓冲区大小。innodb_flush_log_at_trx_commit:日志刷新到磁盘时机。

磁盘结构

  • system tablespace:系统表空间是更改缓冲区的存储区域。

    如果表式在系统表空间而不是每个表文件或通用表空间中创建,它可能包含表和索引数据,在mysql5.X版本还包含INNODB数据字典、undolog等等。

  • file-per-table tablespace:每个表的文件表空间包含单个INNODB表的数据和索引,并存储在文件系统上的单个数据文件中。

    如果开启则说明我们每张表都有一个独立的表空间,存放了表结构、数据和索引,不会在系统表空间中存放。可以通过 innodb_file_per_table查看是否开启。

    mysql> show variables like '%innodb_file_per_table%';
    +-----------------------+-------+
    | Variable_name         | Value |
    +-----------------------+-------+
    | innodb_file_per_table | ON    |
    +-----------------------+-------+
    1 row in set (0.01 sec)
    
  • general tablespaces:通用表空间,需要通过create tablespace语法创建通用表空间,在创建表的时候,可以指定该表空间。

    
    mysql> create tablespace tb_test_space add datafile 'test_space.ibd' engine=innodb; #创建表空间
    Query OK, 0 rows affected (0.02 sec)
    
    mysql> create table test_space(id int primary key ,name varchar(10)) engine=innodb tablespace tb_test_space 
        -> ; #创建表并且指定表空间。
    Query OK, 0 rows affected (0.03 sec)
    
  • undo tablespaces:撤销表空间,mysql实例中在初始化时会自动创建两个默认的undo表空间,初始化大小为16M,用户存储undo log 日志。

  • temporary tablespaces:INNODB使用会话临时表空间和全局临时表空间,存储用户创建的临时表等数据。

  • doublewrite buffer files:双写缓冲区,INNODB引擎将数据页从buffer pool刷新到磁盘前,先将数据页写入双写缓冲区文件中,便于系统异常时恢复数据。

  • redo log:重做日志,用来实现事务的持久性。

    该日志文件由两部分组成,重做日志缓冲(redo log buffer)以及重做日志文件(redo log file),前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都会存到该日志中,用于在刷新脏页到磁盘时、发生错误时等等,进行数据恢复使用。 redo log不会永久保存,他会每隔一段时间去清理没有用的redo log,因为当事务提交之后,它存在的意义就不大了,它的作用主要是异常时候的数据回滚。

后台线程

缓冲区域说完了,磁盘的也说完了。但是这两边目前毫无关联呀,如何把缓冲区域的写到磁盘区域呢?这就涉及到后台线程。

后台线程它的作用就是在INNODB引擎当中,在合适的时候将缓冲区域的数据刷新到磁盘区域当中。

  • 1:master thread:核心后台线程,负责调度其他线程,还负责将缓冲池中的数据异步刷新到新的磁盘中,保持数据的一致性,还包括脏页的刷新,合并插入缓冲和undo页的回收。
  • 2:IO thread:在INNODB存储引擎中大量使用了IO线程来处理IO请求,这样可以极大地提高数据库的性能,而IO Thread主要负责这些IO请求的回调。
线程类型 默认个数 职责
read thread 4 负责读操作
write thread 4 负责写操作
log thread 1 负责将日志缓冲区刷新到磁盘
insert buffer thread 1 负责将缓冲区内存刷新到磁盘
  • 3:purge thread:主要用户回收事务已经提交的undo log,在事务提交之后,undo log可能就用不了了,会用它来回收。

  • 4:page cleaner thread:协助master thread刷新脏页到磁盘的线程,它可以减轻master thread的工作压力,减少阻塞。

事务原理

事务是一组操作的集合,它是一个不可分割的工作单位,事务把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。

特征

  • 原子性(Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败。
  • 一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。
  • 隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。
  • 持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。

实际上,可以分为两类,一类是有INNODB引擎底层的log(redo log/undo log)所保证的原子性、一致性和持久性,一类是有锁和MVCC所保证的隔离性。

acidshiyitu

持久性保证-redo log

重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性。
该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log file),前者是在内存中,后者是在磁盘中。当事务提交之后会把所有修改信息都存在该日志文件中,用于在刷新脏页到磁盘,发生错误时,进行数据恢复使用。
有点模糊是吧,不要急,举个例子。

下图,是一个事务里面包含了两个update和一个delete操作,首先会在buffer池子里面去寻找,是否有该数据,如果没有则从磁盘文件中找到数据后再缓冲到内存结构中的buffer pool。然后再在内存结构中做所有的操作,等到完成后,缓冲区的数据发生了变更(深黄色的为脏页),再通过线程更新到磁盘结构中,这样缓冲中的数据和磁盘的数据就保持了一致。
但这样有个问题,事务提交了,脏页的数据在刷新到磁盘的时候失败了,此时持久性就没有得到保障。

chijiuxingredolog

此时,我们的redo log就出现了。
如下图:我们在操作缓存的时候,也把数据页的变换数据,顺序的写入磁盘结构中(WAL先写日志),当从脏页刷新到磁盘结构中出现了问题,则可以从redolog中进行数据的回滚操作,所以这个redolog只是用于脏页到磁盘出现问题时候进行数据恢复
当然,redolog不是持久存储的,也会定时清除,所以会有两个文件循环使用,且不会永久保持。

redologojiaruzhihou

原子性保证- undo log

回滚日志,用于记录数据被修改前的信息,作用包含两个:提供回滚和MVCC(多版本并发控制). undo log 和redo log记录物理日志不一样,它是逻辑日志。可以认为当delete一条记录时,undo log会记录一条对应的insert记录,反之,当update一条记录时,它记录一条对应相反的update记录。当执行rollback时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚。

  • undo log销毁: undo log在事务执行时产生,事务提交时,并不会立即删除undo log,因为这些日志可能还用于MVCC。
  • undo log存储:undo log采用段的方式进行管理和记录,存放在前面介绍的rollback segment回滚段中,内部包含了1024个undo log segment。

MVCC

基本概念

  • 当前读:读取的记录是最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。

    对于我们日常的操作,如 select ... lock in share mode(共享锁),select ... for update、update、insert、delete(排他锁)都是一种当前读。

  • 快照读:简单的select不加锁就是快照读,读取的是记录数据的可见版本,有可能是历史数据,不加锁,是非阻塞读。

    • read committed:每次select,都生成一个快照读。
    • repeatable read:开启事务后第一个select语句才是快照读的地方。
    • serializable:快照读会退化为当前读。
  • MVCC:全程multi-version concurrency control,多版本并发控制。

    指维护一个数据的多个版本,使得读写操作没有冲突,快照读为mysql实现MVCC提供了一个非阻塞读功能,MVCC的具体实现,还需要依赖于数据库记录中的三个隐式字段、undo log、readview。

三个隐式字段

比如表中的字段为:id,age,name。我们显式的可以看到这三个字段,但是在INNODB引擎中,还会隐藏帮我们创建三个字段,DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID。

隐藏字段 含义
DB_TRX_ID 最新修改事务ID,记录插入这条记录或最后一次修改该记录的事务ID
DB_ROLL_PTR 回滚指针,指向这条记录的上一个版本,用于配合undo log,指向上一个版本,自增的
DB_ROW_ID 隐藏主键,如果表结构没有指定主键,将会生成该隐藏字段

undo log 版本链

不同事务或相同事务对同一条记录进行修改,会导致该条记录的undolog生成一条记录版本链表,链表的头部是最新的旧记录,链表的尾部是最早的旧记录。

undologdebanbenlian

如上图,事务2,3,4,5四个事务同时开启,原始的数据为:id=30,age=30,name=A30.

  • 事务2,把id为30的age修改为3,则会在undo log中记录一条日志,地址为0x0001,age为原先的值20,修改事务id为1,回滚指针为null,因为是第一次修改。
  • 事务3,把id为30的name改为A3,则会在undo log中记录一条日志,地址为0X0002,age为事务2中修改后的值3,name为原记录的值A30,回滚指针为上一个0x0001。
  • 事务4,把id为30的age改为10,则会在undo log中记录一条日志,地址为0X003,age为原先的3,回滚指针为上一个0x0002.

这就是undo log记录版本的链表,链表的头部是最新的记录,尾部为最旧的记录。至于查询的是查询哪个呢?就涉及到下面的,readreview了。

读视图-readview

readview是快照读SQL执行MVCC提取数据的依据,记录并维护系统当前活跃的事务(未提交的)id。readreview中包含了四个核心字段:

字段 含义
m_ids 当前活跃的事务ID集合
min_trx_id 最小活跃事务ID
max_trx_id 预分配事务ID,当前最大事务ID+1,因为事务ID是自增的
creator_trx_id readView创建者的事务ID

trx_id为当前的事务id,根据上述版本链中的查询操作,先了解下版本数据访问规则:

  • trx_id = creator_trx_id,表示可以访问该版本,因为这就表示数据是当前这个事务更改的。
  • trx_id < min_trx_id,可以访问该版本,因为说明数据已经提交了。
  • trx_id > max_trx_id,表示不可以访问该版本,因为说明该事务在readview生成后才开启的。
  • min_trx_id<=trx_id<=max_trx_id,如果trx_id不在m_ids是可以访问该版本的,因为说明数据已经提交了。

不同的隔离级别,生成的readview的时机不同:

read committed:在事务中每一次执行快照读时生成readview。
repeatable read:仅在事务中第一次执行快照读时生成readview,后续复用该readview。

有点懵是吧,来个例子,我们上图还有一个事务5没有解释,因为全是查询的,下面就以事务5在read committed下为例:

事务五中有两个查询:

  • 生成的readview的情况:

readviewshilitu

 - 第一个查询在快照读的时候生成了readreview,由于事务2已经提交了,所以活跃的事务id(m_ids)为3,4,5。最小的(min_trx_id)就是3,最大(max_trx_id)的为预分配事务id,是最大的事务ID+1所以为6。创建者的事务ID(creator_trx_id)是当前的事务id为5。
 - 第二个查询也是在快照读的时候生成了readreview,此时事务3已经提交了,所以m_ids为4,5。 min_trx_id为4,max_trx_id为6,creator_trx_id也是5。
  • 第一个查询提取的版本。

    mvccdiyige

    我们带着事务5,trx_id=4 的第一个查询为30的记录套进去。

    条件1:4!=5,不成立。
    条件2:4>3,不成立。
    条件3:4<5,不成立。
    条件4:3<=4<=6,成立,但是,trx_id在m_ids之中,所以也不成立
    

    说明,当前最新的记录版本不满足,则看下一个DB_ROLL_PTR=0x0003的版本。 则trx_id=3了。

    条件1:3!=5,不成立。
    条件2:3=3,不成立。
    条件3:3<5,不成立。
    条件4:3<=3<=6,成立,但是,trx_id在m_ids之中,所以也不成立
    

    说明,当前最新的记录版本不满足,则看下一个DB_ROLL_PTR=0x0002的版本。 则trx_id=2了。

      条件1:2!=5,不成立。
      条件2:2<3,成立。表示数据已经提交了。则把这条记录返回
    

    也就是age=3,name=A30返回

  • 第二个查询提取的版本。

    mvccdiergejilu

    我们带着事务5,trx_id=4 的第二个查询为30的记录套进去。

    条件1:4!=5,不成立。
    条件2:4>3,不成立。
    条件3:4<5,不成立。
    条件4:3<=4<=6,成立,但是,trx_id在m_ids之中,所以也不成立
    

    说明,当前最新的记录版本不满足,则看下一个DB_ROLL_PTR=0x0003的版本。 则trx_id=3了。

    条件1:3!=5,不成立。
    条件2:3<4,成立。表示数据已经提交了。则把这条记录返回
    

    也就是age=3,name=A3返回

以上是在读已提交的事务隔离级别下,在可重复度的隔离级别下会更加简单,因为第二个查询不会生成快照了,会直接复用上一个查询生成的快照,如下图: kechongfuduqulizi

具体的套用规则和读已提交时一样的
这就是MVCC,它的作用主要是在快照读的时候,来决定我们提取的是哪一个记录的版本

再回顾下,事务的四个特性继续回顾下为:

shiwuzongjietu

人未眠
工作数十年
脚步未曾歇,学习未曾停
乍回首
路程虽丰富,知识未记录
   借此博客,与之共进步