InnoDB存储引擎支持事务,其设计目标主要面向在线事务处理(OLTP)的应用。其特点是行锁设计支持外键,并支持类似于Oracle的非锁定读,即默认读取操作不会产生锁。从MySQL数据库5.5.8版本开始,InnoDB存储引擎是默认的储存引擎。

InnoDB存储引擎将数据放在一个逻辑的表空间中,这个表空间就像黑盒一样由InnoDB存储引擎自身进行管理。从MySQL4.1版本开始,它可以将每个InnoDB存储引擎的表单独存放到一个独立的ibd文件中。此外,InnoDB存储引擎支持用裸设备来建立表空间。

InnoDB通过使用多版本并发控制(MVCC)来获得高并发性,并且实现了SQL标准的4种隔离级别,默认为REPEATABLE级别。同时,使用一种被称为next-key locking的策略来避免幻读(phantom)现象的产生。除此之外,InnoDB存储引擎还提供了插入缓存(insert buffer)、二次写(Double Write)、自适应哈希索引(adaptive hash index)、预读(Read ahead)等高性能和高可用的功能。

对于表中数据的存储,InnoDB存储引擎采用了聚集(clustered)的方式,因此每张表的储存都是按主键的顺序进行存放。如果没有显式地在表定义时指定主键,InnoDB存储引擎会为每一行生成一个6字节ROWID,并以此作为主键。

引擎内部

体系结构

InnoDB存储引擎有多个内存块,可以认为这些内存块组成了一个大的内存池,

内存池

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

截屏2020-04-13 下午1.36.37

后台线程

  • 主要作用是负责刷新内存池中的数据,保证缓冲池中的内存缓存的是最近的数据,

  • 将以修改的数据文件刷新到磁盘文件

  • 保证在数据库发生异常的情况下InnoDB能恢复到正常的运行环境

InnoDB储存引擎是对线程模型,因此其后台有多个不同的后台线程,负责处理不同的任务

  • Master Thread

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

  • IO Thread

在InnoDB引擎中大量使用了AIO(Async IO)来处理写请求,这样可以极大提高数据库的性能。而IO Thread的工作主要是负责这些IO请求的回调。共有4种IO线程 write、read、insert buffer和log IO thread从InnoDB1.0之后read thread和write thread 分别增大到4个可以通过innodb_read_io_threads和innodb_write_io_threads参数进行设置

  • Purge Thread

事务被提交后,奇所使用的undolog可能不再需要,因此Purge Thread来回收已经使用并分配的undo页在InnoDB1.1之前Purge Thread的工作由Master Thread中完成,在InnoDB1.1之后purge操作可以独立单独的线程中进行减轻了Master Thread的工作压力,从而提高了了CPU的使用率以及提升储存引擎的性能。Purge的独立线程可以在MySQL的配置文件中手动开启

[mysqld]
innodb_purge_threads=1
  • Purge Cleaner Thread

Purge Cleaner Thread是在InnoDB1.2.x版本时引入。其作用是将之前版本中脏页的刷新操作都放入单独的线程里来完成。目的是减轻Master Thread的工作以及用户查询线程的阻塞。

缓冲池

InnoDB存储引擎是基于磁盘存储的,其中的记录按照页的方式进行管理。因此可以视为磁盘数据库系统。CPU速度和硬盘速度相差很远,所以一般基于磁盘的数据库通常使用缓冲池技术来提高数据库的性能。

  • 读取页

缓冲池就是一块内存区域数据库进行读取页的操作时,首先将从资盘读到页存放在缓冲池中,下一次在读相同的页时,首先判断该页是否在缓冲池中,若在缓冲池中则称该页在缓冲池中被命中直接读取该页,否则读取磁盘上的页。

  • 修改页

对于数据库中页的修改操作,则首先修改缓冲池中的页,然后再以一定的频率刷新到磁盘上,页从缓冲池刷新回磁盘的操作并不是每次也发生更新时出发而是通过Checkpoint的机制刷新回磁盘。缓冲池的大小也可以设置

innodb_buffer_pool_size
  • 页的分类

缓冲池中数据页的类型有:索引页、数据页、undo页、插入缓冲页、自适应哈希索引、InnoDB储存的的锁信息、数据字典信息等。缓冲池中索引页和数据页占很大一部分,从InnoDB1.0以后的版本中允许有多个缓冲池的实例每个页根据哈希值平均分配到不同的缓冲池实例中这样可以减少数据库内部的资源竞争,增加数据库的并发性。多实例的配置也可以在配置文件中修改

innodb_buffer_pool_instances

截屏2020-04-13 下午3.46.17

页的管理 LRU List、 Free List、 Flush List

  • LRU List

数据库中的缓冲池通过LRU(最近最少使用)算法进行管理。就会死把使用最频繁的放在LRU列表的前端,而最少使用的页放在LRU列表的尾端,当缓冲池满的时候首先释放LRU列表尾部的数据页,InnoDB对传统的LRU算法进行了一些优化,在LRU列表中加入了midpoint位置,新读到的页虽然是最新访问的页,但并不将它放在LRU列表的首都而是放在LRU列表的midpoint位置其中midpoint位置位于LRU列表长度的5/8处midpoint也可以通过配置文件进行修改(以百分之作为单位)

innodb_old_blocks_pct = 50

以上参数代表midpoint位置在LRU列表的50%处,在InnoDB引擎中把midpoint之后的列表称为old list之前的列表称为new list,也可以简单的把new list中的页是活跃的热点数据。为什么使用这种LRU的算法呢?若直接将读取到的页放入LRU的首都那么缓冲区中的页很容易由于索引或数据扫描操作被刷新出去,导致缓冲池效率受影响。为了解决这个问题InnoDB引入了一个配置参数进一步的管理LRU列表

innodb_old_blocks_time

用于表示页读取midpoint位置后等待多久才会被加入LRU列表的热端。

  • Free List

LRU列表用来管理已经读取的页,但当数据库刚启动时,LRU列表是空的,即没有任何的页。这时都存放在Free列表中。当需要从缓冲池中分页时,首先从Free列表中查找是否有可用的空闲页,若有则将该页从Free列表中删除,放入LRU列表中,否则淘汰LRU列表末尾的页,将该内存空间分配给新的页。当页从LRU列表的old部分加入到new部分时称此操作为page made young,而因为innodb_old_blocks_time的设置而导致页没有从old部分移动到new部分的操作称为page not made young。通过查看InnoDB引擎的状态中的Buffer pool hit rate表示缓存中的命中率改值不应该小于95%否则需要观察是否由于全表扫描引起的LRU列表污染。

  • Flush List

在LRU列表中的页被修改后,该页称为脏页,就是缓冲池中的页和磁盘上的页的数据产生了不一致。这时数据库会通过CheckPoint机制将脏页刷新回磁盘,而Flush列表中的页即为脏页列表,需要注意的是,脏页既在LRU列表中也存在Flush列表中。LRU列表用于管理缓冲池中页的可用性,Flush列表用于管理将脏页刷新回磁盘,二者互不影响。

重做日志缓冲

InnoDB引擎首先将重做日志先放入这个缓冲区,然后按一定频率将其刷新到重做日志文件。重做日志一般不需要很大,因为一般情况下每一秒会将重做日志缓冲刷新到日志文件,因此用户只需要保证每秒产生的事务量在这个缓冲大小之内即可,该值也可以通过配置文件改变

innodb_log_buffer_size

在通常情况下8MB的重做日志缓冲池足以满足绝大部分的应用,因为重做日志在下列三种情况下会将重做日志缓冲中的内容刷新到外部磁盘的重做日志中

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

CheckPoint技术

如果一条DML语句如Update、Delete改变了页中的记录,那么此时页是脏的,即缓冲池中的页的版本要比磁盘的新,数据库需要将新版本的页从缓冲池刷新到磁盘。但是每当一个页发生变化就将新的页刷新到磁盘那这个开销就会变的很大,数据库的性能也就变得非常差,同时如果在从缓冲池把脏页刷新到硬盘时发生了宕机那么数据就不能恢复了,为了避免数据丢失,当前事务数据库系统普遍采用了Write Ahead Log策略,即当数据库提交事务时先写重做日志,然后再修改页,这样宕机时的数据就可以通过重做日志来进行恢复。通过重做日志来完成数据恢复,这也是事务ACID中D的要求。

  • CheckPoint解决的问题

1.缩短数据库的恢复时间

2.缓冲池不够用时,将脏页刷新到磁盘

3.重做日志不可用时,刷新脏页

当数据库发生宕机时,数据库不需要重做所有的日志,因为CheckPoint之前的页已经刷新到磁盘,故只需要对CheckPoint后面的重做日志进行恢复这样就大大减少了恢复的时间。

CheckPoint所做的事情无外乎是将缓存池中的脏页刷回到磁盘,不同之处在于每次刷新多少页到磁盘,每次从哪里取脏页,以及什么时间触发CkeckPoint在InnoDB储存引擎内部,有两种CheckPoint分别为

  • Sharp CheckPoint
  • Fuzzy CheckPoint

其中Sharp CheckPoint发生在数据库关闭时将所有的脏页都刷新回磁盘其参数为

innodb_fast_shutdown=1

但是数据库在运行时使用Sharp CheckPoint那么数据库的可用性就会受到很大的影响,所以在InnoDB储存引擎内部使用了FuzzyCheckPoint进行页的刷新,即指刷新一部分脏页而不是刷新所有的脏页回硬盘。几种情况下的Fuzzy CheckPoint

  • Master Thread CheckPoint

以每秒或每十秒的速度从缓存池的脏页列表中刷新一定比例回硬盘整个过程是异步的不会影响查询线程

  • FLUSH_LRU_LIST CheckPoint

InnoDB需要保证LRU列表中需要有差不多100个空闲页可用如果没有或不足那么就会将LRU列表尾部的页移除,如果这些页中有脏页那么就会触发CheckPoint

  • Async/Sync Flush CheckPoint

重做日志在不可用的情况这时需要强制将一些页刷回到硬盘,而此时脏页是从脏页列表中选取的。也是为了保证重做日志的循环使用的可用性。

  • Dirty Page too much CheckPoint

这个很好理解就是脏页的数量太多了导致引擎强制进行CheckPoint,总之目的就是为了保证缓冲池中有足够多的可用页。强制CheckPoint的阈值也是可以通过参数进行配置

innodb_max_dirty_pages_pct

Master Thread 工作方式

Master Thread是具有最高优先级别的线程,内部分为多个循环

  • Main loop
  • background loop
  • flush loop
  • suspend loop

Master Thread会在这四种循环中进行切换执行不同的任务

Main loop

分为每秒的操作和每十秒的操作

每秒的操作

操作发生
日志缓冲刷新到硬盘,即使这个事务还没有提交总是
合并插入缓存可能
至多刷新100个缓冲池中的脏页到硬盘可能
如果用户没有活动切换到background loop可能

每十秒的操作

操作发生
刷新100个脏页到磁盘可能
合并至多5个插入缓冲总是
将日志缓冲刷新到磁盘总是
删除无用的Undo页总是
刷新100个或者10个脏页到硬盘总是

Background loop

当钱没有用户活动或者数据库关闭就会切换到这个循环background loop的操作

操作发生
删除无用的Undo页总是
合并20个插入缓冲总是
跳回到主循环总是
不断刷新100个页直到符合条件可能(跳转到flush loop中完成)

Suspend loop

当flush loop中没有事情可做了引擎会切换到Suspend loop,将Master Thread挂起等待事件发生。

InnoDB 关键特性

插入缓冲(Insert Buffer)

InsertBuffer和数据页一样,也是物理页的以个组成部分,对于非聚簇索引的插入或更新操作不是每一次都直接插入索引页中,而是先判断插入的非聚簇索引是否在缓冲池中,若在则直接插入若不在则先放入一个InsertBuffer的对象然后以一定频率和情况进行InsertBuffer和辅助索引页子节点的merge操作这时通常能将多个插入合并到一个操作中就可以提高非聚簇索引的插入性能。InsertBuffer需要满足以下两个条件引擎才会使用

  • 索引是辅助索引
  • 索引不是唯一的

ChangeBuffer是在InnoDB1.0之后的版本引入的可以看作是InsertBuffer的升级,ChangeBuffer的出现使InnoDB可以对DML操作INSERT、DELETE、UPDATE都进行缓冲他们分别是InsertBuffer、DeleteBuffer、PurgeBuffer和之前InsertBuffer一样ChangeBuffer适用对象依然是非唯一的辅助索引对一条记录进行Update操作分为两个过程:

  • 将记录标记为已删除
  • 真正将记录删除

因此DeleteBuffer对应update操作的第一个过程,即将记录标记为删除PurgeBuffer对应update操作个第二个过程即将记录真正的删除,ChangeBuffer也是可以通过配置文件选择性的开启

innodb_change_buffering=[inserts][deletes][purges][changes][all][none]

也可以配置ChangeBuffer占缓冲池的最大百分比,默认值为25也就是1/4缓冲池内存空间,最大值为50。

innodb_change_buffer_max_size

两次写(Double Write)

如果说InsertBuffer带给InnoDB存储引擎的是性能上的提升,那么double write带给InnoDB引擎的是数据页的可靠。

如果在数据库发生宕机时,可能InnoDB储存引擎正在写入某个页到表中,而这个页只写了一部分,比如16KB的页,只写了前4KB之后发生了宕机,这种情况被称为部分写失效在引擎未使用doublewrite技术前会出现写失效导致数据丢失的情况。

doublewrite由两部分组成,一部分在内存中的doublewrite buffer大小为2MB另一部分是物理磁盘上共享表空间中连续128个页,即2个区,大小同样为2MB,在对缓冲池的脏页进行刷新时,并不直接写磁盘,而是会通过memcpy函数将脏页先复制到内存中的doublewrite buffer,之后通过doublewrite buffer再分两次,每次1MB顺序地写入共享表空间的物理磁盘上,人后马上调用fsync函数,同步磁盘,避免缓冲写带来的问题,在这个过程中,因为doublewrite页时连续的因此这个过程是顺序写,开销并不大。在完成doublewrite页的写入后,再将doublewrite buffer中的页写入各个表空间的文件中,此时写入是离散的。如下图

截屏2020-04-14 下午12.38.33

自适应哈希索引(Adaptive Hash Index)

哈希是一种很快的查找方法,一般情况下hash算法得当时间复杂度为O(1),而B+树的查找次数,取决于树的高度,一般B+树在生产环境中高度在3~4左右,故需要3~4次查询。

InnoDB引擎会监控表上个索引页的查询。如果观察到建立哈希索引可以带来速度提升,就会建立哈希索引,称之为自适应哈希索引(AHI)AHI通过缓冲池的B+树的页构造而来,因此建立的速度很快,而且不需要对整张表构建哈希索引。InnoDB引擎会自动根据访问的频率和模式自动的为某些热点页建立哈希索引。

AHI有一个要求,即对这个页的连续访问模式必须是一样的,访问模式是指查询的条件一样此外AHI还有如下条件

  • 以该模式访问100次
  • 页通过该模式访问了N次,其中N=页中记录*1/16

异步IO(Async IO)

在InnoDB引擎中read ahead方式的读取都是通过AIO完成,脏页的刷新即磁盘的写入操作则全部由AIO完成。

刷新邻接页(Flush Neighbor Page)

当刷新一个脏页时InnoDB引擎会检测该页所在区的所有页如果有脏页那么一起进行刷新,这样做的好处显而易见,通过AIO可以将多个IO写入操作合并为一个IO操作,故这种工作机制在传统机械硬盘上有显著的优势但需要考虑:

  • 是不是可能将不怎么脏的页进行了写入,而该页之后很快又变回了脏页?
  • 固态硬盘有着较高的IOPS是否需要这个特性?

多版本并发控制(MVCC)

可以认为MVCC是行级锁的一个变种,但是它在很多情况下避免了加锁操作,因此开销更低。虽然实现机制有所不同,但大都实现了非阻塞读的操作,写操作也只锁定必要的行。

MVCC的实现是通过保存数据在某个时间点的快照实现的。也就是说不管需要执行行多长时间,每个事务看到的数据都是一致的。根据事务开始时间的不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。对于不同的引擎MVCC实现是不同的典型的有乐观并发控制和悲观并发控制。

InnoDB的MVCC是通过在每行记录后面保存两个隐藏的列实现的,这两个列,一个保存的创建时间,一个保存的过期时间。当然存储的并不是实际的时间值,而是系统版本号。每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本海会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。