1.概念
- 因为 IO操作是很耗费性能的,为了避免一条一条读取磁盘数据,InnoDB采取页的方式,作为磁盘和内存之间交互的基本单位。查询一条数据的时候会把该条数据所在的页全部加载进内存里,以便提高后续查询效率。(存储数据的时候一定会根据主键进行排序,所以id为1,2,3,4,5的数据会在同一页中。)
- 一个页的大小一般是16KB。
- InnoDB为了不同的目的而设计了多种不同类型的页,比如:存放表空间头部信息的页、存
放undo日志信息的页等等。我们把存放表中数据记录的页,称为索引页or数据页。我们主要关注的是索引页(数据页)。
2.页的结构
页的结构
如图所示,页主要包括File Header、Page Header、Infimum+Supremum、User Records、Free Space、Page Directory以及File Trailer
其中:
File Header:文件头 存储页的通用信息 页跟页之间的双向关联,指针等
Page Header:页头 存储数据页专有的信息
Infimum+Supremum:页中的最小和最大记录 表中的数据都会最小记录大,比最大记录小,相当于数学中的-∞、+∞的概念。
User Records:用户记录 存放用户记录,因为最小最大记录不是用户主动去存储的,所以为了方便管理,把最小和最大记录单独进行管理,
Free Space:空闲空间 与User Records是互斥的关系,主要用来记录当前页的剩余空间大小
Page Directory:目录,存储记录在页中的相对位置,为了加快查找,能够查询到记录所在的组
File Trailer:文件尾 校验页是否完整
3.页中存储数据的流程
页中存储数据的流程
- 开始的时候是没有User Records的,当插入数据的时候,会向Free Space申请空间
- 插入数据,此时User Records会逐渐变大,而Free Space会逐渐变小
- 当数据正好满了的时候,Free Space会变为0,不过一般情况下,不会这么凑巧,比如,新插入记录大小为30B,而Free Space只有10B,此时会重新申请一个页存储该条记录,这个10B的Free Space就会剩下在当前页中。
4.记录的头信息
- 我们在mysql中最常用的行存储格式是compact 行格式
记录的真实数据(insert语句中插入的内容):
- 主键列的值:存放主键的值
- 两个隐藏列:
- trx_id列的值:事务id
- roll_pointer列的值:回滚的指针,指向回滚的内容
- a列/b列的值:除主键外依次插入值
记录的额外信息:
- 变长字段长度列表:变长字段指varchar等字符串类型,在存储的过程中,int,number都有固定大小的存储,字符串类型传入值多大就会存储多少,这样的目的是节省空间
- NULL值列表:储存空值列表
- 记录头信息:(最重要)
- 预留位1:无需关心
- 预留位2:无需关心
- deleted_flag:删除标志,0未删除,1已删除
- min_rec_flag:目录项,只会在B+树的非叶子节点最小的目录项中添加这个标记
目录项的作用是快速找到对应的页,在record_type的类型1中使用。
- n_owned:记录当前组里的数量,此位置只会在主键最大的记录中维护
- heap_no:堆号,在页面堆(innodb中数据连续排列,这种结构称为heap)中,记录的相对位置,根据heap_no可以直接找到该条记录所在的位置,序号从2开始排列,heap_no为0的是Infimum,为1的是Supremum,分配之后该值就不会发生变化(特别是删除中)。
- record_type:记录类型,4种类型:
- 0- 普通记录(用户插入的记录)
- 1- B+树中非叶子节点的目录项记录(根据目录项可快速找到对应的记录)
- 2- Infimum
- 3- Supremum
- next_record:记录的是当前记录的next_record与下一条记录的next_record之间的偏移量 。 指向下一个记录,记录下一个记录的信息,此处需要注意的是,这里指向的是记录的真实数据的主键列的值,而不是变长字段长度列表,因为这样方便查找,想查下一条记录的额外信息只需要指向下一条记录再往前查询即可,查询真实信息,往后查询即可
5.删除记录操作对next_record的影响
记录在页中是根据主键从小到大顺序排列的
记录之间是单向链表,优势是存储空间小,缺点是不能往前遍历
删除记录操作对next_record的影响
由上图可以看出:
- 被删除的记录的deleted_flag 从0变成1
- 被删除记录的上一条记录的next_record指向被删除记录的下一条记录;
- supremum中n_owned数量由原来的5变成4(当前分组中)
需要注意的是,图中显示的是虚线,实际上这条数据并没有被删除,这就产生了垃圾链,原因是为了复用空间,如果将这条数据直接删除的话,会出现如下问题:记录是顺序存储的,删除之后会出现碎片空间,如果不处理,会造成资源浪费,如果处理,后续的记录需要向前移动,会产生性能损耗;如果只是修改了记录的状态,后续恢复时只需要,将上述步骤进行反向操作即可,此时避免了分配空间。
6. Page Directory(页目录)— — 记录在页中的展现
作用是在页中直接定位记录,提高查找性能
分组规则:
- Infimum单独一组
- Supremum组允许出现1~8条记录
- 普通组4~8条记录
- 只统计非删除记录
Page Directory
上图的分组即为满足分组条件之后分组情况,此时分组1中只有Infimum,分组2中有5条记录,其中最大的是Supremum(依照主键排列,而Supremum是最大的),上图中的Slot即为槽位,也就是page Directory,Slot指向组中最大的那条记录的next_record。
根据分组的规则,当没有数据的时候默认只有两个分组,Infimum和Supremum;新插入数据的时候,会放到Supremum分组中,当Supremum分组的记录已经有8条(含Supremum记录),再插入一条时,会进行组分裂,开新的组,Supremum会分出4条记录给新的组,此时三个分组的情况是 1.4.4,然后再将新的记录插入。
查询:先查询page Directory,使用二分法查找,查到对应的slot即可指定到对应的组,由于组中都是单向链表形式,所以从组中的第一个元素,依次遍历查找即可,因为组中的记录数不多,所以此时效率也不会损耗很多。
7.页满后的插入操作
页满后的插入操作
插入的时候指定id乱序插入,不使用order by查询出来的结果依然是根据id排序的,存储的时候就是根据主键排序。
如果页已经满了,此时插入新的记录时,会申请一个新的页,此时根据主键排序,进行记录迁移,上图中插入的记录比已有的页中的最大用户记录小,此时会将原id为10的记录迁移到新的页中,而将新插入的id为7的记录插入到原10那个位置。
所以开发过程中在设计表时需要有一个id,设置自增,这样就可以避免因为主键排序导致的记录迁移,从而规避不必要的性能消耗。
主键自增有一个auto_increment锁,在单体的时候可以保证id唯一,但是分布式部署的时候,多台mysql没法保证id唯一,此时可以使用雪花算法生成唯一id来标记数据唯一,不同库的id相同也没所谓。
在进行记录迁移的过程中,heap_no也不会发生变化,查找记录根据next_record来查找,不根据heap_no
**************此文章只是本人学习过程中的学习笔记,不做其他用途,如果有其他意见,欢迎一起讨论,谢谢,侵删*************************