MySQL 之 InnoDB存储引擎(一)

目录

一 逻辑存储结构

1.1 表空间

1.2 段

1.3 区

1.4 页

1.5 行

二 架构

2.1 内存结构

2.1.1 Buffer Pool

2.1.2 Change Buffer

2.1.3 Adaptive Hash Index

2.1.4 Log Buffer

2.2 磁盘架构

2.2.1 System Tablespace

2.2.2 File-Per-Table Tablespace

2.2.3 General Tablespace

2.2.4 Undo Tablespaces

2.2.5 Temporary Tablespaces

2.2.6 Doublewrite Buffer Files

2.2.7 Redo Log

2.3 后台线程

2.3.1 Master Thread

2.3.2 IO Thread

2.3.3 Purge Thread

2.3.4 Page Cleaner Thread


一 逻辑存储结构

InnoDB的逻辑存储结构如下图所示:

这里写图片描述

1.1 表空间

表空间是 InnoDB存储引擎逻辑结构的最高层,如果用户开启了参数 `innodb_file_per_table`(在8.0中默认开启),则每张表都会有一个表空间(xxx.ibd),一个MySQL实例可对应多个表空间,用于存储记录、索引等数据。

1.2 段

段,又分为数据段(Leaf node segment)、索引段(Non-leaf node segment)、回滚段(Rollback segment),InnoDB是索引组织表,数据段就是 B+ 树的叶子节点,索引段就是 B+ 树的非叶子节点。段用来管理多个区。

正常情况下,我们检索都是在叶子节点的双向链表进行的,也就是说我们会把区进行区分,如果不区分把叶子节点和非叶子结点混在一起,那么效果就会打大折扣,所以对于一个索引B+树来说,我们区别对待叶子节点的区和非叶子节点的区,并把存放叶子节点的区的集合称为一个段把存放非叶子节点区的集合也称为一个段,所以一个索引会生成两个段:叶子节点段和非叶子节点段。

1.3 区

区,表空间的单元结构,每个区的大小为 1M。默认情况下,InnoDB存储引擎页大小为 16KB,即一个区中有 64 个连续的页。

通过页其实已经形成了完整的功能,我们查询数据时这样沿着双向链表就可以查到数据,但是页与页之间在物理位置上可能不是连续的,如果相隔太远,那么我们从一个页移动到另一个页的时候,磁盘就要重新定义磁头的位置,产生随机IO,影响性能,所以我们才要引入区的概念,一个区就是物理位置连续的64个页。区是属于某一个段的(或者是混合)。

1.4 页

页是 InnoDB 存储引擎磁盘管理的最小单元,每个页的大小默认为 16KB,为了保证页的连续性。InnoDB存储引擎每次从磁盘申请 4~5 个区。

1.5 行

行,InnoDB 存储引擎的数据是按照行进行存放的,在行中,默认有两个隐藏字段:

  •  Trx_id:每次对某条记录进行改动时,都会把相应的事务ID赋值给 Trx_id隐藏列。

  • Roll_pointer:每次对某条记录进行改动时,都会把旧的版本记录到 undo.log 中,然后这个列相当于一个指针。可以通过它来找到该条记录修改前的信息。

二 架构

MySQL 在 5.5 版本开始默认使用 InnoDB 存储引擎,它擅长事务处理。具有崩溃回复特性,在日常开发中使用非常广泛。下面是 InnoDB 的架构图。左侧为内存结构,右侧为磁盘结构

2.1 内存结构

MySQL的内存结构主要分为四大块:Buffer Pool、Change Buffer、Adaptive Hash Index、Log Buffer

2.1.1 Buffer Pool

应用系统分层架构,为了加速数据访问,会把最常访问的数据,放在缓存(cache)里,避免每次都去访问数据库。 

操作系统,会有缓冲池(buffer pool)机制,避免每次访问磁盘,以加速数据的访问。 

MySQL作为一个存储系统,同样具有缓冲池(buffer pool)机制,以避免每次查询数据都进行磁盘IO。

InnoDB存储引擎基于磁盘文件存储,CPU 速度与磁盘速度之间的差距是非常大的,为了最大可能的弥补之间的差距,提出了缓存池的概念。,所以缓存池,简单来说就是一块「内存区域」,通过内存的速度来弥补磁盘速度较慢,导致对数据库造成性能的影响。,避免每次都去访问磁盘。在 InnoDB的缓冲池中不仅缓冲了索引页和数据页,还包含了 undo页、插入缓存、自适应 hash索引以及 InnoDB的锁信息等。

缓存池的基本原理

「读操作」:

在数据库中进行读取页的操作,首先把从磁盘读到的页存放在缓存池中,下一次读取相同的页时,首先判断该页是不是在缓存池中。

若在,称该页在缓存池中被命中,则直接读取该页,否则,还是去读取磁盘上的页。

「写操作」:

对于数据库中页的修改操作,首先修改在缓存池中的页,然后在以一定的频率刷新到磁盘,并不是每次页发生改变就刷新回磁盘,而是通过 checkpoint 的机制把页刷新回磁盘。

可以看到,无论是读操作还是写操纵,都是对缓存池进行操作,而不是直接对磁盘进行操纵。

缓存池结构

缓存池是一片连续的内存空间,innodb 存储引擎是通过页的方式对这块内存进行管理的。

缓存池的结构如下图:

速度快,那为啥不把所有数据都放到缓冲池里?

凡事都具备两面性,抛开数据易失性不说,访问快速的反面是存储容量小:

(1)缓存访问快,但容量小,数据库存储了200G数据,缓存容量可能只有64G;

(2)内存访问快,但容量小,买一台笔记本磁盘有2T,内存可能只有16G;

因此,只能把“最热”的数据放到“最近”的地方,以“最大限度”的降低磁盘访问。

如何管理与淘汰缓冲池,使得性能最大化呢?

预读:磁盘读写,并不是按需读取,而是按页读取,一次至少读一页数据(一般是 16K),如果未来要读取的数据就在页中,就能够省去后续的磁盘IO,提高效率。数据访问,通常都遵循“集中读写”的原则,使用一些数据,大概率会使用附近的数据,这就是所谓的“局部性原理”,它表明提前加载是有效的,确实能够减少磁盘IO。

缓冲池以 Page页为单位,底层采用链表数据结构管理 Page。根据状态,将 Page分为三种类型:

    • free Page:空闲 Page,未被使用
    • clean Page:被使用的 Page,数据没有被修改过。
    • dirty Page:脏页,被使用的 Page,数据被修改过,页中的数据与磁盘中的数据不一致。

参考文献:

MySQL--缓冲池(buffer pool),这次彻底懂了!!!_椰汁菠萝的博客-CSDN博客

MySQL缓存池 - 知乎

2.1.2 Change Buffer

Change Buffer,更改缓冲区(针对于非唯一的二级索引页)。在执行DML语句时,如果数据页在内存中,则直接更新;否则,在不影响数据一致性的前提下,InnoDB 将这些操作缓存在 change buffer 中,这样就不必从磁盘中读取数据,当下次查询需要访问这个数据页时,再将数据页读入内存,然后执行 change  buffer 中与这个页有关的操作,最后将查询结果返回。

merge:将  change  buffer 的操作应用到数据页的过程称为 merge。除了访问数据页会触发 merge 外;系统后台有线程会定期 merge;数据库正常关闭的过程中也会触发 merge 操作。更新操作记录到 change  buffer ,可以减少读磁盘,提高执行效率;而且读入数据会占用 buffer pool ,还可以提高内存使用率。

使用条件

对于唯一索引,所有更新操作都需要做唯一性约束的判断,必须将数据页读入内存,直接在内存中更新,不使用 change  buffer 。

对于普通索引,当数据页在内存中时,直接进行更新操作即可;当数据页不在内存中时,直接将更新操作写入 change buffer 即可。

作用

  1. 索引与表是紧密的关系,插入,更新,删除一条记录,就会触发与表有关系的索引的操作,你可以将他们看做一个事务,如果其中有任何一个对索引的操作失败,则你的数据对表的操作也应该会失败。
  2. 对于表附加的索引的操作必然影响对源表的数据的操作速度,而DML操作会影响数据的读取和事务隔离性相关的性能,然后引起连锁反应,表插入的,更改的,删除的,慢了,则表的SELECT 的性能也必然受到影响。
  3. 当对表执行插入、更新和删除操作时,索引(非聚集索引)列的值通常是无序的,这需要大量的 I/O 来更新辅助索引。当相关页不在缓冲池中时,更改缓冲区将更改缓存到索引,从而通过不立即从磁盘读入页来避免昂贵的I/O操作。当页面加载到缓冲池中时,将合并已缓存的更改,然后将更新后的页面刷新到磁盘。

与聚集索引不同,二级索引通常是非唯一的,并且以相对随机的顺序插入二级索引。同样,删除和更新可能会影响索引树中不相邻的二级索引页,如果每一次都操作磁盘,会造成大量的磁盘IO。有了 Change Buffer之后,我们可以在缓冲池中进行合并处理,减少磁盘 IO。如果 MySQL 承担大量的DML操作,则 Change Buffer 是必不可少的,他的存在就是尽量减小 I/O 的消耗,通过内存进行数据的合并操作,将多次操作操作尽量变为少量的 I/O 操作。带来的坏处,就是 Change Buffer 会使用 innodb_buffer 的空间。

2.1.3 Adaptive Hash Index

自适用 Hash 索引,用于优化对 Buffer Pool数据的查询,MySQL的 InnoDB引擎中虽然没有直接支持 Hash索引,但是提供了一个功能就是这个自适应 Hash索引,Hash索引在进行等值比较时,性能是高于B+树的,因为 Hash索引只需要一次 IO即可,所以 InnoDB存储引擎会监控对表上各索引页的查询,如果观察到在特定的条件下,Hash索引可以提高速度,则会建立 Hash索引,称之为自适应 Hash索引。自适应 Hash索引无需人工干预。是系统根据情况自动完成

参考文献:

图解MySQL | [原理解析] Adaptive Hash Index 是如何建立的 - 知乎

2.1.4 Log Buffer

Log Buffer:日志缓冲区,用于保存要写入磁盘 中的 log 日志数据(redo log、undo log),默认为16MB,日志缓冲区会定期刷新到磁盘中,如果需要更新、插入和删除多行的事务,增加日志缓冲区可以节省磁盘 IO。

参数:innodb_log_buffer_size:缓冲区大小innodb_flush_log_at_trx_commit:日志刷新到磁盘时机,取值主要包含以下三个:

1: 日志在每次事务提交时写入并刷新到磁盘,默认值。

0: 每秒将日志写入并刷新到磁盘一次。

2: 日志在每次事务提交后写入,并每秒刷新到磁盘一次

2.2 磁盘架构

2.2.1 System Tablespace

系统表空间是更改缓冲区的存储区域,如果表在系统表空间而不是表文件或通用表空间中创建的,它也可能包含表和索引数据。InnoDB 系统表空间包含 InnoDB 数据字典(InnoDB 相关对象的元数据),同时,双写缓冲(Doublewrite Buffer)、改变缓冲(Change Buffer)和 undo日志(undo logs)等也存储于系统表空间中。此外,系统表空间也包含用户在该表空间创建的表和索引等数据。由于系统表空间可以存储多张表,因此,其为一个共享表空间。系统表空间由一个或多个数据文件组成,默认情况下,其包含一个叫ibdata1的系统数据文件,位于mysql数据目录(datadir)下。系统表空间数据文件的位置、大小和数目由innodb_data_home_dir和innodb_data_file_path启动选项控制。

show variables like 'innodb_data_file_path'

系统表空间,默认的文件名叫 ibdata1。

参考文献:

MySQL :: MySQL 8.0 Reference Manual :: 15.6.3.1 The System Tablespace

2.2.2 File-Per-Table Tablespace

如果开启了 `innodb-file-per-table` 开关,则每个表的文件表空间包含单个 InnoDB 表的数据和索引,并存储在文件系统上的单个数据文件中

show variables like 'innodb_file_per_table'

也就是说,我们每创建一个表,都会产生一个表空间文件

# 查看数据存放位置 
show global variables like '%datadir%'

参考文献:

win10查找文件存储路径:WIN10下怎么找到MYSQL数据库中存储数据的位置。(默认路径)_qq_1144521901的博客-CSDN博客

2.2.3 General Tablespace

通用表空间,需要通过 `CREATE TABLESPACE` 语法创建通用表空间,在创建表时,可以指定该表空间

# 创建表空间 
CREATE TABLESPACE ts_name ADD DATAFILE 'file_name' ENGINE = engine_name; 

# 创建表时指定表空间 
CREATE TABLE xxx ... TABLESPACE ts_name;

常规表空间功能提供以下功能:

  • 类似于系统表空间,常规表空间是共享表空间,可以存储多个表的数据。
  • 常规表空间比每表文件表空间具有潜在的内存优势 。服务器在表空间的生存期内将表空间元数据保留在内存中。与单独的每表文件表空间中的相同数量的表相比,较少的常规表空间中的多个表为表空间元数据消耗的内存更少。
  • 常规表空间数据文件可以放置在相对于MySQL数据目录或独立于MySQL数据目录的目录中,该目录为您提供了许多数据文件和每表文件表空间的存储管理功能 。与每表文件表空间一样,将数据文件放置在MySQL数据目录之外的功能使您可以分别管理关键表的性能,为特定表设置RAID或DRBD或将表绑定到特定磁盘。
  • 常规表空间支持Antelope和Barracuda文件格式,因此支持所有表行格式和相关功能。支持两种文件格式,通用表空间不依赖

innodb_file_format或innodb_file_per_table 设置,这些变量也不影响通用表空间。

  • 该TABLESPACE选项可用于 CREATE TABLE在常规表空间,每表文件表空间或系统表空间中创建表。
  • 该TABLESPACE选项可用于 ALTER TABLE在常规表空间,每表文件表空间和系统表空间之间移动表。以前,不可能将表从每个表文件表空间移动到系统表空间。使用常规表空间功能,您现在可以这样做。

参考文献:  

mysql通用表空间(general tablespace) - 知乎

2.2.4 Undo Tablespaces

撤销表空间,MySQL实例在初始化时会自动创建两个默认的undo表空间(初始大小16M),用于存储undo log日志。

在MySQL5.6中开始支持把undo log分离到独立的表空间,并放到单独的文件目录下。这给部署不同IO类型的文件位置带来便利,对于并发写入型负载,可以把undo文件部署到单独的高速SSD存储设备上。

参考文献:

MySQL :: MySQL 8.0 Reference Manual :: 15.6.3.4 Undo Tablespaces

2.2.5 Temporary Tablespaces

InnoDB 使用会话临时表空间和全局临时表空间。存储用户创建的临时表等数据

2.2.6 Doublewrite Buffer Files

双写缓冲区,innoDB引擎将数据页从Buffer Pool刷新到磁盘前,先将数据页写入双写缓冲区文件中,便于系统异常时恢复数据。

什么是Double Write

Doulbe Write是开辟在Innodb Tablespace文件上的一块有100个连续 Page 的空间. 注意它是在Innodb Tablespacee文件上. 它的作用是当 MySQL 将数据刷新到 data file的时候, 先将数据 write + fsync() 到Doulbe Write 空间上. 然后在某一个时刻会将数据从Doulbe Write Space写到对应的真正需要写到的page上。

为什么我们会需要 Doulbe Write

这是因为会存在 partial write 的问题。partial write是指mysql在将数据写到数据文件的时候,会出现只写了一半但由于某种原因剩下的数据没有写到innodb file上,出现这种问题可能是由于系统断电,mysql crash造成的, 而造成这个问题更根本的原因是由于mysql 的page size跟系统文件的page size不一致, 导致在写数据的时候, 系统并不是把整个buffer pool page一次性写到disk上,所以当写到一半时, 系统断电,partial write也就产生了; 如果partial write产生, 会发生什么问题呢? 因为我们知道在flush buffer cache的时候,其实redo log已经写好了. 为什么还需要担心partial write呢? 这是因为mysql在恢复的时候是通过检查page的checksum来决定这个page是否需要恢复的, checksum就是当前这个page最后一个事务的事务号; 如果系统找不到checksum, mysql也就无法对这行数据执行写入操作。

Double Write的优点是什么?

double write解决了partial write的问题, 它能保证即使double write部分发生了partial write但也能恢复. 另外一个好处就是double write能减少redo log的量, 有了double write, redo log只记录了二进制的变化量, 也就等同于binary log, 而通过前段时间的测试确实发现,在double write关闭的情况下, redo log比binary logs要大。

double write的缺点是什么?

虽然mysql称double write是一个buffer, 但其实它是开在物理文件上的一个buffer, 其实也就是file, 所以它会导致系统有更多的fsync操作, 而我们知道硬盘的fsync性能是很慢的, 所以它会降低mysql的整体性能. 但是并不会降低到原来的50%. 这主要是因为: 1) double write是一个连接的存储空间, 所以硬盘在写数据的时候是顺序写, 而不是随机写, 这样性能更高. 另外将数据从double write buffer写到真正的segment中的时候, 系统会自动合并连接空间刷新的方式, 每次可以刷新多个pages。

Double Write在恢复的时候是如何工作的?

If there’s a partial page write to the doublewrite buffer itself, the original page will still be on disk in its real location.When InnoDB recovers, it will use the original page instead of the corrupted copy in the doublewrite buffer. However, if the doublewrite buffer succeeds and the write to the page’s real location fails, InnoDB will use the copy in the doublewrite buffer during recovery.InnoDB knows when a page is corrupt because each page has a checksum at the end; the checksum is the last thing to be written, so if the page’s contents don’t match the checksum, the page is corrupt. Upon recovery, therefore, InnoDB just reads each page in the doublewrite buffer and verifies the checksums. If a page’s checksum is incorrect, it reads the page from its original location

如果是写doublewrite buffer本身失败,那么这些数据不会被写到磁盘,innodb此时会从磁盘载入原始的数据,然后通过innodb的事务日志来计算出正确的数据,重新写入到doublewrite buffer.

如果 doublewrite buffer写成功的话,但是写磁盘失败,innodb就不用通过事务日志来计算了,而是直接用buffer的数据再写一遍,在恢复的时候,innodb直接比较页面的checksum,如果不对的话,就从硬盘载入原始数据,再由事务日志开始推演出正确的数据,所以innodb的恢复通常需要较长的时间.

double write是否一定需要?

In some cases, the doublewrite buffer really isn’t necessary—for example, you might want to disable it on slaves. Also, some filesystems (such as ZFS) do the same thing themselves, so it is redundant for InnoDB to do it. You can disable the doublewrite buffer by setting innodb_doublewrite to 0.

如果启用double write以及临控double write的使用

innodb_doublewrite=1表示启动double write

# 查询double write的使用情况 
show status like 'innodb_dblwr%'

2.2.7 Redo Log

重做日志,是用来实现事务的持久性。该日志文件由两部分组成:重做日志缓冲(redo logbuffer)以及重做日志文件(redo log),前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都会存到该日志中, 用于在刷新脏页到磁盘时,发生错误时, 进行数据恢复使用。以循环方式写入重做日志文件,涉及两个文件:

事务的四大特性里面有一个是持久性,具体来说就是只要事务提交成功,那么对数据库做的修改就被永久保存下来了,不可能因为任何原因再回到原来的状态。那么mysql是如何保证持久性的呢?最简单的做法是在每次事务提交的时候,将该事务涉及修改的数据页全部刷新到磁盘中。但是这么做会有严重的性能问题,主要体现在两个方面:

因为Innodb是以页为单位进行磁盘交互的,而一个事务很可能只修改一个数据页里面的几个字节,这个时候将完整的数据页刷到磁盘的话,太浪费资源了!

一个事务可能涉及修改多个数据页,并且这些数据页在物理上并不连续,使用随机IO写入性能太差!

因此mysql设计了redo log,具体来说就是只记录事务对数据页做了哪些修改,这样就能完美地解决性能问题了(相对而言文件更小并且是顺序IO)。

基本概念

redo log是在innodb即存储引擎层产生的,物理日志。

redo log包括两部分:一个是内存中的日志缓冲(redo log buffer),另一个是磁盘上的日志文件(redo log file)。mysql每执行一条DML语句,先将记录写入redo log buffer,后续某个时间点再一次性将多个操作记录写到redo log file。这种先写日志,再写磁盘的技术就是MySQL里经常说到的WAL(Write-Ahead Logging) 技术。

在计算机操作系统中,用户空间(user space)下的缓冲区数据一般情况下是无法直接写入磁盘的,中间必须经过操作系统内核空间(kernel space)缓冲区(OS Buffer)。因此,redo log buffer写入redo log file实际上是先写入OS Buffer,然后再通过系统调用fsync()将其刷到redo log file中,过程如下:

mysql支持三种将redo log buffer写入redo log file的时机,可以通过innodb_flush_log_at_trx_commit参数配置

Mysql的基本存储结构是页(记录都存在页里边),所以MySQL是先把这条记录所在的页找到,然后把该页加载到内存中,将对应记录进行修改

redo log记录形式

redo log实际上记录数据页的变更,而这种变更记录是没必要全部保存,因此redo log实现上采用了大小固定,循环写入的方式,当写到结尾时,会回到开头循环写日志。如下图:

在innodb中,既有redo log需要刷盘,还有数据页也需要刷盘,redo log存在的意义主要就是降低对数据页刷盘的要求。在上图中,write pos表示redo log当前记录的LSN(逻辑序列号)位置,check point表示数据页更改记录刷盘后对应redo log所处的LSN(逻辑序列号)位置。

write pos到check point之间的部分是redo log空着的部分,用于记录新的记录;check point到write pos之间是redo log待落盘的数据页更改记录。当write pos追上check point时,会先推动check point向前移动,空出位置再记录新的日志。

启动innodb的时候,不管上次是正常关闭还是异常关闭,总是会进行恢复操作。因为redo log记录的是数据页的物理变化,因此恢复的时候速度比逻辑日志(如binlog)要快很多。

重启innodb时,首先会检查磁盘中数据页的LSN,如果数据页的LSN小于日志中的LSN,则会从checkpoint开始恢复。

还有一种情况,在宕机前正处于checkpoint的刷盘过程,且数据页的刷盘进度超过了日志页的刷盘进度,此时会出现数据页中记录的LSN大于日志中的LSN,这时超出日志进度的部分将不会重做,因为这本身就表示已经做过的事情,无需再重做。

2.3 后台线程

在InnoDB的后台线程中,分为4类,分别是:Master Thread 、IO Thread、Purge Thread、Page Cleaner Thread

2.3.1 Master Thread

核心后台线程,负责调度其他线程,还负责将缓冲池中的数据异步刷新到磁盘中, 保持数据的一致性,还包括脏页的刷新、合并插入缓存、undo页的回收。

Master Thread 线程里的工作主要是刷新日志、清理LRU缓存、合并insert buffer、确保日志空间足够、设置新检查点,在数据库处于的状态不同时,这些任务的量不相同。

但是真正的刷脏页的操作并没有放在 Master Thread 中进行,而是单独放在了 Page Cleaner Thread 中进行了,这大大减轻了 Master Thread 的工作量。

参考文献:

【MySQL innoDB读书笔记】04 Master Thread 源码分析_代码被吃掉了的博客-CSDN博客

2.3.2 IO Thread

在InnoDB存储引擎中大量使用了AIO来处理IO请求, 这样可以极大地提高数据库的性能,而 IO Thread主要负责这些 IO 请求的回调

线程类型

默认个数

职责

Read thread

4

负责读操作

Write thread

4

负责写操作

Log thread

1

负责将日志缓冲区刷新到磁盘

Insert buffer thread

1

负责将写缓冲区内容刷新到磁盘

我们可以通过以下的这条指令,查看到InnoDB的状态信息,其中就包含IO Thread信息

show engine innodb status \G;

2.3.3 Purge Thread

主要用于回收事务已经提交了的undo log,在事务提交之后,undo log可能不用了,就用它来回收。

InnoDB中delete所做删除只是标记为删除的状态,实际上并没有删除掉,因为MVCC机制的存在,要保留之前的版本为并发所使用。最终的删除由purge线程来决定的什么时候来真正删除文件的

参考文献:

MySQL Innodb Purge简介 - 掘金

MySQL purge 线程 - 腾讯云开发者社区-腾讯云

2.3.4 Page Cleaner Thread

协助 Master Thread 刷新脏页到磁盘的线程,它可以减轻 Master Thread 的工作压力,减少阻塞

参考文献:

MySQL:Innodb page clean 线程 (一): 基础篇-阿里云开发者社区

猜你喜欢

转载自blog.csdn.net/weixin_46058921/article/details/127841282