InnoDB存储引擎简介

数据库和实例

数据库是物理操作系统文件或其它形式文件的集合。而数据库实例则是真正操作数据库文件的,MySQL数据库实例由后台线程以及一个共享内存区组成,共享内存可以被运行的后台线程所共享。MySQL是一个单进程多线程架构的数据库,也就是说MySQL数据库实例在系统上的表现就是一个进程。

数据库是文件的集合(一般来说是二进制文件),是依照某种数据模型组织起来并存放于二级存储器的数据集合;数据库实例是程序,是位于用户与操作系统之间的一层数据管理软件,用户对数据库数据的任何操作,包括数据库定义、数据查询、数据维护等待都是在数据库实例下进行的,应用程序只有通过数据库实例才能和数据库打交道。所以,其实MySQL被理解为数据库是有点偏颇的,但也没有严格区分。

InnoDB存储引擎

上面提到的数据库实例时操作数据库的,但是操作数据库的方式有很多种,比如:插入一条数据,可以之间修改数据库的文件,也可以“假修改”,等到了一定的时机才修改。而那么这里提到的存储引擎就是解决怎么去操作数据库的问题的。存储引擎有很多种,而MySQL采用的默认的存储引擎就是InnoDB存储引擎。InnoDB存储引擎最早由Inbobase Oy公司开发,其特点是事务安全、行锁设计、支持MVCC、支持外键、提供一致性非锁定读、同时被设计用来最有效的利用以及使用内存和CPU。InnoDB被许多大公司、大项目广泛使用,是一款高性能、高可用、高扩展的存储引擎。

InnoDB体系架构

整体结构

image.png
InnoDB存储引擎有多个内存块,可以认为这些内存块组成了一个大的内存池,内存池主要负责以下工作:

  • 维护所有进程/线程需要访问的多个内部数据结构
  • 缓存磁盘上(即数据库)的数据,方便快速的读取,同时在对磁盘文件的数据修改之前进行缓存
  • 重做日志(redo log)的缓存

后台线程包括多条线程,其主要作用如下:

  • 负责刷新内存池中的数据,保证缓冲池中的内存缓存的是最近的数据
  • 将已修改的数据文件刷新到磁盘并保证数据库发生异常的时InnoDB能恢复到正常运行的状态

    后台线程

    后台线程有多条线程,各条线程负责处理不同的任务,主要包括:Master Thread、IO Thread、Purge Thread和Page Cleaner Thread。

    Master Thread

    Master Thread是后台线程中最核心的线程,主要负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性,包括脏页的刷新、合并插入缓存、undo页的回收……

    IO Thread

    InnoDB存储引擎中会使用大量的异步IO来处理IO请求,这样可以极大的提高数据库的性能,而IO Thread的工作主要就是负责这些IO请求的回调处理(call back)。

    Purge Thread

    事务被提交后,其使用的undo log可能不再需要,因此需要Purge Thread来回收清除已经使用并分配的undo页。purge(回收)操作可以独立到单独的Purge Thread中进行,可以减轻Master Thread的工作压力,从而提高CPU的使用效率以及提升存储引擎的性能。

    Page Cleaner Thread

    Page Cleaner Thread的作用是将脏页刷新进磁盘,脏页的刷新独立到该线程也是为了减轻Master Thread的压力,提高InnoDB存储引擎的性能。

    内存池

    image.png

    缓冲池

    缓冲池结构(Buffer Pool Struct)

    InnoDB 存储引擎是基于磁盘存储的,也就是说数据都是存储在磁盘上的,由于 CPU 速度和磁盘速度之间的鸿沟,InnoDB 引擎使用缓冲池技术来提高数据库的整体性能。缓冲池简单来说就是一块内存区域,在数据库中进行读取页的操作,首先将从磁盘读到的页存放在缓冲池中,下一次读取相同的页时,首先判断该页是不是在缓冲池中,若在称该页在缓冲池中被命中,直接读取该页;否则,读取磁盘上的页。对于数据库中页的修改操作,首先修改在缓冲池中页,然后再以一定的频率刷新到磁盘,并不是每次页发生改变就刷新回磁盘。总的来说,缓冲池的大小之间影响数据库的整体性能。

从图中来看,缓冲池中缓存的数据页类型主要有:索引页(index page)、数据页(data page)、插入缓冲(insert buffer)、自适应哈希索引、数据字典信息。其中缓存索引页和数据页占了缓冲池的很大一部分,在InnoDB中,缓冲池中的页大小默认为16KB。缓冲区其实是一片连续的内存空间,这些缓存页一般连续排布。除了缓存页,缓冲池还会存储这些缓存页的控制信息,InnoDB会为每一个缓存页都创建一个控制信息:包括该页所属的表空间编号、页号、页在Buffer Pool中的地址,一些锁信息以及LSN信息,当然还有一些别的控制信息。每个缓存页对应的控制信息占用的内存大小是相同的,我们就把每个页对应的控制信息占用的一块内存称为一个控制块,控制块和缓存页是一一对应的,它们都被存放到 Buffer Pool 中,其中控制块被存放到 Buffer Pool 的前边,缓存页被存放到 Buffer Pool 后边,所以整个Buffer Pool对应的内存空间看起来就是这样的:
image.png
其中碎片产生的原因是剩余的那点儿空间不够一对控制块和缓存页的大小的存放。

缓冲池管理(LRU、Free、Flush)

当我们最初启动MySQL服务器的时候,需要完成对Buffer Pool的初始化过程,就是分配Buffer Pool的内存空间,把它划分成若干对控制块和缓存页。但是此时并没有真实的磁盘页被缓存到Buffer Pool中,之后随着程序的运行,会不断的有磁盘上的页被缓存到Buffer Pool中,那么怎么知道那些空间是空闲的,那些空间是已经存放了缓存页的了?InnoDB的处理方式是采用一个链表来管理这些空闲的缓存页内存空间,而这个链表就叫做Free List,或叫做空闲链表,效果图如下:
image.png
Free List的节点存储着缓存页控制块的内存地址,而缓存页控制块中又存储着缓存页的内存地址,因此Free List的每一个节点就对应着一个空闲的缓存页,因此当空闲缓存页被数据填充的时候,其对应的Free List中的节点就会被删除。

缓冲池的内存大小是有限的,不可能存放许多的缓存页,那如果缓冲池中已经没有空闲缓存页,但这时又有新的页想要进入缓冲池,那这怎么解决了?这样的话,就必须从缓冲池中移除不活跃的(即不会被经常访问到的)旧缓冲页,那现在的主要问题就是如何确定不活跃的缓冲页了。首先我们需要明确缓冲池出现的意义就是想减少程序和磁盘之间的IO交互以提高效率,我希望的最好情况就是每次访问的页都存在缓冲池,这样就无需把磁盘的页缓存到缓存池了,我们希望缓存页命中率越高越好。而InnoDB采用经典LRU算法来保证比较高的命中率,而LRU算法采用的数据结构就是链表,因此这个管理缓存页的链表就被称为LRU List,当我们需要访问某个页时,可以这样处理LRU链表:

  • 如果该页不在Buffer Pool中,在把该页从磁盘加载到Buffer Pool中的缓存页时,就把该缓存页包装成节点塞到链表的头部
  • 如果该页在Buffer Pool中,则直接把该页对应的LRU链表节点移动到链表的头部
  • 当缓冲池不能存放新读取到的页时,将首先释放LRU List中尾端的页

但是这样做会有一些性能上的问题,比如一次全表扫描或一次逻辑备份就把热数据给冲完了,就会导致导致缓冲池污染问题。Buffer Pool中的所有数据页都被换了一次血,其他查询语句在执行时又得执行一次从磁盘加载到Buffer Pool的操作,而这种全表扫描的语句执行的频率也不高,每次执行都要把Buffer Pool中的缓存页换一次血,这严重的影响到其他查询对 Buffer Pool 的使用,严重的降低了缓存命中率 。为此,InnoDB在原来的基础对LRU算法做了改进,它在LRU List中加入了midpoint,新读到的页,虽然是最新访问的页,但并不是直接插入到LRU列表的首部,而是插入LRU列表的midpoint位置,这个策略称之为midpoint insertion stategy。默认配置插入到列表长度的5/8处。midpoint由配置文件中的参数innodb_old_blocks_pct控制。midpoint之前的列表称之为new列表,之后的列表称之为old列表。可以简单的将new列表中的页理解为最为活跃的热点数据。同时InnoDB存储引擎还引入了参数innodb_old_blocks_time来表示页读取到mid位置之后需要等待多久才会被加入到LRU列表的热端。可以通过设置该参数保证热点数据不轻易被刷出。

我们把缓存页和磁盘上面的页的数据不一致的页称做脏页,一般用户需要修改某页的数据就会使得该页(前提是该页是缓存页)成为脏页。既然缓冲池出现了脏页,就需要把脏页刷新到磁盘,但是LRU List中并不是所有的页都是脏页,如果把LRU List中的页都刷新一般,由于磁盘的IO性能很差,这必然导致需要很长的时间,会造成严重的用户体验不好。为了避免这种情况,我们需要一个新的链表来存储脏页的控制块(不是直接存储脏页),而这个链表就被称为Flush List,这个链表节点指向的脏页都会按照顺序被刷新。

注意: 脏页既存在于LRU列表中,也存在与Flush列表中。LRU列表用来管理缓冲池中页的可用性,Flush列表用来管理将页刷新回磁盘,二者互不影响。


这三个重要链表(LRU List, Free List,Flush List)的关系可以用下图表示:
image.png

重做日志缓冲

InnoDB存储引擎的内存池除了有缓冲池外,还有重做日志缓冲(redo log buffer)。InnoDB存储引擎会先把重做日志信息放入到这个缓冲区,然后按照一定的频率(每秒一次)将其刷新到重做日志文件。因此,重做日志缓冲一般不需要设置很大,用户只需要保证每秒产生的事务量在这个缓冲大小之内即可。重做日志缓冲也可以通过配置文件中的innodb_log_buffer_size参数进行配置,默认值是8M,默认值足以满足绝大部分的应用程序的需求了。下面这三种情况会将重做日志缓冲中的内容刷新到外部磁盘的重做日志文件中:

  • Master Thread每一秒重做日志缓冲刷新到重做日志文件
  • 每个事务提交时会将重做日志缓冲刷新到重做日志文件
  • 当重做日志缓冲中剩余空间小于1/2时,重做日志缓冲刷新到重做日志文件

额外的缓冲池

在InnoDB中,对内存的管理是通过一种称为内存堆的方式进行的。在对一些数据结构本身的内存进行分配时,需要从额外的缓冲池(也称额外内存池)进行申请,如果该区域内存不够则从缓冲池申请。比如:缓冲池的帧缓冲(frame buffer)和对应的缓冲控制对象(buffer control block),这些对象记录了一些锁、等待之类的信息,而这个对象的内存需要从额外缓冲池中申请。因此,在申请了很大的InnoDB缓冲池时,也应该适当考虑增加额外缓冲池的内存大小。