学习目标
- 掌握页的概念
- 认识B(B+)树
- 掌握索引
- 了解Buffer Pool缓冲池
- 了解查询一条数据的过程
知识要点
Page–页
定义
磁盘和内存之间交互的基本单位。一个页的大小一般是16KB。
页分为很多种,存放表空间头部信息的页、存放undo log信息的页等等,我们把存放表中数据记录的页称为索引页或数据页。
页结构
先将页结构分为三部分,暂时只关注第二部分。第二部分的蓝色区域,存放页中最大记录和最小记录,不是用户新增的记录,我们可以看作是无穷大(Supermum)和无穷小(Infimum),下方的User Records是用户新增的记录,Free Space是空闲的空间。随着User Records变大,Free Space会不断变小。
记录的头信息
结构图:
- deleted_flag:逻辑删除标记(0未删除,1已删除)
- min_rec_flag:B+树中每层非叶子结点中的最小的目录项记录。
- n_owned:一个页面被分若干组后,最大的那个记录用于保存组中所有的记录条数。
- heap_no:表示当前记录在页面堆中的相对位置。
- record_type:表示当前的记录类型。
- 0:普通记录。
- 1:B+树中非叶子结点的目录项记录。
- 2:表示Infimum记录。
- 3:表示Supremum记录。
- next_record:表示吓一条记录的相对位置,也就是链表。这个属性非常重要。他表示从当前记录的真实数据到下一跳记录的真实数据的距离。指向记录头信息和主键列的值的中间。
将记录删除后,将deleted_flag置为1,然后将上一条数据的next_record指针指向下一条记录。
同时,我们也可以看到其他记录头信息的数值所代表的不同含义。
示例图:
Page Directory
引言:记录在页中的数据是按照主键值从小打到的顺序串联成为的一个单向链表,因此查询只能以头节点开始逐一向后查询,但是如果数据量比较大,那么性能就无法保证,针对这个问题InnoDB采用了图书目录的解决方案,即:Page Directory
分组规则:
- Infimum记录所在的分组只能有一条记录。(该分组只有Infimum)
- Supremum记录所在的分组只能在1~8条记录之间。
- 剩下的其他记录所在的分组只能在4~8条记录之间。
分组步骤:
- 初始状态,一个页中只有两条数据,即Infimum和Supremum,所以应该分为两组。
- 之后每当插入一条记录时,都会从目录中找到对应记录的主键值比待插入记录的主键值大,并且差值最小的槽,然后把该槽对应的n_owned加1。
- 当一个组中的记录等于8时,当再次插入一条记录时,会将组中的记录拆分成两个组(一个组中四条记录,另一个组中五条记录)。并且在拆分过程中,会在Page Directory中新增一个槽,并记录分组中最大的那条数据的偏移量。
记录在页中的提现:
其中Slot是顺序的,查找时会依次查找,Slot指向改组中最大那条记录。
插入操作
在页中,记录逻辑上是顺序存储的,当我们乱序插入时,查询出来依然是顺序的。当页满后,就会开辟出一个新的页,当插入的记录主键比该页中最大的记录要小时,就会将较大的记录迁移到新的页中,将要插入的记录插入原来的页中,这样就形成了记录迁移。
这里需要注意的是,数据迁移在底层是比较浪费性能的,所以如果插入时主键不是顺序(使用uuid做为主键值)的,那么每次页满后再次插入数据都很有可能造成数据迁移。
B树&B+树
共同特点:
- 一个结点可以存储多个元素。
- 叶子结点有序。
- 每个结点中的元素,都按照从小到大的顺序排列,即:左小右大。
- 所有叶子结点都位于同一层,或者说根结点到每个叶子结点的高度都相同。
B+树特点(与B树的不同点):
- B+树叶子结点是有单项指针的(MySQL中采用的是双向指针)
- B+树的非叶子结点的元素是与叶子结点优点冗余的。
B树(3阶):
B+树(3阶):
Index–索引
采用B+树的数据结构,叶子结点存储完整的数据(数据页),非叶子结点存储主键索引(索引页)。
聚簇索引–主键
这边是将索引直接当作一个数据进行存储,record_type为1,存放了主键和页码。
索引创建原则:
将每一页中最小主键的记录提出,组成索引页,再将索引页中最小主键的索引提出,组成新的索引页…
因为每个页中数据都是逻辑上有序存储的,查询一个数据时,根据当前查询的主键的值找到小于该主键的值,依次向下查找。
比如要查询主键为20的数据,根结点中就需要找到1->根据页码30,找到Page30->在page30中找到12->根据页码9,找到page9->再依次遍历找到主键为20的数据
二级索引–非主键
如果一个属性没有被设置非主键索引,但是采用了这个属性去查询,那么进行的是全表查询,当数据量过大时,就会导致查询数据过慢。
其中查询方式和主键方式一致,不同的是,叶子结点中只存放了非主键索引的值和主键值,如果需要查询表中其他数据,需要进行回表查询,即采用主键索引查询。
联合索引–多列
在联合索引中,会先以第一个字段进行排序,当第一个字段有数据存在多个相同的值时,再以第二个字段进行排序,叶子结点中存放了两个索引和主键索引。
查询语句为c2=4,c3=‘u’,从根结点开心寻找小于4的值,查询到页码为51->找到page51,直接查询到了c2=4的值,该页中没有其他的c2=4的记录,所以接着向下查询,页码为43->下面存在多个c2=4的记录,并且已经是叶子结点,所以遍历查询,查询到主键为1->如果需要查询其他字段的,进行回表查询
记录唯一性
由于目录项中记录有唯一性,所以二级索引中,其实相当于是和主键一起的联合索引。即:索引列的值+主键值+页号,这样如果索引列的值相同,就可以接着比较主键值
Buffer Pool–缓冲池
为了缓存磁盘中的页,MySQL服务器启动时就向操作系统申请了一片连续的内存空间(Buffer Pool),默认的Buffer Pool只有128M,可以在启动服务器的时候配置innodb_buffer_pool_size(单位为字节)启动项来设置自定义缓冲池大小。Buffer Pool对应的一片连续的内存被划分为若干个页面,默认也是16K,该页面称为缓冲页。为了更好的管理Buffer Pool中的这些缓冲页,InnoDB为每个缓冲页都创建了控制块,它与缓冲页时一一对应的。
free链表
Buffer pool的初始化过程中,是先向操作系统申请连续的内存空间,然后把它划分成若干个【控制块&缓冲页】对儿。当插入数据的时候,为了能够知道哪些缓冲页是空闲且可以分配的,MySQL把所有空闲的缓冲页对应的控制块作为一个结点放到一个链表中(free链表)。
类似HashMap,一个控制块对应一个缓冲页。
在free链表中的基结点中,start指向第一个控制块,end指向最后一个控制块,count为空闲控制块的数量。
当缓冲页数据插入数据之后,指针就不再指向对应控制块(链表的删除),同时count-1。
flush链表
如果我们修改了Buffer Pool中某个缓冲页的数据,那么它就与磁盘上的页不一致了,这样的缓冲页被称之为脏页(dirty page)。为了性能问题,我们每次修改缓冲页后,并不着急立刻把修改刷新到新的磁盘上,而是将被修改过的缓冲页对应的控制块作为结点加入到这个链表中(flush链表)。
基结点和free链表相同,当数据被修改后(页变成脏页)会添加一个控制块到flush链表中(链表的插入),同时count+1,直到flush链表被刷到磁盘后,控制块将从flush移除,脏页才会变成不脏的。
刷新方式:
- 后台线程根据当时系统繁忙程度确定刷新速率,定时从flush链表中刷新一部分页面到磁盘。即:BUF_FLUSH_LIST
- 有时后台线程刷新脏页速度比较慢,导致用户准备加载一个磁盘页到Buffer Pool中时没有可用的缓冲页,如果没有,就会尝试查看LRU链表尾部,看是否存在可以直接释放掉的未修改的缓冲页。如果没有,则不得不将LRU链表尾部的一个脏页同步刷新到新的磁盘(速度缓慢,会降低处理用户请求的速度)即:BUF_FLUSH_SINGLE_PAGE
- 后台线程定时从LRU链表尾部开始扫描一些页面,扫描的页面可以通过系统变量innodb_lru_scan_depth来指定,如果在LRU链表中发现脏页,则把它们刷新到磁盘。即:BUF_FLUSH_LRU。
LRU链表
线性预读:如果顺序访问某个区(extent,一个区默认64页)的页面超过了innodb_read_ahead_threshold(默认56)的值,就会出发一次异步读取下一个区中全部的页到Buffer Pool中的请求。
随机预读:如果开启了随机预读功能(默认:innodb_random_read_ahead=OFF),如果某个区(extent)有13个连续的页面都已经被加载到了Buffer Pool中,无论这些页面是否是顺序读取的,都会触发一次异步读取本区全部的页到Buffer Pool中的请求。
针对预读的优化:
InnoDB规定,当磁盘上的某个页在初次加载(只是加载,没有设计读取)到Buffer Pool中的某个缓冲页时,该缓冲页对应的控制块就会被放在old区域的头部。这样预读页就只会在old区域,不会影响young区域中使用比较频繁的缓冲页。
针对全表扫描的优化:
虽然首次加载放到的是old区域的头部,但是由于是全表扫描,会对加载的数据进行访问,那么第一次访问的时候,就会将该页放的young区域的头部,这样仍然会影响young区域的数据,所以,InnoDB规定,在对某个处于old区域的缓冲页进行第一次访问时,就在它对应的控制块中记录下这个访问时间,如果后续的访问时间与第一次访问的时间在某个时间间隔内,那么就不会从old区域移动到young区域的头部,否则就会移动。
chunk和Buffer Pool实例
由于buffer pool是在mysql启动时向操作系统申请的空间,所以如果改变大小可能影响到原来的数据,所以增加chunk,当修改大小时,就会新增一个chunk,从而不会影响原来的空间。
MySQL InnoDB搜索引擎(二)redo log、undo log
MySQL InnoDB搜索引擎(三)事务、MVCC、锁