InonoDB(一)

发布于:2025-05-24 ⋅ 阅读:(17) ⋅ 点赞:(0)

1. InnoDB简介

1.1. mysql为什么默认使用InnoDB存储引擎?

InnoDB在设计的时候考虑了处理巨大数据量的性能,InnoDB支持事务,回滚并且具有崩溃修复的能力,通过多版本并发控制减少锁定,同时还支持外键约束,通过缓冲池在主内存中缓存数据从而提升查询性能,也可以每个表使用各自的独立空间存储数据并且文件大小只受限于操作系统,由于InnoDB存储引擎存储数据量大,性能高,可以有效地保证数据安全等特点,在MySQL5.5版本成为默认的存储引擎

1.2. 长啥样?

内存结构包括

  1. 缓冲池(Buffer Pool): 内存中的主要工作区域,优化查询的性能
  2. 变更缓冲区(Change Buffer): 优化修改操作的性能
  3. 日志缓冲区(Log Buffer)
  4. 自适应哈希(Adaptive Hash Index) : 进一步提升查询的性能

磁盘中的结构包括:

  1. 系统表空间(System Tablespace)
  2. 独立表空间(File-Per-Table-Tablespaces)
  3. 通用表空间(General-Tablespaces)
  4. 临时表空间(Temporary Tablespaces)
  5. 撤销表空间(Undo Tablespaces)
  6. 重做日志(Undo Log)
  7. 双写缓冲区(Doublewrite Buffer)

1.2.1. 为什么要设计成内存和磁盘结构两个部分?

从MySQL实现角度来思考这个问题,数据库的作用就是保存数据,用户的真实数据最终都会保存在磁盘上,在查询数据的过程中,如果每次都从磁盘上读取将会严重影响效率,为了提高访问效率,InnoDB会把查到的数据缓存到内存中,当再次查询的时候,如果目标数据已经存在存在中,就可以从内存中直接读取,从而大幅提升效率

也就是说,磁盘中的文件是用来保存数据时间数据持久化的,内存结构是用来缓存数据提升效率的

1.3. 使用InnoDB存储引擎创建的表对应的数据文件在哪里?

  1. 当使用InnoDB存储引擎创建一个表的时候,默认会在数据命令对应的数据库子目录中生成响应的表空间文件,以.ibd为文件额度后缀,用来存储数据和索引,如果每个表都对应一个表空间文件,称为独立表空间,在MySQL5.7之后的版本中默认为每个表生成独立表空间,可以通过系统变量innodb_file_per_table[=ON|OFF]进行控制,如果关闭这个选项,则所有的表的数据都在系统表空间中存在,独立表空间文件如下所示:
root:/var/lib/mysql/test_db# ls
test_tb.ibd

2. MySQL存储结构

2.1. 什么是表空间文件

表空间文件是用来存储表中数据的文件,表空间文件的大小由存储的数据多少决定,不同的表空间文件存储的数据也有所不同,在MySQL中表分为5类,包括系统表空间,独立表空间,通用表空间和撤销表空间

2.1.1. 表空间和表空间文件的关系

表空间可以定义为MYSQL为了管理数据而设计的一种数据结构,主要描述的是对结构的定义,表空间文件是对定义的具体实现,以文件的格式存在与磁盘上

2.2. 用户数据在表空间中是怎么存储的?

  1. 用户的数据以数据行的方式存储在对应表空间文件中,那么表空间文件中很多个数据行都需要进行管理,以便后续进行高效的查询
  2. 为了方便管理,表空间由段,区组,区,页,数据行组成,其中页是Innodb磁盘管理的最小单位
  3. 也就是说,若干数据组成了页,多个页组成了区,多区组成了区组,多个区组组成了段,多个段组成了表空间

2.3. 为什么使用页这个数据管理单元

  • 首先需要明确,MySQL中的页是应用层的一个概念,是MySQL根据自身的应用场景,定义的一种数据结构
  • 通常操作系统中的文件系统在管理磁盘文件的时候以4KB作为一个管理单元,称为"数据块",但是在数据库的应用场景里,查询时候数据量都比较大,如果也使用4KB作为存储的最小单元,就显得有点小了,同时会造成频繁的磁盘IO,导致降低效率
  • 所以MySQL根据自身的情况定义了大小为16KB的页,作为磁盘管理的最小单位
  • 每次内存与磁盘的交互至少读取一页,所以在磁盘中每个页内部的地址都是连续的,之所以这样做,是因为在使用数据的过程中,根据局部性原理,将来要使用的数据大概率与当前访问的数据在空间上是临近的,所以一次查询直接从磁盘中读取一页的数据放在内存中,当下次查询还在这个页中的时候直接从内存中读取,从而减少磁盘IO,提高性能
  • MySQL根据自身的应用场景使用页作为数据管理单元,最主要就是要减少IO次数,提高性能

2.4. 数据页的基本特性

  1. 页的16KB的大小是MySQL的一个默认设置,适用于大多数场景,当然也可以根据自己的实际业务场景进行修改页的大小,通过系统变量innodb_page_size进行调整与查看,调整页大小的时候需要保证设置的值是操作系统"数据块"(4KB)的整数倍,从而保证操作系统与磁盘交互的时候数据块的完整性,不会被分割或者浪费,所以规定了innodb_page_size可以设置的值,分别是:4096,8192,163843276865536 ,对应 4K8K16K32K64K
  2. 每一个页即使没有数据也会使用16KB的存储空间,同时与索引的B+树中的节点对应,查看页的大小,可以通过系统变量innodb_page_size查看
mysql> show variables like 'innodb_page_size';
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| innodb_page_size | 16384 |
+------------------+-------+
1 row in set (0.01 sec)

  1. 在不同的使用场景中,页的结构也有所不同,MySQL中有多种不同类型的页,但不论哪种不同的页都会包含页头(File Header) 和 页尾(File Trailer),在这页头和页尾之间的页主体信息根据不同的类型有不同的结构,最常用的就是用来存储数据和索引的’索引页’,也叫做数据页,页的主体信息使用数据行进行填充,页的基本结构如下所示:

2.5. 查询的数据超过一个页的大小,怎么提升效率? -区

2.5.1. 不同的页在磁盘的位置

在不做任何控制的情况下,不同页在磁盘中申请的地址大概率是不连续的,所以如果页在磁盘中可以被连续读取,那么查询效率就高

2.5.2. 为什么不连续的地址会降低查询的效率?

  1. 当存储介质是机械硬盘的时候,访问不连续的地址会带来磁盘寻址的开销,也就是磁头在不同盘面,磁道和扇区的机械转动,这个过程称为磁盘随机访问,非常影响效率
  2. 因此,当查询的数据大于一页时不加任何控制会产生磁盘随机访问,这个是影响查询效率的主要因素,那么怎么提升查询效率的问题就变成了,页在磁盘中是否连续的问题

2.5.3. Innodb怎么保证页在磁盘上的连续性?

  • 为了解决磁盘随机访问非常低效的问题,需要尽可能在磁道上读取连续的数据,减少磁头的移动,从而提升效率,MySQL使用Extent(区)这个结构来管理页 规定每个区的固定大小为1MB,可以存放64个页,这时候如果跨页读取数据的话,大概率都是在附近的区域,可以较大幅度减少磁头移动
  • 同时,如果频繁的读取某个区中的页,可以把整个区都读取出来放在内存中,减少后续查询对磁盘的访问次数,进一步提升效率

2.6. 当表中的数据很少的时候怎么避免空间浪费?

新创建的表中没有数据,或者说数据很少,1MB的空间用不到,那不就存在空间浪费的情况了嘛?

实际上,为了节省空间,最初只是创建了7个初始页,而不是一个完整的区,可以通过以下sql查看:

这些零散页会放在表空间中一个叫做碎片区的区域,随着数据量的增加,会申请新的页来存储数据,当碎片区达到32个页的时候,后续每次都会申请一个完整的区来存储更多的数据

2.7. 如果访问的数据跨区了,怎么办? 区组

不同的区在磁盘上的位置大概率是不连续的,当表中的数据越来越多的时候,为了有效的管理区,定义了区组的结构,每个区组固定管理256个区,即256MB,通过区组可以在物理结构层面非常高效的管理和定位到每个区

  1. 第一个区组中的首个区的前四页比较特殊,也就是初始页中的前四页,分别是:
    1. File Space Header:表空间和区组中的条目信息
    2. Insert Buffer Bitmap: Change Buffer 相关信息
    3. File Segment inode: 段信息
    4. B-tree Node : 索引根信息
    5. 其他空闲页用来存储真实的数据
  2. 其他区组中首个区的结构都一样,前两个页分别是:
    1. Extent Descriptor(XDES) 区组条目信息
    2. Insert Buffer Bitmap:Change Buffer相关信息
  3. 使用区组结构有效的管理区,每个区组固定管理256个区即256MB,区组条目信息中会记录每个区的偏移量并用双向链表连接

2.8. 进一步优化-> 段

上面说到的区,区组,还有页这种都是物理结构

在物理结构的基础上,定义了一个逻辑上的概念,就是"段"

  1. 段并不对应表空间中的连续的物理区域,可以看做是"区"和’页’的一个附加标注信息,段的主要作用是区分不同功能的区和在碎片区中的页,主要分为"叶子节点段"和"非叶子节点段" 等,这两个段和我们常说的B+树索引中的叶子,非叶子节点对应,可以简单的理解为"非叶子节点段" 存储和管理索引树,“叶子节点段” 存储和管理实际数据,从逻辑上讲,最终由"叶子节点段"和"非叶子节点段" 等段空间线程了表空间文件,如图所示

3. 页结构

3.1. 页的分类

Innodb在不同的场景定义多种不同类型的页,常用的有数据页,Undo log页,Change Buffer页,Extent Descriptor页,Innodb段信息页等,每种页的数据结构都不同,其中我们最关注的就是数据页,由于InnoBD中有一个概念叫做"索引即数据" ,所以也叫做索引页

不论哪种类型的页都具有页头(File Header)和页尾(File Trailer)两个信息

3.2. 页头和页尾包含的信息

页头和页尾中包含的是用来描述文件相关的信息:

  1. 页头:
    1. 页号: 占用4字节,相当于页的身份证号,通过这个长度可以计算出每个InnoDB表中最多可以拥有2^(4*8)-1约42亿个页,表空间第一个页编号从0开始,之后的页号分别是1,2,3…依次类推,具体页的偏移量计算公式为: 页号*每页大小,那么按照每个页默认的16KB计算.一个表空间最大的容量为2^(4*8) * 16KB = 64TB,这也是InnoDB表空间最大容量是64TB的原因
    2. 上一页页号
    3. 下一页页号:多个页通过这两个信息组成双向链表,即使不同的页地址不连续,也可以通过链表连接
    4. 表空间ID,当前页属于哪个表空间
    5. 页类型: 数据页对应的页类型是FILE_PAGE_INDEX = 0x45BF
    6. 最近一次修改的LSN,占用8Byte
    7. 已被刷到磁盘的LSN ,占用8Byte
    8. 校验和: 用于页的完整性校验
  2. 页尾:
    1. 最近一次修改的LSN
    2. 校验和: 对应页头中的校验和

如果在数据传输的过程中数据丢失或异常中断,导致一个数据页不完整就可以通过页头和页尾的校验

和进行验证,验证算法默认使用 CRC32

LSN:是"Log Sequence Number"的缩写,表示日志序号。用一个任意的、不断增加的值表示日志中

记录的操作对应的时间点,用8字节的无符号长整形表示

3.3. 数据行包含的信息

数据行主要存储真实数据,为了方便数据的管理和描述,InnoDB在每个数据行中还添加了一些额外(管理)信息,于是每一个DYNAMIC数据行都可以划分为两部分,一部分存储额外信息,一部分存储真实数据,额外信息部分包含变长字段长度列表和NULL值列表两个大小不确定的区域,以及固定占用5字节以及40BIT的头信息区域,头信息中存储了行的基本信息,包含行在页内的位置heap_no,行类型record_type,下一行的地址偏移量next_record等6项信息.

3.3.1. 数据行是如何组织在一起的?

  • 数据行通过下一行的地址偏移量,即next_record将页内的所有数据行组成了一个单向链表,这里要注意的是,地址偏移量执行的是下一行中的真实数据的起始地址,这样做的好处是,向右是真实数据,向左就是头信息,而无需额外的长度计算

3.3.2. 怎么标识新页中的第一行和最后一行

了解了行的基本结构和组织方式之后,那么当遍历页中的行的时候,从哪里开始到哪里结束呢?

实际上,每创建一个新页,都会自动分配两个行,一个是行类型为2的最小行Infimun,heap_no位置固定为0号,和一个行类型为3的最大行Supermun,heap_no位置固定为1号,这两个行并不存储任何真实信息,而是作为数据行链尾的头和尾,虽然不存储真实数据,但他们的数据结构和真实数据行完全一致,只不过数据区域存储的是代表他们身份的固定字符串Infimun,Supermun,新页没有数据的时候,最小行Infimunnext_record直接连接最大行Supermun,最大行不连接任何行,他的next_record为0

3.4. 向一个新页插入数据的执行步骤

当向一个新页插入数据时,heap_no会从2 号开始递增,表示当前记录在页面堆中的相对位置;如果是真实数据则record_type 为0,如果是索引目录(B+树非叶节点)数据则record_type 为1;再将Infimun 连接第一个数据行,最后一行真实数据行连接Supremun ,这样数据行就构建成了一个单向链表,更多的行数据插入后,会按照主键从小到大的顺序进行链接;为了使页的结构更加清晰,通常将页中有数据行的区域称为用户数据区User Records ,把未被数据行占用的区域称为空闲区Free Space ,如下图所示:

3.5. 查询数据是否在页中的方式

从头开始遍历是一个最简单的方法,也可以实现数据的查找,当按主键或索引查找某条数据时,从头行 infimun 开始,沿着链表顺序逐个比对查找,但一个页有16KB,通常会存在数百行数据,每次都要遍历数百行,无法满足高效查询。

如何提高效率? - > 页目录

  1. 为了提高查询效率,InnoDB采用二分查找来解决查询效率问题。
  2. 具体实现方式是,在每一个页中加入一个叫做页目录Page Directory 的结构,将页内包括头行、尾行在内的所有行进行分组,约定头行单独为一组,其他每个组最多8条数据,同时把每个组最后一行在页中的地址,按主键从小到大的顺序记录在页目录中在,页目录中的每一个位置称为一个槽,每个槽都对应了一个分组,这样在插入数据行完成链接后,一旦最后一个分组中的数据行超过分组的上限8个时,就会分裂出一个新的分组,为了快速判断每个分组是否达到了8个的上限,在每个分组最后一行中用 n_owned 记录了这个分组内的行数,与此同时在页目录中创建一个新的槽,后续插入的行都遵守这个规则;
  3. 后续在查询某行时,就可以通过二分查找,先找到对应的槽,然后在槽内最多8个数据行中进行遍历即可,从而大幅提高了查询效率;
  4. 例如要查找主键为6的行,先比对槽中记录的主键值,定位到最后一个槽2,再从最后一个槽中的第一条记录遍历,第二条记录就是我们要查询的目标行

3.6. 关于事务、索引这些信息在页中的记录


网站公告

今日签到

点亮在社区的每一天
去签到