mysql 内存页解析

父文章 mysql 原理探究

InnoDB表聚集索引层高什么时候发生变化 - 知乎

好书推荐 《MySQL是怎样运行的》笔记_whitedust_白塵的博客-CSDN博客_mysql怎么运行 h

锁知识点: next-key locks nextkey lock 间隙锁 到底是什么?

大家都知道mysql中数据是存储在物理磁盘上的,而真正的数据处理又是在内存中执行的。由于磁盘的读写速度非常慢,如果每次操作都对磁盘进行频繁读写的话,那么性能一定非常差。为了上述问题,InnoDB将数据划分为若干页,以页作为磁盘与内存交互的基本单位,一般页的大小为16KB。这样的话,一次性至少读取1页数据到内存中或者将1页数据写入磁盘。通过减少内存与磁盘的交互次数,从而提升性能。

页是什么

首先,我们需要知道,页(Pages)是 InnoDB 中管理数据的最小单元。Buffer Pool 中存的就是一页一页的数据。再比如,当我们要查询的数据不在 Buffer Pool 中时,InnoDB 会将记录所在的页整个加载到 Buffer Pool 中去;同样的,将 Buffer Pool 中的脏页刷入磁盘时,也是按照页为单位刷入磁盘的。

页的概览

我们往 MySQL 插入的数据最终都是存在页中的。在 InnoDB 中的设计中,页与页之间是通过一个双向链表连接起来。

而存储在页中的一行一行的数据则是通过单链表连接起来的。

上图中的 User Records 的区域就是用来存储行数据的。那 InnoDB 为什么要这么设计?假设我们没有页这个概念,那么当我们查询时,成千上万的数据要如何做到快速的查询出结果?众所周知,MySQL 的性能是不错的,而如果没有页,我们剩下的只能是逐条逐条的遍历数据了。

那页是如何做到快速查询的呢?在当前页中,可以通过 User Records 中的连接每条记录的单链表来进行遍历,如果在当前页中没有找到,则可以通过下一页指针快速的跳到下一页进行查询。

Infimum 和 Supremum

有人可能会说了,你在 User Records 中还不是通过遍历来解决的,你就是简单地把数据分了个组而已。如果我的数据根本不在当前这个页中,那我难道还是得把之前的页中的每一条数据全部遍历完?这效率也太低了。

当然,MySQL 也考虑到了这个问题,所以实际上在页中还存在一块区域叫做 The Infimum and Supremum Records ,代表了当前页中最大和最小的记录。

有了 Infimum Record 和 Supremum Record ,现在查询不需要将某一页的 User Records 全部遍历完,只需要将这两个记录和待查询的目标记录进行比较。比如我要查询的数据 id = 101 ,那很明显不在当前页。接下来就可以通过下一页指针跳到下页进行检索。

Page Directory索引

可能有人又会说了,你这 User Records 里不也全是单链表吗?即使我知道我要找的数据在当前页,那最坏的情况下,不还是得挨个挨个的遍历100次才能找到我要找的数据?你管这也叫效率高?

不得不说,这的确是个问题,不过是一个 MySQL 已经考虑到的问题。不错,挨个遍历确实效率很低。为了解决这个问题,MySQL 又在页中加入了另一个区域 Page Directory 。

顾名思义,Page Directory 是个目录,里面有很多个槽位(Slots),每一个槽位都指向了一条 User Records 中的记录。大家可以看到,每隔几条数据,就会创建一个槽位。其实我图中给出的数据是非常严格按照其设定来的,在一个完整的页中,每隔6条数据就会有一个 Slot。

Page Directory 的设计不知道有没有让你想起另一个数据结构——跳表,只不过这里只抽象了一层索引。

MySQL 会在新增数据的时候就将对应的 Slot 创建好,有了 Page Directory ,就可以对一张页的数据进行粗略的二分查找。至于为什么是粗略,毕竟 Page Directory 中不是完整的数据,二分查找出来的结果只能是个大概的位置,找到了这个大概的位置之后,还需要回到 User Records 中继续地进行挨个遍历匹配。

不过这样的效率已经比我们刚开始聊的原始版本高了很多了。

页的真实面貌

数据页的结构

Infimum + Supremum

这个值位于整个页面的第三部分,分别是最小记录和最大记录,属于MySQL为每个页添加的虚拟记录。最小记录的记录头中heap_no为0, 最大记录的记录头中heap_no为1, 也就是说正式记录中的heap_no属性从2开始。最小记录的record_type 是2,最大记录的record_type 是3。最小记录是页中单链表的头结点,最大记录是页中单链表的尾结点。

记录的头信息

COMPACT行格式

记录的真实数据除了包含各列具体的数据外,还会自动添加一些隐藏列数据。DB_ROW_ID(行ID,唯一标识一条记录)、DB_TRX_ID(事务ID)、DB_ROLLPTR(回滚指针)。只有当数据库没有定义主键或者唯一键时,隐藏列row_id才会存在,并且将其作为数据表主键。

  • 变长字段长度列表:逆序记录每一个列的长度,如果列的长度小于 255 字节,则使用一个字节,否则使用 2 个字节。该字段的实际长度取决于列数和每一列的长度,因此是变长的。
  • NULL 标志位:一个字节,表示该行是否有 NULL 值。
  • 记录头信息:五个字节,其中 next_record 记录了下一条记录的相对位置,一个页中的所有记录使用这个字段形成了一条单链表。

  • delete_mask:标记该记录是否被删除。
  • min_rec_mask:B+树的每层非叶子节点中的最小记录都会添加该标记。
  • n_owned:表示当前记录拥有的记录数。
  • heap_no:表示当前记录在记录堆的位置信息。
  • record_type:表示当前记录的类型,0表示普通记录,1表示B+树非叶子节点记录,2表示最小记录,3表示最大记录。
  • next_record:表示下一条记录的相对位置。

delete_mask:标记着当前记录是否被删除,0表示未删除,1表示删除。未删除的记录不会立即从磁盘上移除,而是先打上删除标记,所有被删除的记录会组成一个垃圾链表。之后新插入的记录可能会重用垃圾链表占用的空间,因此垃圾链表占用的存储空间也被成为可重用空间。

heap_no:表示当前记录在本页中的位置,比如上边4条记录在本页中的位置分别是2、3、4、5。实际上,InnoDB会自动为每页加上两条虚拟记录,一条是最小记录0,另一条是最大记录1。这两条记录的构造十分简单,都是由5字节大小的记录头信息和8字节大小的固定部分(其实内容就是infimum或者supremum)组成的。这两条记录被单独放在Infimum + Supremum的部分。

next_record:表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量。可以简单理解为是一个单向链表,最小记录的下一个是第一条记录,最后一条记录的下一个是最大记录。为了更加形象的展示,我们可以用箭头来替代一下next_record中的地址偏移量。

从图中也能看出来,用户记录实际上按照主键大小正序排序行成一个单向链表。如果从中删除掉一条记录,这个链表也是会跟着变化的,比如我们把第2条记录删掉:

  • 第2条记录并没有从存储空间中移除,而是把该条记录的delete_mask值设置为1。
  • 第2条记录的next_record值变为了0,意味着该记录没有下一条记录了。
  • 第1条记录的next_record指向了第3条记录。

行溢出数据

VARCHAR(M)最多能存储的数据

MySQL对一条记录占用的最大存储空间是有限制的,除了BLOB或者TEXT类型的列之外,其他所有的列(不包括隐藏列和记录头信息)占用的字节长度加起来不能超过65535个字节。可以不严谨的认为,mysql一行记录占用的存储空间不能超过65535个字节。这个65535个字节除了列本身的数据之外,还包括一些其他的数据(storage overhead),比如说我们为了存储一个VARCHAR(M)类型的列,其实需要占用3部分存储空间:

  1. 真实数据
  2. 真实数据占用字节的长度
  3. NULL值标识,如果该列有NOT NULL属性则可以没有这部分存储空间

mysql中磁盘与内存交互的基本单位是页,一般为16KB,16384个字节,而一行记录最大可以占用65535个字节,这就造成了一页存不下一行数据的情况。在Compact和Redundant行格式中,对于占用存储空间非常大的列,在记录的真实数据处只会存储该列的一部分数据,把剩余的数据分散存储在几个其他的页中,然后记录的真实数据处用20个字节存储指向这些页的地址,从而可以找到剩余数据所在的页,如图所示:

这种在本记录的真实数据处只会存储该列的前768个字节的数据和一个指向其他页的地址,然后把剩下的数据存放到其他页中的情况就叫做行溢出,存储超出768字节的那些页面也被称为溢出页。

Dynamic和Compressed行格式

mysql中默认的行格式就是Dynamic。Dynamic和Compressed行格式和Compact行格式很像,只是在处理行溢出数据上有差异。Dynamic和Compressed行格式不会在记录的真实数据出存放前768个字节,而是将所有字节都存储在其它页面中。Compressed行格式会采用压缩算法对页面进行压缩,以节省空间。

Page Directory(页目录)--记录在页中的展现

我们已经知道,记录在页中按照主键大小正序串联成了一个单链表。如果我们要根据主键查找具体的某条记录应该怎么办,简单的方式是根据链表进行遍历。但是在数据量比较大的情况下,这种方式显然效率太差了。因此mysql使用了Page Directory(页目录)来解决这个问题。

Page Directory(页目录)大致的原理如下:

  • 将所有正常的记录(包括最大和最小记录,不包括标记为已删除的记录)划分为几个组。怎么划分先不关注。
  • 每个组的最后一条记录(也就是组内最大的那条记录)的头信息中的n_owned属性表示该组内共有几条记录。
  • 将每个组的最后一条记录的地址偏移量单独提取出来按顺序存储到靠近页尾部的地方,这个地方就是所谓的Page Directory。

mysql规定对于最小记录所在的分组只能有 1 条记录,最大记录所在的分组拥有的记录条数只能在 1-8 条之间,剩下的分组中记录的条数范围只能在是 4-8 条之间

比如现在的page_demo表中正常的记录共有18条,InnoDB会把它们分成5组,第一组中只有一个最小记录,如下所示:

通过Page Directory在一个数据页中查找指定主键值的记录的过程分为两步:

  1. 通过二分法确定该记录所在的槽,并找到该槽所在分组中主键值最小的那条记录。
  2. 通过记录的next_record属性遍历该槽所在的组中的各个记录。

Page Header(页面头部)

Page Header专门用来存储数据页相关的各种状态信息,比如本页中已经存储了多少条记录,第一条记录的地址是什么,页目录中存储了多少个槽等等。固定占用56个字节,各部分字节属性含义如下:

名称占用空间大小描述
PAGE_N_DIR_SLOTS2字节在页目录中的槽数量
PAGE_HEAP_TOP2字节还未使用的空间最小地址,也就是说从该地址之后就是Free Space
PAGE_N_HEAP2字节本页中的记录的数量(包括最小和最大记录以及标记为删除的记录)
PAGE_FREE2字节第一个已经标记为删除的记录地址(各个已删除的记录通过next_record也会组成一个单链表,这个单链表中的记录可以被重新利用)
PAGE_GARBAGE2字节已删除记录占用的字节数
PAGE_LAST_INSERT2字节最后插入记录的位置
PAGE_DIRECTION2字节最后一条记录插入的方向
PAGE_N_DIRECTION2字节一个方向连续插入的记录数量,如果最后一条记录的插入方向改变了的话,这个状态的值会被清零重新统计。
PAGE_N_RECS2字节该页中记录的数量(不包括最小和最大记录以及被标记为删除的记录)
PAGE_MAX_TRX_ID8字节修改当前页的最大事务ID,该值仅在二级索引中定义
PAGE_LEVEL2字节当前页在B+树中所处的层级
PAGE_INDEX_ID8字节索引ID,表示当前页属于哪个索引
PAGE_BTR_SEG_LEAF10字节B+树叶子段的头部信息,仅在B+树的Root页定义
PAGE_BTR_SEG_TOP10字节B+树非叶子段的头部信息,仅在B+树的Root页定义

File Header(文件头部)

File Header是用来描述各种页都适用的一些通用信息的,由以下内容组成:

名称占用空间大小描述
FIL_PAGE_SPACE_OR_CHKSUM4字节页的校验和(checksum值)
FIL_PAGE_OFFSET4字节页号
FIL_PAGE_PREV4字节上一个页的页号
FIL_PAGE_NEXT4字节下一个页的页号
FIL_PAGE_LSN8字节页面被最后修改时对应的日志序列位置(英文名是:Log Sequence Number)
FIL_PAGE_TYPE2字节该页的类型
FIL_PAGE_FILE_FLUSH_LSN8字节仅在系统表空间的一个页中定义,代表文件至少被刷新到了对应的LSN值
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID4字节页属于哪个表空间

我们重点关注一下几个属性:

  • FIL_PAGE_SPACE_OR_CHKSUM:当前页面的校验和(checksum)。对于一个很长的字节串来说,我们可以通过某种算法来计算一个比较短的值来代表这个很长的字节串,这个比较短的值就称为校验和。通过校验和可以大幅度提升字符串等值比较的效率。
  • FIL_PAGE_OFFSET:每一个页都有一个唯一的页号,InnoDB通过页号来可以定位一个页。
  • FIL_PAGE_TYPE:代表当前页的类型,我们前边说过,InnoDB为了不同的目的而把页分为不同的类型。
  • FIL_PAGE_PREV和FIL_PAGE_NEXT:表示本页的上一个和下一个页的页号,各个页通过FIL_PAGE_PREV和FIL_PAGE_NEXT形成双向链表。

File Trailer

mysql中内存和磁盘的基本交互单位是页。如果内存中页被修改了,那么某个时刻一定会将内存页同步到磁盘中。如果在同步的过程中,系统出现问题,就可能导致磁盘中的页数据没能完全同步,也就是发生了脏页的情况。为了避免发生这种问题,mysql在每个页的尾部加上了File Trailer来校验页的完整性。

File Trailer由8个字节组成:

  • 前4个字节代表页的校验和:这个部分是和File Header中的校验和相对应的。简单理解,就是File Header和File Trailer都有校验和,如果两者一致则表示数据页是完整的。否则,则表示数据页是脏页。
  • 后4个字节代表页面被最后修改时对应的日志序列位置(LSN)
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
MySQL内存占用分为两部分:系统内存占用和MySQL进程内存占用。下面分别介绍如何分析这两部分内存占用。 1. 系统内存占用 系统内存占用主要是指MySQL进程使用的操作系统内存,包括缓存、堆栈、共享内存、代码段等。可以使用操作系统工具来分析系统内存占用情况。 Linux系统可以使用top、ps和free等命令来查看系统内存占用情况。例如,使用top命令可以查看系统中各个进程的内存占用情况,包括进程的占用内存、虚拟内存、共享内存等。使用free命令可以查看系统的内存使用情况,包括已用内存、可用内存、缓存等。 Windows系统可以使用任务管理器来查看系统内存占用情况。在任务管理器的“进程”选项卡中,可以查看各个进程的内存占用情况。在“性能”选项卡中,可以查看系统的内存使用情况。 2. MySQL进程内存占用 MySQL进程内存占用主要是指MySQL服务器进程使用的内存,包括缓存、临时表、连接池、查询缓存等。可以使用MySQL内置工具来分析MySQL进程内存占用情况。 可以使用SHOW PROCESSLIST命令来查看MySQL服务器当前的连接情况,包括连接数、连接状态、连接使用的内存等。可以使用SHOW VARIABLES LIKE 'key_buffer_size'、SHOW VARIABLES LIKE 'query_cache_size'等命令来查看MySQL服务器配置的缓存大小,从而估算其内存占用情况。 可以使用MySQL自带的性能监控工具Performance Schema来分析MySQL进程内存占用情况。Performance Schema可以监视MySQL服务器的内部活动和性能指标,包括内存占用、CPU占用、I/O等。可以使用Performance Schema相关的视图和表来查询MySQL服务器的内存占用情况,例如: ``` SELECT * FROM performance_schema.memory_summary_global_by_event_name; ``` 以上是一些常用的方法,可以帮助分析MySQL内存占用情况。需要根据具体情况选择合适的方法进行分析。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值