《MySQL 是怎样运行的》学习笔记

747 阅读37分钟

juejin.cn/book/684473…

www.processon.com/view/link/5…

初识MySQL

服务器处理客户端请求

字符集

  • utf8字符集一个字符需要使用1~4个字节

  • 字符集的设置会影响char,varchar占用空间大小。char(10)指的是字符长度为10,而不是10字节

行数据结构

  • 页:磁盘和内存之间交互的基本单位,InnoDB中页的大小一般为16KB

行结构

  • Compact

是InnoDB的默认格式image_1c9g4t114n0j1gkro2r1h8h1d1t16.png-42.4kBimage_1c9h256f9nke14311adhtu61ie2dn.png-92kB

  • 记录meta信息

为什么变长字段和null要逆序?

  • 变长字段长度列表

1. VARCHAR、TEXT等称为变长字段

2. 所有变长字段占用的字节数按照列的顺序逆序存放

3. 可变字段允许存储的最大字节数超过255并且真实存储的字节数超过127,则使用2个字节记录字段长度,否则使用1个字节

4. 变长字段长度列表中只存储值为非NULL 的列内容占用的长度

  • NULL值列表

1. 将允许存储NULL的列对应一个二进制位图,按照列的顺序逆序排列;  二进制位的值为1时,代表该列的值为NULL,二进制位的值为0时,代表该列的值不为NULL

2. NULL值列表必须用整数个字节的位表示, 如果不是整数个字节,则在字节的高位补0

3.值为NULL的列,在记录的真实数据处就不再冗余存储,从而节省存储空间

4.表中所有的列都不是变长的数据类型的话,就没有变长字段长度列表

image_1c9g8g27b1bdlu7t187emsc46s61.png-19.4kB

  • 记录头信息

**image_1c9geiglj1ah31meo80ci8n1eli8f.png-29.5kB
**

 固定的5个字节组成,会在页结构章节详细讲述

  • 记录的真实数据

  • 隐藏列

  • 记录的真实数据要注意的点

  • CHAR(M)列的存储格式

1. 当列采用的是定长字符集时,该列占用的字节数不会被加到变长字段长度列表,而如果采用变长字符集时,该列占用的字节数也会被加到变长字段长度列表

2. char是固定长度的,当内容未填满时,会在其右边填充空格 

3. char固定长度,没有碎片的困扰;varchar会有

  • Redundant

MySQL5.0之前用的一种行格式,兼容旧数据

image_1ctfppb4c1cng1m8718l91760jde9.png-36.2kB

行溢出

  • Mysql行大小规范
  1. MySQL规定除了BLOB或者TEXT类型的列,其他所有的列(不包括隐藏列和记录头信息)占用的字节长度不能超过65535个字节。所以,VARCHAR列最多可以存储65532个字节

2. MySQL规定一个页中至少存放两行记录。一行65535个字节or VARCHAR列65532个字节,造成一个页存放不了一条记录。只会存储该列的前768个字节的数据和一个指向其他页的地址,然后把剩下的数据存放到其他页(溢出页)中,这个过程也叫做行溢出

image_1d48e3imu1vcp5rsh8cg0b1o169.png-149kB

3. 触发行溢出条件:132 + 2×(27 + n) < 16384(132:每个页的header空间;27:每行记录的header空间;16384:一页16KB)

页数据结构 

页的类型

许多种不同类型的页,比如存放Insert Buffer信息的页,存放undo日志信息的页等等。

用户的数据存放在索引(INDEX)页中,也就是所谓的数据页

数据页的结构

记录在页中的存储

  • User Records和Free Space

1. 用户数据行记录就在User Records中

2.每插入一条记录都会从Free Space部分申请一个记录大小的空间划分到User Records,Free Space空间不足会申请新的页

image_1cosvi1in9st476cdqfki1n39m.png-133.8kB

  • 行记录的记录头

image_1c9o2eib2vl11qnf1dfl1d2lco313.png-76.4kB

  • delete_mask

1. 被删除的记录不会立即从磁盘上移除(软删除

2. 通过delete_mask进行标志,并组成垃圾链表。之后如果有新记录插入到表中的话,可能把这些被删除的记录占用的存储空间覆盖掉

3. delete_mask位设置为1和将被删除的记录加入到垃圾链表中其实是两个阶段,会在介绍事务的时候详细讲

  • heap_no

表示该记录在本页中的位置,其中自动在每页的最开头加入2条虚拟记录(最大,最小值) 并不存放在页的User Records部分

  • next_record

1.表示到下一条按照主键值由小到大的顺序的记录的地址偏移量

image_1cot1r96210ph1jng1td41ouj85c13.png-120.5kB

2. 记录按照主键从小到大的顺序形成了一个单链

3. 如果从中删除掉一条记录,这个链表也是会跟着变化的。如,删除第2条记录并没有从存储空间中移除,而是把该条记录的delete_mask值设置为1; 第2条记录的next_record值变为了0; 第1条记录的next_record指向了第3条记录;最大记录的n_owned值减一

4. 为啥next_record要指向记录头信息和真实数据之间的位置呢? 因为这个位置向左读取就是记录头信息,向右读取就是真实数据。变长字段长度列表、NULL值列表中的信息都是逆序存放,这样可以使记录中位置靠前的字段和它们对应的字段长度信息在内存中的距离更近,可能会提高高速缓存的命中率

Page Directory(页目录)

在页中按照主键值由小到大顺序串联成一个单链表,在此基础上如何提高查找速度?

image_1d6g64af2sgj1816ktl1q22dehp.png-189.1kB

  • 页面目录:槽(Slot)

1.将所有行记录(包括最大和最小记录,不包括标记为已删除的记录)划分为几个**组。**分组中记录的条数范围只能在是 4~8 条之间

2.每组最后一条记录头信息中的地址偏移量(next_record)单独提取出来存储到

3. 每组最后一条记录头信息中的n_owned属性表示该组内共有几条记录

4. 随着记录的插入或删除,组和槽是会增加和删除的

  • 数据页中****查找指定主键值的记录的过程

分为两步:

1. 通过二分法确定该记录所在的槽,并找到该槽中主键值最小的那条记录

2. 通过记录的next_record属性遍历该槽所在的组中的各个记录

Page Header

针对数据页记录的各种状态信息

  • 目录中的槽数量

  • free space

  • 记录的数量

  • 删除链表

等等

File Header

针对各种类型的页都通用描述了一些针对各种页都通用的一些信息(有别于page header)

如页的编号是多少,它的上一个页、下一个页等等。占用固定的38个字节

  • FIL_PAGE_OFFSET

每一个页都有一个单独的页号,InnoDB通过页号来可以唯一定位一个页

  • FIL_PAGE_TYPE

InnoDB为了不同的目的而把页分为不同的类型

存放记录的数据页的类型其实是FIL_PAGE_INDEX,也就是所谓的索引页。还有很多别的类型的页

  • FIL_PAGE_PREV和FIL_PAGE_NEXT

**image_1ca00fhg418pl1f1a1iav1uo3aou9.png-90.9kB
**

通过建立一个双向链表把页就都串联起来了,而无需这些页在物理上真正连着

File Trailer

校验信息

File Trailer与File Header类似,都是所有类型的页通用的

B+树索引

已知所有的页是通过双向链表连起来的,怎么快速定位到要找的页

InnoDB索引

  • 目录项记录

目录项记录和普通的用户记录的不同点:

1.record type

2.用的是一样的数据页类型

聚簇索引、二级索引、联合索引注意事项

  • 根页面不动

B+树的形成过程:

为表创建B+树聚簇索引时,先创建一个根节点页面。此时没有数据页,只有根节点页面

向表中插入用户记录,先把用户记录存储到这个根节点中 

当根节点中的可用空间用完时继续插入记录,此时会将根节点中的所有记录复制到一个新分配的页,比如页a中,然后对这个新页进行页分裂的操作,得到另一个新页,比如页b。这时新插入的记录根据键值的大小就会被分配到页a或者页b中,而根节点便升级为存储目录项记录的页

特别注意的是:一个B+树索引的根节点自诞生之日起,便不会再移动。这样只要我们对某个表建立一个索引,那么它的根节点的页号便会被记录到某个地方,然后凡是InnoDB存储引擎需要用到这个索引的时候,都会从那个固定的地方取出根节点的页号

  • 内节点中目录项记录的唯一性

B+树索引的内节点中目录项记录的内容是索引列 + 页号的搭配,但是这个搭配对于二级索引来说有点儿不严谨

为了让新插入记录能找到自己在那个页里,我们需要保证在B+树的同一层内节点的目录项记录除页号这个字段以外是唯一的。所以对于二级索引的内节点的目录项记录的内容实际上是由三个部分构成的: 索引列的值 主键值 页号

  • 一个页面最少存储2条记录

依据这个结论推导了表中只有一个列时该列在不发生行溢出的情况下最多能存储多少字节

索引使用

  1. B+树索引在空间和时间上都有代价,所以没事儿别瞎建索引。

  2. B+树索引适用于下边这些情况:

    • 全值匹配
    • 匹配左边的列
    • 匹配范围值
    • 精确匹配某一列并范围匹配另外一列
    • 用于排序
    • 用于分组
  3. 在使用索引时需要注意下边这些事项:

    • 只为用于搜索、排序或分组的列创建索引

    • 为列的基数大的列创建索引

    • 索引列的类型尽量小

    • 可以只对字符串值的前缀建立索引

    • 只有索引列在比较表达式中单独出现才可以适用索引

    • 为了尽可能少的让聚簇索引发生页面分裂和记录移位的情况,建议让主键拥有AUTO_INCREMENT属性。

    • 定位并删除表中的重复和冗余索引

    • 尽量使用覆盖索引进行查询,避免回表带来的性能损耗。

表空间

image_1d9ppsbelendcbb13hghhn18pe9.png-3564.2kB

前置回忆:

页类型

页 file header 和 file tracker:页号、页类型、前后页指针、页空间

区(extent)

对于16KB的页来说,连续的64个页就是一个区,一个区默认占用1MB空间大小

为什么引入区?如果以页为单位来分配存储空间的话,B+树双向链表相邻的两个页之间的物理位置可能离得非常远。范围查询时,需要定位到最左边的记录和最右边的记录,然后沿着双向链表一直扫描就可以了,而如果链表中相邻的两个页物理位置离得非常远,会产生随机I/O。应该尽量让链表中相邻的页的物理位置也相邻,进行范围查询的时候才可以使用顺序I/O。在表中数据量大的时候,为某个索引分配空间的时候就不再按照页为单位分配了,而是按照区为单位分配

区的组

每256个区划分为一,每个组的最开始的几个页面类型是固定的,存放一些head信息

image_1cri1nutcorp5ghf5c7vqagt1j.png-71.4kB

段(segment)

为保证一个extent下所有页都是一类,引入段。把叶子节点的页和非叶子节点的页分别存储在不同的区中,存放叶子节点的区的集合就算是一个段,存放非叶子节点的区的集合也算是一个段

段其实不对应表空间中某一个连续的物理区域,而是一个逻辑上的概念

一个索引会生成2个段,而段是以区为单位申请存储空间的,一个区默认占用1M存储空间

碎片(fragment)区的概念:碎片区中的页可以用于不同的目的。防止一个只存了几条记录的小表也需要2M的存储空间。段分配存储空间的策略是这样的: 在刚开始向表中插入数据的时候,段是从某个碎片区以单个页面为单位来分配存储空间的。 当某个段已经占用了32个碎片区页面之后,就会以完整的区为单位来分配存储空间

XDES Entry

开始的2个页面的类型是固定的,其中一个就是XDES,用来登记本组256个区的属性

每一个区都对应着一个XDES Entry结构,这个结构记录了对应的区的一些属性

image_1crre79uq9971bsdj9s1i0j11en8a.png-96.2kB

  • Segment ID:每一个段都有一个唯一的编号,Segment ID字段表示就是该区所在的段。当然前提是该区已经被分配给某个段

  • List Node:将若干个XDES Entry结构串联成一个链表。想定位表空间内的某一个位置的话,只需指定页号以及该位置在指定页号中的页内偏移量即可image_1crre8tlh1vmqtfipk663l173q97.png-69.1kB

  • State:区类型FREE、FREE_FRAG、FULL_FRAG和FSEG

  • Page State Bitmap:表示区内(64个页)是否是空闲的bitmap

XDES Entry链表

向某个段中插入数据的过程

1. 当段中数据较少的时候,首先会查看表空间中是否有状态为FREE_FRAG的区,也就是找还有空闲空间的碎片区,如果找到了,那么从该区中取一些零散的页把数据插进去;否则到表空间下申请一个状态为FREE的区,也就是空闲的区,把该区的状态变为FREE_FRAG,然后从该新申请的区中取一些零散的页把数据插进去。之后不同的段使用零散页的时候都会从该区中取,直到该区中没有空闲空间,然后该区的状态就变成了FULL_FRAG

怎么知道表空间里的哪些区是FREE的,哪些区的状态是FREE_FRAG的,哪些区是FULL_FRAG的?直属于表空间的区对应的XDES Entry结构可以分成FREE、FREE_FRAG和FULL_FRAG这3个链表

2.段中数据已经占满了32个零散的页后,就直接申请完整的区来插入数据了

怎么知道哪些区属于哪个段的呢

根据段号(也就是Segment ID)来建立链表,每个段中的区对应的XDES Entry结构建立了三个链表:FREE链表、NOT_FULL链表、FULL链表

INODE Entry

像每个区都有对应的XDES Entry来记录这个区中的属性一样,每个段都定义了一个INODE Entry结构来记录一下段中的属性

image_1crrju0cnji91a2fhv91ijb15hgb1.png-111.4kB

  • Segment ID

  • NOT_FULL_N_USED:NOT_FULL链表中已经使用了多少个页面

  • 3个List Base Node: 分别为段的FREE链表、NOT_FULL链表、FULL链表定义了List Base Node

  • Fragment Array Entry:段是一些零散页面和一些完整的区的集合,每个Fragment Array Entry结构都对应着一个零散的页面,这个结构一共4个字节,表示一个零散页面的页号

组开头的一些类型固定的页面

1. FSP_HDR类型

第一个组的第一个页面,也是表空间的第一个页面,页号为0

image_1crmfvigk938c8h1hahglr15329.png-146.8kB

FSP_HDR类型的页面大致由5个部分组成,重点来看看File Space Header和XDES Entry

File Space Header:

  • List Base Node for FREE List、List Base Node for FREE_FRAG List、List Base Node for FULL_FRAG List

分别代表:直属于表空间的FREE链表的基节点、FREE_FRAG链表的基节点、FULL_FRAG链表的基节点

这三个链表的基节点在表空间的位置是固定的,就是在表空间的第一个页面(FSP_HDR类型的页面)的File Space Header部分

  • Next Unused Segment ID

当前表空间中最大的段ID的下一个ID,这样在创建新段的时候赋予新段一个唯一的ID值

  • List Base Node for SEG_INODES_FULL List和List Base Node for SEG_INODES_FREE List

XDES Entry

把256个区划分成一组,在每组的第一个页面中存放256个XDES Entry结构

2. XDES类型

除去第一组以外,之后的每个分组的第一个页面只需要记录本组内所有的区对应的XDES Entry结构即可,不需要再记录表空间的属性(FSP_HDR类型)了

为了和FSP_HDR类型做区别,把之后每个分组的第一个页面的类型定义为XDES,结构和FSP_HDR类型是非常相似的。与FSP_HDR类型的页面对比,除了少了File Space Header部分之外,也就是除了少了记录表空间整体属性的部分之外,其余的部分是一样一样的

image_1cs3vmoii1h971aje1iveack1l109.png-149.5kB

3. IBUF_BITMAP类型

Change Buffer

4. INODE类型

为每个段设计了一个INODE Entry结构,这个结构中记录了关于这个段的相关属性。而INODE类型的页就是为了存储INODE Entry结构而存在的

重点关注List Node for INODE Page List和INODE Entry

数据字典

InnoDB存储引擎特意定义了一些列的内部系统表(internal system table)来记录这些这些元数据

SYS_TABLES、SYS_COLUMNS、SYS_INDEXES、SYS_FIELDS这四个表尤其重要,称之为基本系统表(basic system tables) 因为这些表是系统内部表,用户无法直接访问

www.processon.com/view/link/6…

Data Dictionary Header

一个固定的页面来记录SYS_TABLES、SYS_COLUMNS、SYS_INDEXES、SYS_FIELDS这四个表的聚簇索引和二级索引对应的B+树位置面,类型为SYS,记录了Data Dictionary Header,也就是数据字典的头部信息

Buffer Pool

  缓存磁盘中的页

内部组成

控制块和缓存,一一对应

image_1d15mh3d4oadq0e1qpme22u8i61.png-47.4kB

free链表

新增缓存页时,如何获取空闲页

  • 所有空闲的缓存页对应的控制块作为一个节点放到一个链表中,free链表

  • 链表的基节点占用的内存空间并不包含在Buffer Pool内存空间之内

  • 当需要从磁盘中加载一个页到Buffer Pool中时,就从free链表中取一个空闲的缓存页,并且把该缓存页对应的控制块的信息填上,然后把该缓存页对应的free链表节点从链表中移除

image_1d155te021bmgjt09mo1lln17dum.png-132.6kB

缓存页的哈希

怎么知道该页在不在Buffer Pool中

  • 用表空间号 + 页号作为key,缓存页作为value创建一个哈希表

flush链表

怎么知道Buffer Pool中哪些页是脏页

  • 凡是修改过的缓存页对应的控制块都会作为一个节点加入到一个链表中image_1d1589dpqmt5v1849s7614nu23.png-133.5kB

LRU链表

free链表中已经没有多余的空闲缓存页时,移除哪些缓存页呢

  • LRU基础,问题

  • 预读机制,劣币驱逐良币

  • 扫描全表,劣币驱逐良币

  • 把LRU链表按照一定比例分成两截,分别是热数据和冷数据

  • 针对预读:初次加载时,该缓存页对应的控制块会被放到old区域,不影响hot

  • 针对全表扫描:取完某个页面的记录就相当于访问了这个页面好多次,导致全表扫描的误进hot,怎么办?如果第一次和最后一次访问该页面的时间间隔小于某个时间,该页是不会被加入到young区域image_1d15fb53d2lf13ovglg1rnv1h2n2g.png-116.5kB

  • 只有被访问的缓存页位于young区域的1/4的后边,才会被移动到LRU链表头部,降低调整LRU链表的频率,从而提升性能

刷新脏页到磁盘

后台有专门的线程每隔一段时间负责把脏页刷新到磁盘

  • 两种刷新路径:

  • 从LRU链表的冷数据中刷新一部分页面

  • 从flush链表中刷新一部分页面到磁盘

多个bufferpool

分治

Redo log

为何要有redo log

  • 如果,事务完成时刷页

  • 随机IO刷起来比较慢

  • 刷新一个完整的数据页太浪费了

  • 刷 redo log

  • redo日志占用的空间非常小

  • 顺序IO

redo日志格式

image_1d36k7d3412oo1c0qcuuben12l79.png-31.3kB

表空间id;页号

  • redo日志中只需要记录一下在某个页面的某个偏移量处修改了几个字节的值,具体被修改的内容是啥

image_1d3fv01mv3jd7m719rpmn2jcsp.png-42.6kB

image_1d3bn8tsq1ssp1nmdks8kdr17e31t.png-85.7kB

Mini-Transaction

  • 语句在执行过程中可能修改若干个页面,在执行语句的过程中产生的redo日志被划分成了若干个不可分割的组

规定在执行这些需要保证原子性的操作时必须以的形式来记录的redo日志,在进行系统崩溃重启恢复时,针对某个组中的redo日志,要么把全部的日志都恢复掉,要么一条也不恢复

  • 对底层页面中的一次原子访问的过程称之为一个Mini-Transaction,简称mtr

  • 向log buffer中写入redo日志时不是一条一条写入的,而是以一个mtr生成的一组redo日志为单位进行写入的

  • image_1d4hgjr7t4es1v2mf2b1bt51rf95b.png-27.6kB

写入过程

  • redo log block

  • redo log buffer(写入redo日志时也不能直接直接写到磁盘上,实际上在服务器启动时就向操作系统申请了一大片称之为redo log buffer的连续内存空间)

image_1d4i4orkr17vl1m5l3hl1l341pad1j.png-76.5kB

redo日志刷盘时机

即log buffer刷盘时机

  • buffer空间不足时

  • 事务提交时

  • 将某个脏页刷新到磁盘前,会保证先将该脏页对应的 redo 日志刷新到磁盘中

  • 后台线程定时

  • checkpoint

redo日志文件格式

  • 磁盘上的redo日志文件不只一个,而是以一个日志文件组的形式出现的。从ib_logfile0开始写,如果ib_logfile0写满了,就接着ib_logfile1写,依此类推。如果写到最后一个文件该咋办?那就重新转到ib_logfile0继续写

  • (循环写入)+checkpoint机制

  • image_1d4mu4s6f7491l7l1jcc6pc1rbk16.png-49.7kB

  • 将log buffer中的redo日志刷新到磁盘的本质就是把block的镜像写入日志文件中,所以redo日志文件其实也是由若干个512字节大小的block组成

  • 前4个block,meta信息

  • image_1d4njgt351je21kitk7u1gbioa46j.png-64.9kB

日志序列号Log Sequence Number

  • 每一组由mtr生成的redo日志都有一个唯一的LSN值与其对应,LSN值越小,说明redo日志产生的越早
  • flushed_to_disk_lsn:标记当前log buffer中已经有哪些日志被刷新到磁盘中了
  • 在mtr结束时把在mtr执行过程中可能修改过的页面加入到Buffer Pool的flush链表。每修改一次页面,都会将修改该页面的mtr结束时对应的lsn值写入这个属性image_1d4v63pct1v9o14l3812gnj11de44.png-31.8kBimage_1d4v68bhl1jb9r8m6vn1b157cn5e.png-110.8kBflush链表中的脏页按照修改发生的时间顺序进行排序,也就是按照oldest_modification代表的LSN值进行排序,被多次更新的页面不会重复插入到flush链表中,但是会更新newest_modification属性的值

checkpoint

redo日志文件组容量是有限的,循环使用redo日志文件组中的文件

checkpoint_lsn来代表当前系统中可以被覆盖的redo日志总量是多少

  • 一次checkpoint

  • 页a被刷新到了磁盘,mtr_1生成的redo日志就可以被覆盖了,所以我们可以进行一个增加checkpoint_lsn的操作,我们把这个过程称之为做一次checkpoint

  • 步骤一:计算一下当前系统中可以被覆盖的redo日志对应的lsn值最大是多少

  • image_1d678eiie125j1flp1tc617jp1dvo9.png-68.1kB

innodb_flush_log_at_trx_commit

控制:事务提交时,是否需要将该事务执行过程中产生的所有redo日志都刷新到磁盘上

  • 当该系统变量值为2时,表示在事务提交时需要将redo日志写到操作系统的缓冲区中,但并不需要保证将日志真正的刷新到磁盘。 这种情况下如果数据库挂了,操作系统没挂的话,事务的持久性还是可以保证的,但是操作系统也挂了的话,那就不能保证持久性了

Undo log

事务id

某个事务执行过程中对某个表执行了增、删、改操作,那么InnoDB存储引擎就会给它分配一个独一无二的事务id

事务中全是查询语句,并没有执行增、删、改的语句,那也就意味着这个事务并不会被分配一个事务id

  • 事务id是怎么生成的

  • 事务id值是一个递增的数字

image_1d62h05ffsum114cn05koa1igbp.png-45.1kB

undo日志的格式

  • undo日志是被记录到类型为FIL_PAGE_UNDO_LOG的页面中

  • 一般每对一条记录做一次改动,就对应着一条undo日志,但在某些更新记录的操作中,也可能会对应着2条undo日志

  • roll_pointer

  • 一个指向记录对应的undo日志的一个指针

  • 不同的类型

  • insert操作

  • DELETE

  • delete mark操作前,需要把该记录的旧的trx_id和roll_pointer隐藏列的值都给记到对应的undo日志中来。版本链

  • UPDATE

undo日志会被具体写到什么地方

Undo页面结构

image_1d79ec33apm47brq901sur3bi16.png-63.2kB

  • page type:把undo日志分成两个大类,是因为类型为TRX_UNDO_INSERT_REC的undo日志在事务提交后可以直接删除掉,而其他类型的undo日志还需要为所谓的MVCC服务,不能直接删除掉
  • TRX_UNDO_PAGE_NODE

Undo页面链表

  • 单个事务中的Undo页面链表;TRX_UNDO_PAGE_NODE属性连成了链表

image_1d79v7bib12041n9d1gpe1t8a10jc1g.png-60.8kB

  • 同一个Undo页面要么只存储TRX_UNDO_INSERT大类的undo日志,要么只存储TRX_UNDO_UPDATE大类的undo日志。所以在一个事务执行过程中就可能需要2个Undo页面的链表,一个称之为insert undo链表,另一个称之为update undo链表

  • 规定对普通表和临时表的记录改动时产生的undo日志要分别记录。一个事务中最多有4个以Undo页面为节点组成的链表

image_1d7bg5o7c3t11nch988lj51hsl9.png-106.8kB

按需分配

多个事务中的Undo页面链表

写入过程·

事务隔离级别和MVCC

问题

  • 脏写

  • 脏读

  • 不可重复读

image_1d8nk4k1e1mt51nsj1hg41cd7v5950.png-139.4kB

  • 幻读image_1d8nl564faluogc1eqn1am812v79.png-96.1kB

四种隔离级别

  • READ UNCOMMITTED:未提交读。

  • READ COMMITTED:已提交读。

  • REPEATABLE READ:可重复读。

  • SERIALIZABLE:可串行化。

MySQL的默认隔离级别为REPEATABLE READ

MVCC原理

版本链 

聚簇索引记录中都包含两个必要的隐藏列:trx_id,roll_pointer

所有的版本都会被roll_pointer属性连接成一个链表,我们把这个链表称之为版本链

每个版本中还包含生成该版本时对应的事务idimage_1d8po6kgkejilj2g4t3t81evm20.png-81.7kB

  • ReadView

READ UNCOMMITTED直接读取记录的最新版本

SERIALIZABLE 锁

ReadView主要为RR和RC服务。READ COMMITTED和REPEATABLE READ隔离级别的的一个非常大的区别就是它们生成ReadView的时机不同。每一个事务的ReadView是自己独有的

  • m_ids:当前系统中活跃的读写事务的事务id列表

  • min_trx_id

  • max_trx_id:下一个事务的id值

  • creator_trx_id:表示生成该ReadView的事务的事务id.(只有在对表中的记录做改动时(执行INSERT、DELETE、UPDATE这些语句时)才会为事务分配事务id)

  • 可见性判断:

  • 被访问版本的trx_id属性值与ReadView中的creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问

  • 被访问版本的trx_id属性值小于ReadView中的min_trx_id值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问

  • 被访问版本的trx_id属性值大于或等于ReadView中的max_trx_id值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问

  • 被访问版本的trx_id属性值在ReadView的min_trx_id和max_trx_id之间,那就需要判断一下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问

  • 如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录

  •  READ COMMITTED —— 每次读取数据前都生成一个ReadView

  • REPEATABLE READ —— 在第一次读取数据时生成一个ReadView 

  • 执行DELETE语句或者更新主键的UPDATE语句并不会立即把对应的记录完全从页面中删除,而是执行一个所谓的delete mark操作,相当于只是对记录打上了一个删除标志位,这主要就是为MVCC服务的

  • insert undo在事务提交之后就可以被释放掉了,而update undo由于还需要支持MVCC,不能立即删除掉。在确定系统中包含最早产生的那个ReadView的事务不会再访问某些update undo日志以及被打了删除标记的记录后,有一个后台运行的purge线程会把它们真正的删除掉

  • 所谓的MVCC只是在我们进行普通的SEELCT查询时才生效。普通select是快照读,而update是当前读,读取记录的最新版本。并且,读取之后,还需要保证其他并发事务不能修改当前记录,对读取记录加锁

  • MVCC 机制下,为啥还会发生幻读:读语句不会分配事务id,creator_trx_id = 0; 假设T0两个select之间使用了update语句;会对creator_trx_id 赋值 ;if x not in m_ids but x < creator_trx_id, 可见, x 为可其他插入的数据;两次select结果可能不一样