MySQL-InnoDB 数据结构总结

前言:

摘自《MySQL是怎样运行的》 作者:小孩子4919

文章内纯属个人学习总结,不喜勿喷~ 

一,行格式 :MySQL中的数据在磁盘上的组织形式

  • COMPACT
  • REDUNDANT
  • DYNAMIC
  • COMPRESSED

二,COMPACT 行格式下记录的组织结构

MySQL-INNODB-COMPACT 行格式下记录在磁盘中的结构

1,变长字段长度列表:用于存储 VARCHAR(M),VARBINARY(M),各种TEXT,BLOB类型等长度不固定类型的字段的实际长度。比如 当前表的字符集为UTF8-mb4,字段类型定

义 VARCHAR(32) ,但是该字段实际存储 'AAAA',则对应的字段长度为 4 个字节(byte),若实际存储的是 '中国',则对应字段长度为 2 * 3 = 6 字节(byte),当然某些单个汉字会占用四个字节,

变成字段长度列表的存储顺序是与记录的真实数据顺序相反的,因为记录中next_record 设计时是表示当前记录到下一条记录真实数据的距离(单位/字节);每个变长字段长度表示需要占用 一个字

节还是两个字节,需要看这个字段最大的存储长度(字节),如果小于255 个字节,则使用一个字节的长度来存储该变长字段的长度,如果大于255,则需要看实际存的字段长度是否大于127 ,

是则使用两个字节来存储,否则使用一个字节来存储。比如,字符集UTF8-mb4字符集,VARCHAR(64) 类型的字段,如果实际存 'AAAA',则该变长字段长度使用一个字节来存储,因为实际长度

是4 字节(byte) ,如果该字段实际存储64个中文字符,每个字符使用4个字符表示,则实际长度为4 * 64 = 256 > 255 则使用2个字节存储,如果实际存储 10 个中午字符,每次字符使用4个字节表示

10 * 4 = 40 < 127 则使用1个字节存储长度。对CHAR(M) 这种定义类型的字段,如果编码格式是定长的字符集,则不会加入到变长字段长度列表中

2,NULL 值列表 : 存储字段定义是否为NULL, 二进制位存储,如果定义为NULL,存储顺序也是按照字段顺序的逆序存储。

三,数据页

1,数据页的结构
数据页结构
1.1 Page header : 页面头部
  • PAGE_N_DIR_SLOTS :页面内有Slot数量
  • PAGE_HEAP_TOP
  • PAGE_N_HEAP
  • PAGE_FREE
  • PAGE_GARBAGE
  • PAGE_LAST_INSERT
  • PAGE_DIRECTION
  • PAGE_N_DIRECTION
  • PAGE_N_RECS
  • PAGE_MAX_TRX_ID
  • PAGE_LEVEL : B+ 树表是树的层级
  • PAGE_INDEX_ID
  • PAGE_BTR_SEG_LEAF
  • PAGE_BTR_SEG_TOP
1.2 File Header 文件头部
  • FIL_PAGE_SPACE_OR_CHKSUM
  • FIL_PAGE_OFFSET
  • FIL_PAGE_PREV : 上一页
  • FIL_PAGE_NEXT : 下一页
  • FIL_PAGE_LSN
  • FIL_PAGE_TYPE : 数据页类型 比如 FIL_PAGE_UNDO_LOG ,FILE_PAGE_INDEX 
  • FIL_PAGE_FILE_FLUSH_LSN
  • FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID
1.3 File Trailer 文件尾部
  • 前四个字节代表页面校验和
  • 后四个四节 页面最后被修改的LSN的后四个字节

2,记录在数据页中组织形式
记录在数据页中的组织形式
2.1,next_record : 表示当前记录的真实数据到下一条记录真实数据的距离,next_record = 10 ,表示从当前记录的真实记录开始处向后第10个字节就是下一条记录的真实数据位置,next_record = -10 表示向前10个字节就是下一条记录真实数据的开始位置;
2.2,infimum : 表示当前数据页中默认开始的记录,下一条记录为主键ID 最小的记录, Supremum 表示最后一条记录,也就是说本页实际用户记录主键ID最大的下一条记录是Supremum 。
2.3,delete_flag : 删除标记,当用户执行delete 语句,会将对应记录的delete_flag = 1。delete 掉的记录会别记录到垃圾链表中,
2.4,n_owned :  一个数据页会被分成好多组,每组记录中最大的记录会记录本组内有多少条记录,一般每个组中有记录4~8条,初始情况,一个数据页只有infimum,supremum 两个记录,会分成两个组。当组内记录数超过8 条,再次Insert 数据时,一个组会进行被拆分成两个组, 同时修改 n_owed 数目,Page Directorty 新增Slot ( 地址偏移量),Page Header,File Header 中的信息
2.5,heap_no :记录在本数据页中的相对大小 
2.6,record_type : 记录的类型,0 :普通记录,1 : B+ 数据非叶子节点的目录项记录,2:infimum 记录 ,3:supremum 记录
2.7,Page Directorty : 将每组中最大记录的在本数据页地址偏移量,也就是记录额外信息与真实数据中间的位置,按照顺序排序, 就形成Page Directory。
3,一个数据中查找制定主键的记录

通过二分法定位对应的Slot ,然后找到组内最小的记录,然后通过next_record 便利整个组内的数据。

4,另外记录中还有三个隐藏列,row_id,trx_id,roll_pointer
四,B+ 树结构

数据检索过程 : ID = 8 

从根节点开始,找到符合ID = 8 所在的目录项所在页10,比较主键 值 5< 8 定位下一个目录项数据页page_no = 12,7 < 8 ,再次定位下一个数据页16 ,遍历数据页中记录找到ID= 8

(数据页中定位数据 是根据二分法 定位SLOT ,在去遍历组内记录(4~8条))

1,这棵B+ 树 有三个层级,非叶子节点称为 目录项,对应记录的record_type = 1 ,页与页之间 前后指针 FIL_PAGE_PREV,FIL_PAGE_NEXT中存储,叶子节点存储真实数据

2,目录项: 非叶子节点,目录项中记录和普通记录一样,记录之间使用next_record 进行链接,同样的目录项数据页也是有 Page Directory 用于快速检索记录,每个目录项中的记录中 还包括

目录项记录存储 用户记录数据页中 最小的主键值,以及对应数据页号page_no。

3,聚簇索引:图例表示 是一个主键索引,也就是聚簇索引,即所有的用户记录存储在叶子节点,索引即数据,数据即索引,另外数据页中的用户数据是按照主键ID大排序

4,非聚簇索引: INDEX(A) 二级索引,使用索引字段进行记录以及页号的排序,在根据主键进行排序,等同与 INDEX(A,ID),叶子节点并非存储记录的全部信息,而是存储索引字段值以及主键ID,假设通过二级索引进行检索,先通过二级索引查询到对应记录,在根据对应的ID值 去聚簇索引(主键索引)对应的B+ 树查询全部,这个过程称为 回表。

联合索引: INDEX(A,B)比如该联合索引,叶子节点的组织形式 先按照A排序 在按照B排,在按照主键ID 进行排序 ,等同于INDEX(A,B,ID)进行组织记录,同样的叶子节点上还是会添加对应

的主键值(用于回表),但是在非叶子节点,除了对应的页号之外,还会为其添加主键值(这个主键值来自叶子节点 最小的条记录的主键值),为什么要在目录项的记录添加主键值呢?

目录项的作用的是为快速检索,那当新的数据需要插入是,也是需要目录项来判断这条数据对应的索引记录位于哪一个叶子节点,如果INDEX(A,B)的目录项 存在相同记录,那么新插入时,会比较新记录A,B字段值,如果新插入记录A,B 的值也是和目录项中的相同,那么可能会判断不出新的记录是在那个一个叶子节点(数据页),所以会默认添加主键用于比较。这也是为什么联合索引在使用需要满足最左前缀,只有这样检索才是最有效的,当查询的时候如果查询的字段只有索引的字段,即查询值在二级索引的B+树上时,则不需要回表,这个过程称为索引覆盖。

5,INNODB 数据页默认会存储两条记录,初始创建数据表的时候,都会为其创建一个根节点页面。当时新的数据插入时,逐渐满足不了一个数据页时会发生也分裂,从而产生叶子节点数据页和非叶子节点数据页(目录项数据页),一个根节点自从创建开始就不会变, 后续凡是用到该节点就会从这里开始访问索引

6,聚簇索引是默认存在,即使没有显示创建 primary Key ,或者没有唯一键约束的字段时系统会自动创建 ,记录中的隐藏列 row_id 就是使用在这里。

五,表空间结构
1,独立表空间结构
独立表空间划分

1, 区(extent) : 连续的物理空间,大小1MB,64个16KB的数据页组成一个区。InnoDB 定义了区的四种状态FREE(空闲的区),FREE_FRAG(有剩余空间的碎片区),FULL_FRAG(没有剩余空间的碎片区),FSEG(附属于某个段的区)

2,组(group): 逻辑上的划分,将extent0~extent255划分为一个组,extent256~extent512划分一组,第一组内的第一个区(extent)的前三个数据页的数据页类型分别为

        FSD_HDR,用于记录整个表空间以及本组内的一些属性,整个表空间只有这么一个FIL_PAGE_TYPE = FSD_HDR 类型的数据页。

        IBUFF_BITMAP,

        INODE, 用与存储INODE entry 结构的数据,用于管理段信息

第二组内,XDES :全称extent descripter,用于记录本组内的一些属性。

        IBUFF_BITMAP,                        

3,段(Segment): 某些零散的页面以及一些完整区的集合,B+树的索引结构的存储将叶子节点的区组合起来构成一个段,非叶子节点存储的区组合起来构成一个段,也就是说一个B+树索引有两个段组成,零散的页面怎么理解? 段的空间申请单位不是按照区(1M)为单位进行申请,这样会造成资源浪费一个区默认大小是1MB,那一个小表开始的时候就会分配两个段(2MB)显然不合理,所以Innodb 定义了碎片区的概念, 碎片区中的页面可能不属于同一个段,在数据插入时,段的空间申请是从碎片区按照页来进行申请,当某个段占用了32 个碎片区后, 后续申请就会按照区单位进行申请。

2,独立表空间细节
独立表空间划分结构

2.1 FIL_PAGE_TYPE = FSP_HDR 

该数据页位于第一个组内,第一个区内,第一个数据页中,主要的存储的是XDES Entry ,上图中换色图便是,注意其中 state 便是存储这个区四种状态,Page State BitMap (16字节)用于表示一个区中的64个数据页,一共是128bit ,划分64分,没分两个bit,每一个bit 的第一位表示这个数据页是否是空闲的。为了能够快速的在这个组内找到哪一个区是空闲的,即State = Free ,InnoDB 设计了一种链表,即

  • 通过List Node 把状态为Free 的区对应的XDES Entry 链接成一个链表,称为Free 链表
  •  通过List Node 把状态为Free_FRAG的区对应的XDES Entry 链接成一个链表,称为Free_FRAG 链表
  • 通过List Node 把状态为FULL_FRAG的区对应的XDES Entry 链接成一个链表,称为FULL_FRAG链表

并且在File Space Header 结构中把对应各个链表的头尾节点进行存储,那么在往一些零散数据页插入数据时,从File Space Header 中的基节点中找到FREE_FRAG 链表,找到一个状态Free_FRAG 的区,再去定位到空闲的页(Page State Mapbit),当Free_FRAG 状态的区满足32 个时则需要申请新的区进行插入,那如果定位一个区在哪一个段呢? INNODB有定义了一种链表

  • 同一个段(Seqment)中所有空闲页面对应的区的 XDES Entry 链接成一个链表,称为Free 链表
  • 同一个段(Seqment)中仍然有空闲页面对应的区的 XDES Entry 链接成一个链表,称为not Full 链表
  • 同一个段(Seqment)中没有空闲页面对应的区的 XDES Entry 链接成一个链表,称为Full 链表

这里的链表是属于段(Segment),和 前面三种链表不一样,他们属于表空间。

前面提到一个索引,都有两个段:叶子节点段,非叶子节点段,每个段都有上述属于段的三个链表,在加上属于表空间中的三个链表,就有 2  * 3 + 3 = 9 个链表。

2.2 FIL_PAGE_TYPE = XDES

这个类型的页面和FSP_HDR 类型页面类似,相比少了 Page Space header 结构

2.3 FILE_PAGE_TYEP=INODE

该数据页中的主要结构是 INODE Entry ,主要是维护段内零散页的地址以及数据该段FREE,NOT_FULL,FULL 链表的基节点,一个INODE Entry的大小为192字节,一个页面最多存储85 个节点,那么一个表空间中存在的段超过85 过,就需要使用新的页面进行存储时,就需要使用新的页面,FIL_PAGE_TYPE= INODE,为了更好的管理这个Inode 类型的数据页,INNODB  定义了

两中链表

  • SEG_INNODS_FULL : 在该链表中,INODE类型的页面已经没有空闲去存储INODE Entry 结构
  • SEG_INODES_FREE : 在该链表中,INODE类型的页面还有空闲去存储INODE Entry 结构

这两个链表的基节点也会存储到 FSP_HDR 页面的 File Space Header 中。

2.4 在数据页中还有Page Header 的结构,这个里面有两个 属性用于存储Inode Entry 的地址偏移量

  • PAGE_BTR_SEG_LEAF : 存储叶子节点段对应的Inode Entry 在哪一个表空间,哪一个数据页,数据页中的偏移量
  • PAGE_BTR_SEG_TOP :  存储非叶子节点段对应的Inode Entry 在哪一个表空间,哪一个数据页,数据页中的偏移量

B+树的索引结构,分为叶子段和非叶子段,当使用B+树时,会从B+树的根节点(一旦创建不会更改)中的page  Header 的读取到对应段信息。

3,小结

1,插入数据,从段申请新页面的过程: 首先会寻找FREE_FRAG 状态的区,这就涉及到怎么定位一个表空间中对应状态的区呢? 使用链表,定义了三种状态的链表,并且基节点保存在FSP_HDR 中的File Space Header 中。使用链表节点XDES Entry State状态中定位到FREE_FRAG区后,查看该区下的是否有空闲的数据页,空闲页的定位方法是使用( Page State BitMap) 定位到对应的数据页,有空闲则执行插入,没有空闲页,则需要申请新的状态为FREE的区,将其状态改为 FREE_FRAG,随着数据的插入这个FREE_FRAG区也会变成FULL_FRAG区 ,如果一个段中占满了零散页,就需要申请新的区进行插入数据,于是从段中的NOT FULL 链表,找到一个空闲状态 XSES ENtry 节点,并从去获取空闲的数据页进行插入。

2,引入区的原因,B+ 树的每一层的页时一个双向链表,为了保证相邻的页在物理(磁盘)层面也是相邻的,减少随机IO,所以申请的区的物理地址是连续的,

3,引入段的原因,如果只是按照区来进行分配空间,默认一个新的区的大小是1M ,如果把B+树的节点都分配到区中,那么在读取扫描时,IO成本也是很大的,当发生页分裂的时候,由于区是连续空间, 那么索引项的修改以及数据项的修改带来的变动是很大的。索引InNODB 将B+树将叶子节点所在的区和非叶子节点组成的区分开管理,当分开管理时,那么初始新建表时,如果按照区的大小来进行申请空间,就需要2MB ,显然是有资源浪费,所以才会有段的逻辑概念,用来管理零碎的页面以及完整的区。新表申请空间会先按照数据页进行寻找,当一个段的32 个零星页都沾满时,才会按照区(1M)来进行申请。

4,引入段,组,区,的概念是为了更好的减少MySQL 的随机IO成本,以及更好的避免空间浪费。其中组的概念是为了更好的管理段和区所做手段做的一些规定。例如extent0~entent255 为一组,前三个页面已经定义好了,用于存储段和区的信息,以及表空间的一些信息。 

六,Buffer Pool 缓存池

1,Buffer Pool 的本质是为了减少磁盘IO,和平时使用Redis 减少Mysql 的压力是类似的。Innodb 访问数据时,是把一个页完整的加载到内存称之为缓存页(16KB),为了方便管理这些缓存页,于是为每一个缓存页都创建一个控制块,这些控制块记录这个页所属表空间ID,页号,缓冲页在Buffer Pool 中的地址,链表节点信息

2,Free 链表:链表节点为空闲缓存页对应的控制块,当需要加载新的数据时,即数据页,于是从该链表中取一个空闲缓存页用于存储加载的数据页。

3,Flush 链表:链表节点为被修改的缓存页对应的控制块,对应的缓存页称之为脏页,这些数据页在某个时间点会被同步到磁盘。

4,LRU 链表: 为了减少频繁数据页的加载和缓存页的刷盘,淘汰最少使用的缓存页。频繁使用的缓存页放在链表的头部

4.1 InnoDB 提供了两种预读方式,也是为了减少读取磁盘的IO损耗

  • 线性预读,如果顺序访问某个区(extent,大小1MB) 的数据页超过一个阈值(innodb_read_ahead_thredshold = 56),就会将下一个区的所有数据页都加载到Buffer pool 中,
  • 随机预读,如果一个区的13个连续页面都被加载到Buffer Pool ,那么就会异步将这个区的其余页面也会加载到Buffer Pool,默认该功能关闭 ,innodb_random_read_ahead = OFF

正式由于这两个机制的存在,简单的LRU链表的头部节点会被反复修改,并不能达到想要的效果。

4.2 LRU 链表会被划分为两个区,yonng区,old 区, 首次从磁盘加到Buffer Pool 的页先放到Old 区域的头部,在Innodb_old_blocks_time 的间隔内访问该页,该页不会被移动到Old 区域,当BufferPool 空间不足时首先将Old区头部节点淘汰。有没有和JAVA 虚拟的分代垃圾回收算法相似,不过是反的,年轻代的对象会被先回收掉。

5,为了能动态调整BufferPool的大小,MySQL 5.7 新增chunk 概念,一个BUffer Pool 由多个Chunk 组成,当想要调整大小时,以chunk 为单位进行调整就可以了。但是chunk 的大小是不能动态分配的,因为这是一个连续的空间,在启动时就被指定。

六,Redo Log

1,日志格式

type : 日志格式,针对不同的场景,设计的不同存储格式,图例只是通用结构,其中len 结构也是个别类型才会有的属性,

SpaceId :表空间ID

page number : 页号

offeset :表示上一条记录在页面中地址

len :字节序列的大小

2,那些场景会记录Redo Log ?

1,当没有显示为某个表定义一个主键时,那么系统内部会自己维护一个Max Row Id 的属性用于记录Row ID,当该全局变量的数值是256 的倍数时,就会吧这个值刷新到表空间第七个页面中的属性中,这个属性使用8字节(byte) 进行存储,那么在这个对该属性的修改就会被记录到一条redo log 

2,执行一条Insert 语句,修改包系统数据页和用户数据页(聚簇索引和二级索引对应的B+ 树)时,表中有几个索引就需要更改多少棵B+ 树,对于一颗B+ 树,那么就有更新叶子节点数据页,内节点数据页(目录项),新增新的页面(可能发生页分裂),就有可能更新 Page Directory 槽信息,Page header 中一些统计信息,记录之间是单向链表,使用next_record 进行标识,这种修改也会被记录下来。

3,undo log 数据页的一些信息修改也会被记录,总之redo log 需要记录的信息非常多。但是redo log 在涉及针对不同的场景使用不同的类型,索引占用的空间是很小,

3,页分裂过程

1,插入数据时当数据页剩余的空间不足时,会发生页分裂现象,也就是新建一个叶子节点,把原来数据页的数据一部分复制到这个数据页,然后再把记录插入进去,最后还要在内节点(目录项)数据页新增一条记录用于指向这个新建的数据页。这个情况便会一次性产生很多条记录,Innodb 称为悲观插入,

2,悲观插入有什么问题?多条记录如果不是原子操作,那么可能会产生一个错误的树结构,在数据恢复的时候也是个问题。

3,Innodb 如何解决  多条redo log 记录的原子性问题? 

把多条 redo log 需要原子性保存 称为一个 不可分割的组 (MTR = mini - Transaction)。按照组进行记录,并且在这一组rodo log 的尾部新增一个redo log 用来表示这条记录为这一组redo log 的结束标记,

4,rodo log 的写入过程  

 1,redo log 刷盘时机?
  • log buffer 空间不足时
  • 事务提交时,必须保证对数据页修改的redo log 刷新到磁盘
  • 将某个buffer pool 中的脏页刷新到磁盘时,必须把之前产生在 log buffer 中redo log 刷新到磁盘
  • 后台线程 一秒刷新一次
  • 正常关闭服务时
  • checkpoint 时
2,Innodb 那些手段保证 redo log 的刷新?
  • LSN : 全局变量 log sequence number ,初始值 8704 ,每产生一个MTR 就会以实际写入的字节长度在LSN上累加。(网络协议数据交换的时候也有类似LSN值,递增且连续)。
  • BUF_FREE : 全局变量,用来标记下一条redo log 应该写入到log buffer 的位置,初始值指向第一个日志block的12 字节初,log block header 占用12 KB,LSN 也会增加12 ,此时LSN = 8704 + 12 = 8716,随着 log block 的不断填充,buf_free 也会随之增加 ,但是还会将 log block header 和 log block trailer 的长度加上去。
  • BUF_NEXT_TO_WRITE :全局变量, 初始值8704 ,表示 log buffer 中已经刷新到磁盘的日志偏移量,随着log buffer 的redo log 写入 ,由于redo log 不是立即刷新的,所以该值不会增长,只有将redo log 刷新到日志文件组内时才会更新,刷新多少字节,对应值就会增加多少。
  • 日志问题组前四个block中关键管理信:log file Header, 记录一些基本信息,版本,校验和之类;checkpoint 1: LOG_CHECKPOINT_NO (每执行一个checkpoint 该值加一) ;LOG_CHECKPOINT_LSN,LOG_CHECKPOINT_OFFSET(前者记录结束checkpoint 时对应LSN,系统在恢复时从改值进行,后者表示LSN值在redo log 文件组的偏移量,恢复开始的起点。
  • buffer pool 中 Flush 链表缓存页控制块中 oldest_modification : 第一次修改对应缓存页时,将当时的MTR 开头的LSN 进行记录,并将其控制块移动到链表头部,再次对该数据页的修改时的MTR 对应的LSN记录到newest_modification 中,lsn 越小说明对应的redo log 日志越早产生。flush 链表按照oldest_modification 继续排序。
  • checkpoint_lsn : 用来表示当前系统中可以覆盖redo 日志总量,如果某个MTR对应修改的脏页没有被刷新到磁盘,那么对应的redo log在log buffer 是不可以被覆盖的,如果该脏页刷新到了磁盘,说明对应redo log 占用的空间是可以被覆盖的,同时记录可被覆盖时对应的LSN。这个过程称之一次checkpoint 

3,如何写入

当执行某条语句时,是先修改到Buffer pool 的缓存页,假设是新页,修改后会添加到Flush 链表中,产生多条redo log ,然后会暂存一个缓存区,形成一组或者多组MTR ,写入前根据全局变量BUF_NEXT_TO_WRITE 找到下一条redo log 的偏移量,一组或者多组MTR 会按照顺序写入到 log buffer 中,随之LSN 会增加, BUF_FREE 也会增加,同时修改对应控制块上的oldest_modification 和newest_modification 的值,即LSN值,根据刷盘时机,当redo log 完成刷盘,(可能)对应的LOG_CHECKPOINT_NO,LOG_CHECKPOINT_LSN,LOG_CHECKPOINT_OFFSET此时的数值也会被记录日志文件组中的 checkpoint1或者是checkpoint 2 中 ,这个是根据checkpoint_no 

(执行一次check point 就会加一), 是偶数就放到checkpoint1,奇数就放到checkpoint2中,此时BUF_NEXT_TO_WRITE 也会更新到已经刷盘的log buffer 位置,checkpoint_lsn 也会更新,为什么是可能?脏页刷盘是时刻不断进行的,checkpont 并不是,两个并不在一个线程完成。

4,崩溃恢复

根据redo log 恢复的是BUFFER POOL 中未及时刷新到磁盘的的脏页中的记录数据。

1,确认恢复的起点:在日志文件组中有两个block(checkpoint 1 和checkpoint 2),都存储有的checkpoint_lsn ,对log buffer 中的LSN值小于 checkpoint_lsn 说明脏页此时已经被刷盘了,没有必要进行恢复,checkpoint_lsn 值越大表示最近的一次脏页刷盘,那么比较两个block 中那个checkpoint_no 那个大,再去拿对应的LOG_CHECKPOINT_LSN,LOG_CHECKPOINT_OFFSET,即找到恢复起点。

2,确定恢复的终点:普通block中的log block  header中有个值为 LOG_BLOCK_HDR_DATA_LEN,表示这个block的大小,当填满这个block ,该值为512KB,如果值不满512 则是恢复的终点

3,如何恢复:

  • 使用HASH表,加快恢复速度 key : SPACEID + PAGE number ,同一个页的redo log会在同一个hash 槽中,按照生成数据进链接,这样可以一次性将一个数据页恢复好
  •  跳过已经刷新到磁盘的页面: 即便是对于lsn 大于checkpoint_lsn,但是可能存在脏页时已经刷新到磁盘了,这种就 没有必要恢复了,如何确定这个页面在崩溃时已经刷新到磁盘呢,缓存页上File header 中有个FIL_PAGE_LSN,记录该缓存页最新的修改的LSN,也就是缓存页控制块中的newest modification 值,如果该页已经刷新到磁盘,那么该值是会大于checkpoint_lsn ,这种也是没有必要进行恢复。

七,Undo Log

 1, undo log 的作用?

为了满足事务回滚的需求,保证事务的原子性

2,undo log 与 redo log 的关系?

undo log 本质也是一种数据页,当向File_TYPE = FIL_TYPE_UNDO_LOG 的缓存页写入undo log时同时也会记录该数据页变化,同时记录redo log 。

3,delete 处理流程?
  • delete mark 阶段 : 仅仅将记录的delete_flag = 1 ,此时该记录不会加到垃圾链表中
  • purge 阶段 :专门的线程会真正的将记录删除,从正常的记录链表中移除,并加入到垃圾链表头结点中,同时更新页面的一些信息(File  header ,Page header)

在针对事务回滚,也是只针对第一个阶段,InnoDB 定义一中TRX_UNDO_DEL_MARK_REC 类型的undo log ,该日志会记录旧记录的trx_id,roll_pointer 记录在undo log 中,为了方便寻找上一次对该记录修改的redo log ,

4, update 处理流程?


1,不更新主键-被更新的列占用的存储空间不发生变化

  • 就地更新:直接在原记录上进行修改

2,不更新主键-被更新的列占用的存储空间发生变化

  • 先删除旧记录,再插入新的记录:将这条记录从聚簇索引页面上真正的删除,也就是把这个记录从正常链表移动到垃圾链表,这时真正的删除线程非delete线程,而是由用户线程操作。

3,更新主键

  • 将就记录进行delete mark 操作,事务执行之前只进行delete mark ,事务提交后才会交给系统线程进行真正的删除
  • 根据跟新后的各列的值创建一条新记录,并将其插入聚簇索引中,
5,增删改对二级索引的影响?
  • 对旧的二级索引执行delete mark 
  • 根据跟新后的值创建一个新的二级索引 ,然后重新再B+树中定位并插入进去。

6,表空间如何对undo log 进行管理?

  • 每一个事务都会为其分配一个undo 页面(16KB) 链表,一个事务会生成很多redo log ,那一个页面空间可能就不够,那么每个事务就会分配一个链表,一个事务涉及insert ,update ,delete,这些操作会被分割开到两个链表中,insert undo 页面链表,update undo 页面链表。
  • 如果一个事务涉及到临时表,那么对应的insert undo 页面链表,update undo 页面链表会和对普通表的链表分开
  • Innodb 规定每一个UNDO页面链表都对应一个段,链表中的页面都是从这个段中申请,每个undo 页面链表的头结点 会存储这个页面链表处于那种状态。一共有五个,这个些状态控制这个undo 页面链表如何被重用。
  • 对于没有重用的undo 页面链表,第一个页会被填充Undo page header ,undo log Segment header ,undo Log header ,之后才会真正的写入undo log 。

7,重用Undo log 

每个事务都会独立分配链表,最多可能会被分配四个链表 针对普通表,临时表的 insert undo 页面链表,update undo 页面链表。

  • 该链表中只包含一个Undo 页面
  • 该Undo 页面的已使用的空间小于整个页面空间的四分之三

对于inser undo页面链表,在事务提交时,这部分数据就没有用了,重用时新的undo log 是直接覆盖的,但是对update undo 链表,在一个事务提交后,其日志是不能立马删除的。还要用于MVCC,重用该页面时 旧的记录则不能覆盖,而是继续写入。

8,为事务分配undo 页面链表过程?

  1. 事务的执行过程中首次对普通表进行修改时,首先回去系统表第5 号页面中分配一个回滚段(其实是获取一个Rollback seqment header),
  2. 再分配完回滚段后,在看这个回滚段有没有已经cached 的 undo slot , 根据执行的操作不同,insert 就去inset undo 页面链表, update就去update undo 链表,如果对应的slot 是缓存状态,就会被分配给这个事务
  3. 如果没有找到缓存的SLOT 就会区回滚段重新申请一个可用UNDO slot ,一个undo slot 是否满足条件,看其值是否是FIL_NULL,
  4. 找到可用的Undo slot ,找到可用的undo 页面,没有的话就想undo log seqment,重新分配一个undo log seqment 作为fist undo log ,
  5. 然后事务就可以把undo log 写入这个undo log 页面

9,undo 日志在恢复时候的作用?

没有提交的事务的redo log,可能已经被刷盘了,那么未提交的事务修改过的页面在恢复时可能也会被恢复掉,造成数据的不一致

从系统表空间第5号定位到128 个回滚段的位置上,在每一个回滚段的1024 个SLOT,找到那些值不为FIL_NULL的undo slot ,每一个undo slot 对应着一个undo 页面链表,然后找到第一个链表

的Undo log Seqment Header 对应的TRX_UNDO_STATE,如果属性是TRX_UNDO_ACTIVE,说明这个活跃的事务正在向undo 页面链表写入UNDO log,然后从Undo log Seqment Header 找到TRX_UNDO_LAST_LOG 找到这个undo 页面链表 最后一个undo Log header ,从其中可以找到事务对应ID和其他信息,这些事务就是未提交的。

八,MVCC

1,脏读 : 如果一个事务读取到另外一个事务未提交事务修改过的数据

2,不可重复读:如果一个事务修改了另一个未提交事务读取的数据

3,幻读:某个事务读取了复合搜索条件的记录,之后别的事务有插入复合条件的新纪录,导致本次事务读取了新的记录, 这个所谓的新纪录也就是幻影记录,在本地事务之前Readview 也不知道,Inndb 在可重复的隔离级别下 生成的ReadView在第一次SELECT,该现象无法通过MVCC进行阻断掉。

4,隔离级别:未提交读,读已提交,可重复读,可串行化读

5,MVCC 原理:

1,版本链:一条记录被多次更新,会产生多条undo log ,所有的版本都会被roll_pointer属性链接成一个链表,链接的头结点就是当前记录的最新信息

2,利用版本链来控制并发事务访问相同记录时的行为,称为多版本并发控制

3,适用范围:读已提交,可重复读,需要判断当前事应该读取哪一个版本的数据。

4,一致性视图:ReadView,

在不同的隔离界别生成一致性视图的时机不同

  • 读已提交:每次读取数据前都会生成一个Readview;
  • 可重复读:在第一次读取数据时生成一个readview;

组成部分:

  • m_ids:  在生成readview 时,当前系统活跃的读写事务的事务ID列表
  • min_trx_id : 在生成readview 时,活跃的事务中,事务ID最小的的事务ID
  • max_trx_id : 在生成readview 时,系统应该分配给下一个事务ID(下一个事务ID)
  • creator_trx_id : 生成该事务的事务ID

5,判断某个记录的某个版本是否可见?

  1. 如果被访问的版本trx_id 属性值与readview 中的creator_trx_id 一致。说明当前事务在访问他自己修改过的记录,
  2. 如果被访问版本的trx_id 比readview中min_trx_id 小,则说明该版本的事务在当前事务生成readview 前已经提交,说明该版本可以被当前事务访问
  3. 如果被访问的版本的trx_id 大于等readview中max_trx_id ,说明生成该版本的事务在当前事务生成的readview后才开启,则该版本不可以被当前事务所访问
  4. 如果被访问的版本的trx_Id 在max_trx_id 与min_trx_id 之间,则需要判断trx_id 是否在m_ids 中。如果在,表示该版本对应的事务还处于活跃状态,则不可以被访问,如果不在,说明该版本对应的事务已经提交,则可以被访问。

6,mvcc 的版本链对应的undo log  如何删除?何时删除?

  •  在一个事务提交时,会给事务生成一个为事务no 的值,该值用来表示事务提交的顺序,在事务提交后,该值会被记录到undo log header 中一个trx_undo_trx_no 的属性中
  • 一个readview 生成时还会把系统中最大的事务no +1 的值记录下来
  • Innodb还会把系统中的readview 按照创建时间链接成一个链表,当执行purge 时,就把系统中最早的readviewd取出来,如果当前系统不存在readview ,就会新建一个readview ,然后从History 链表中取出事务no 较小的各组undo 日志,如果一组undo log 的事务no 比系统中最生成的readview的事务no 还要小的话,就说明这个组的undo log 没有用了,就会从history链表中移除,并释放他们占用的空间 

九,锁

1,快照读:一致性读,一致性无所读,利用MVCC 进行读取操作,不会对表中的记录进行加锁操作,所有的普通读在读已提交,可重复读都是属于这种。

2,当前读:

3,共享锁:S锁 (LOCK IN SHARE  MODE)

4,独占说:X锁 (FOR UPDATE)

5,意向共享锁:IS 锁,当前事务准备在某条记录加上S锁时,需要先对表级别加IS锁

6,意向独占锁:IX 锁,当前事务准备对某条记录加上X锁时,需要对当前表级别加IX锁

7,AUTO_INC 锁:执行一个插入语句就会加一个表级别上加一个AUTO_INC锁

8,Record Lock :LOCK_REC_NOT_GAP ,也区分X,S 锁,

9,GAP 锁:间隙锁,仅仅是为了防止幻影记录插入,

10,Next-key Lock:既想锁住某条记录,又想阻止其他事务在该记录之前的间隙插入新的记录,LOCK_ORDINARY  本质是一个Record Lock 和一个GAP锁

11,Insert Intention Lock : 如果插入操作需要等待时,事务在等待的过程中也会加上一个锁,称之为插入意向锁,这里是利用其他的一些机制(事务ID)完成锁的作用。

12,隐式锁 

13,那些情况下,多条记录的锁可以放到一个锁结构中

  1. 在同一个事务中进行加锁
  2. 被加锁的记录都在同一个页面中
  3. 加锁的类型是一样的
  4. 等待状态是一样的

14,加锁场景:

普通SELECT:

  1. 读未提交:读 不加锁,会产生脏读,不可重复读,幻读
  2. 读已提交:读不加锁,每次执行普通的SELECT都会生成一个readview ,避免了脏读,不能避免不可重复读,幻读
  3. 可重复读: 读不加锁,只有在第一次select 的时候才会生成一个readview ,避免脏读,不可重复读,幻读(可能存在)
  4. 序列化读:当autocommit = 0 ,普通的select 会被转换为select ..Lock in share mode ,读取当前记录需要获取当前记录的S ;当autocommit = 1 ,自动提交,则不加锁,意味着每条语句就是一个事务,利用MVCC 生成一个ReadView 来读取记录

锁定读: LOCK IN SHARE MODE,FOR UPDATE,UPDATE.DELETE

加锁流程

  • 19
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值