InnoDB概述

InnoDB是MySQL默认也是一款比较优秀的存储引擎,他的优秀在于很多特有的特性以及优秀的事务处理能力,与其说是存储引擎倒不如说他是一个数据库实例,我说的是功能上的丰富,实际上在MySQL世界里没有明显的数据库(database)和实例(instance)的区分,如果严格的按照Oracle方式来区分MySQL数据库和实例的话那么MySQL就只是一个数据库,它提供的是一个或多个存储引擎的载体,为SQL提供一个基于成本的优化器,但又不同的是它包含优化器这又区别Oracle中实例和数据库的区别。

一、InnoDB体系结构

MySQL是一个多线程数据库,那么组成InnoDB的主要就包含线程和内存了,不同线程就好比Oracle中的不同进程各司其职处理MySQL运行中的各项任务。

mysql innodb 存储方式 mysql中innodb_sql

1、后台线程(Background Thread)

1.Master Thread

Master Thread 线程是一个非常核心的后台线程,主要负责Innodb的主循环保证数据的一致性,调度所有后台线程的触发。

2.IO Thread

在 InnoDB 存储引擎中大量使用了 AIO (Async IO) 来处理写 IO 请求,这样可以极大提高数据库的性能。而 IO Thread 的工作主要是负责这些 IO 请求的回调 (call back)处理。 InnoDB 1.0 版本之前共有 4 个 IO Thread, 分别是 write、 read 、 insert buffer 和 log IO thread。从 InnoDB 1.0.x 版本开始, read thread 和 write thread 分别增大到了 4 个,并且不再使用 innodb_file_io_threads 参数,而是分别使用 innodb_read_io_threads 和 innodb_write_io_threads 参数进行设置。

3.Purge Thread

在 InnoDB 存事务被提交后,其所使用的 undo log 可能不再需要,因此需要 Purge Thread 来回收已经使用并分配的 undo 页。在 InnoDB 1.1 版本之前, purge 操作仅在 InnoDB 存储引擎的 Master Thread 中完成。而从 InnoDB 1.1 版本开始, purge 操作可以独立到单独的线程中进行,以此来减轻 Master Thread 的工作,从而提高 CPU 的使用率以及提升存储引擎的性能。用户可以在 MySQL 数据库的配置文件中添加如下命令来启用独立的 Purge

[mysqld]

innodb_purge_threads=1

4.Page Cleaner Thread

Page Cleaner Thread 负责脏页的刷新操作。而其目的是为了减轻原 Master Thread 的工作及对于用户查询线程的阻塞,进一步提高 InnoDB 存储引擎的性能。

2、内存(Memroy)

内存是所有数据库系统最为复杂的一部分,作为数据库与存储之间衔接的缓冲区,这部分的设计尤为复杂。

1.缓冲池(innodb buffer pool )

InnoDB 存储引擎是基于磁盘存储的,并将其中的记录按照页的方式进行管理,在InnoDB中允许有多个缓冲池实例,每个页根据哈希值平均分配到不同缓冲池实例中,缓冲池中的数据页类型也主要包含索引页、数据页、undo页、插入缓冲、自适应哈希索引、InnoDB存储的所信息、数据字典信息等,其中页默认大小为16KB。

innodb_buffer_pool_instances=2

InnoDB的缓冲池的结构同Oracle buffer cache中的结构类似,都是基于链表遵循LRU规则的一套内存结构,InnoDB中缓冲池的内存结构大致是这样的。

在InnoDB中同样也会有三个链表 LRU List、Free List和Flush List,

LRU List是存储热数据的主要链表,使用最频繁的也存放在LRU列表的前端,使用最少的页放在LRU列表的最尾端,但为了保证LRU 链表不至于过快的老化,在LRU LIst中有一个midpoint位置,该位置默认在LRU链表的5/8处,受innodb_old_blocks_pct参数控制,新读入的数据页插入到LRU链表的3/8处位置。

当读取大量数据页到LRU链表的midpoint位置就会造成一个很严重LRU链表老化的问题,为了解决这个问题,InnoDB引入了innodb_old_block_time参数用来控制数据页读取到midpoint位置后需要等待多久才会被加入到LRU链表的热端,用此方式老化掉大量一次读取的数据页。

InnoDB存储引擎支持压缩页的功能,可将原本16KB的数据页压缩成1KB、2KB、4KB和8KB,对于这些非16KB的页是受unzip_LRU 列表进行管理的,从存储引擎信息可以发现,LRU len包含unzip_LRU len的值,也就说明unzip_LRU链表属于LRU List的子链表,用于特殊管理非16KB数据页的链表。而对于这种非16KB的页是大致是这样被分配的,比如申请4KB内存页,首先检查4KBunzip_LRU列表,若有则直接使用,如果没有检查8KB的unzip_LRU列表,若有将8KB分成两个4KB使用,否则从LRU列表申请16KB的页,将页分成一个8KB,2个4KB,分别存放到对应的unzip_LRU列表中。

Free List是缓冲池存放空闲页的列表,可以通过show engine innodb status 中free buffers 值查看当前空闲池大小,

Flush List

LRU列表中的页被修改后,该页变成了脏页存放在Flush List,等待CHECKPOING机制将脏页刷新回磁盘

LRU列表管理缓冲池页的可用性,flush列表管理将页刷新回磁盘,互不影响

2.重做日志缓冲(redo log buffer )

顾名思义,该区域的作用和触发机制同Oracle的redo log buffer类似,用于存放事务产生的重做日志,其大小受innodb_log_buffer_size控制,默认为8MB,其触发写出log buffer的条件也通 Oracle类似

①Master Thread每秒将重做日志缓冲刷新到重做日志文件中。

②commit;

③redo log buffer 剩余空间少于1/2。

其实InnoDB在redo写出的时候还是用了group commit来合并IO,事务提交上是用了两次commit来保证事务完整性,这些特性以后在详细讨论。

3.额外的内存池(innodb additional mem pool size)

在InnoDB存储引擎中,对于内存的管理是通过内存堆(heap)的方式进行管理的,对于数据结构本身(诸如LRU、锁、等待等)的内存分配需要从额外的内存池进行申请,当该区域内存不够时会从换重置进行申请。因此该区域也相当重要

3、检查点(CHECKPOINT)

提起checkpoint大家都不陌生,它是用来定期刷新内存中脏数据到磁盘的一种机制,以保证数据成功落盘减少数据库CRASH后的RECOVER时间。InnoDB事务执行完毕将数据重做日志写入redo中通过LSN(log Sequence Number)来标记位置以便更好的找到redo位置的,它等同于Oracle的SCN,存在于所有的页当中,LSN是8字节的数字。

对于InnoDB中的checkpoint技术也同Oracle中的类似,同样也分为全量检查点和增量检查点的概念,只是在InnoDB中的叫法不同,在InnoDB中称作Sharp Checkpoint和Fuzzy Checkpoint

Sharp Checkpoint发生在数据库关闭时将所有脏页刷新回磁盘,即我们通常所说的全量检查点,innodb_fast_shutdown=1

Fuzzy Checkpoint为刷新部分脏页,即通常所说的增量检查点,对于它的触发点有以下几种情况:

①Master Thread Checkpoint

Master Thread每秒或每10秒异步触发一次Fuzzy Checkpoin。

②FLUSH_LRU_LIST Checkpoint

InnoDb存储引擎需要保证Flush LRU列表有足够的空闲页来使用,这个检查是通过Page Cleaner线程来做的,可以通过innodb_lru_scan_depth参数来控制LRU列表中可用也的数量

③Async/Sync Flush Checkpoint

当重做日志不可用强制刷新页回新盘,考虑到性能,这里在不同阈值下调用的刷新方式不同。将已经写入到redo log的LSN记为redo_lsn,将已经刷新回磁盘的最新页LSN记为checkpoint_lsn,则

checkpoint_age=redo_lsn - checkpoint_lsn

async_water_mark = 75% * total_redo_log_file_size

sync_water_mark = 90% * total_redo_log_file_size

·checkpoint_age<async_water_mark时,不需要任何刷页操作。

·async_water_mark<checkpoint_age<sync_water_mark时触发Async Flush。

·checkpoint_age>sync_water_mark时强制触发Sync Flush,冲flush列表刷新足够的脏页回磁盘,不过此情况一般很少发生,除非redo log太小

5.6版本以后此操作不在阻塞用户查询线程。

④Dirty Page too much Checkpoint

脏页太多导致InnoDB轻质进行 Fuzzy Checkpoint,其阈值受innodb_max_dirty_pages_pct控制,默认是75%。

综上。除了每秒和或每10秒Master Thread线程调用Purge Cleaner来刷脏以外,即使没有达到buffer pool 的innodb_max_dirty_pages_pct参数阈值,只要产生的redo log过多达到了async_water_mark = 75% * total_redo_log_file_size阈值也会调用Purge Cleaner来刷脏。

二、InnoDB Master Thread

Master Thread线程其实就是一个死循环内嵌多个循环组成的,包括:主循环,后台循环,刷新循环,暂停循环。Master Thread根据数据库的状态在不同的循环之间切换,其中主循环运行每秒或每10秒会操作一些动作,但这个时间会随着代码运行的速度,即数据库的复杂有所延迟。而InnoDB每一次合并插入和刷脏的页数可受innodb_io_capacity控制,根据实际生产IO来设定,默认200,合并插入数量为innodb_io_capacity*5%,从buffer pool中刷脏为innodb_io_capacity。

每一秒执行的操作:

①redo log buffer刷新回磁盘,即使事务未提交。(总是)

② 触发IO线程,合并插入缓冲(可能,前一秒IO次数大于5次才会触发,innodb_io_capacity*5%)

③触发Page Cleaner Thread 线程,最多刷新innodb_io_capacity 个Flush List中的dirty page到磁盘(可能,判断buffer pool中的比例是否超过了innodb_max_dirty_pages_pct,默认75%)innodb_lru_scan_depth

④如果没有用户活动,切换到background loop(可能)

⑤checkpoint

每10秒执行的操作:

①触发Page Cleaner Thread 线程,刷新innodb_io_capacity 个脏页到磁盘(可能,InnoDB判断过去10s之内磁盘的IO操作是否小于200次,如果是,便将innodb_io_capacity 脏页刷新到磁盘)

②触发IO线程,合并至多innodb_io_capacity*5%个插入缓冲(总是,InnoDB会合并插入缓冲)

③将日志缓冲刷新到磁盘(总是)

④触发Page Thread 线程,删除无用的undo page(总是,InnoDB执行full purge,判断undo使用,删除无用 UNDO,受innodb_purge_batch_size控制,默认20,可动态调整)

⑤触发Page Cleaner Thread 线程,刷新innodb_io_capacity个或者innodb_io_capacity*10%个脏页到磁盘(总是,判断buffer pool中的比例是否超过了innodb_max_dirty_pages_pct,超过70%刷新innodb_io_capacity个脏页,小于70%只需刷新innodb_io_capacity*10%脏页)

⑥checkpoint

background loop执行的操作(数据库空闲或数据库关闭):

①触发Page Thread 线程,删除无用undo页(总是)

②触发IO线程,合并innodb_io_capacity*5%个插入缓冲(总是)

③调回主循环(总是)

④触发Page Cleaner Thread 线程,不断刷新innodb_io_capacity个页直到符合条件(可能,跳转到flush loop)

三、InnoDB特性

InnoDB在保证高效的运行时同时还引入了一些特性来提高性能。

1、插入缓冲(Insert Buffer)

Insert Buffer是InnoDB令人兴奋的一个功能,它的存在主要是为了维护InnoDB中非聚集索引叶子节点数值在数据页中的连续性,避免离散读。InnoDB中 Insert Buffer 和数据页一样也是物理页的一个组成部分,Insert Buffer可以对DML操作(INSERT、DELETE、UPDATE)进行缓冲,在 cache pool中分别使用 Insert Buffer、Delete Buffer、Purge Buffer。在InnoDB存储引擎中,主键是行的唯一标识符,通常应用程序中杭机路的插入顺序是递增的,所以插入聚集索引(Primary Key)一般顺序的,但是在实际生产中很多时候是使用非唯一的辅助索引(secondary index),在插入操作时,数据页的存放还是按照聚集索引(Primary Key)的顺序存放的,而对于辅助索引(secondary index)不是顺序的,这时需要离散的访问非聚集索引页,导致性能下降。

Insert Buffer的使用条件:①索引是辅助索引(secondary index)②所以不是唯一(unique)的。

Insert Buffer 内部实现:

Insert Buffer实际上就是一个B+树结构,负责对所有表的辅助索引进行Insert Buffer,它存放在共享表空间中(ibdata1)。

这个B+树结构非叶子节点存放的是查询的search key(键值),它是一个9字节的数据结构,由space、marker和offset组成,其中space表示带插入记录所在表的表空间ID,它是唯一的,占用4字节,marker用来兼容老版本的Insert Buffer,占用1字节,offest表示页所在的偏移量,占用4字节。

mysql innodb 存储方式 mysql中innodb_数据库_02

对于插入到Inset Buffer B+树叶子节点的记录,也同样需要构造结构,

mysql innodb 存储方式 mysql中innodb_sql_03

它的工作方式大致是这样的:

①对于非聚集索引的插入或更新操作,首先判断非聚集索引页是否在缓冲池中,若存在直接插入,若不存在则首先按照Insert Buffer规则构造一个search key,接下来查询这个B+树,然后插入到一个Insert Buffer B+树的叶子节点中,最后InnoDB在按一定的频率进行合并。

②而对于update等操作,首先将记录标记为删除,然后在Purge Buffer中将记录真正删除,最后在执行insert操作

Merge Insert Buffer

对于已经插入到Insert Buffer B+树中的记录什么时候进行合并(merge)呢,概括的说合并可能发生在以下情况下:

①辅助索引页被读取到cache pool中,当进行查询操作时,需检查Insert Buffer Bitmap页,确认辅助索引页是否有记录存放在insert Buffer B+树中,若有,则将Insert Buffer B+树中该页的记录插入到该辅助索引页中。

②Insert Buffer Bitmap页追踪辅助索引页无空间,Insert Buffer Bitmap页追踪每个辅助索引页可用空间,最少有1/32页的空间,若不够会强制进行合并操作。

③Master Thread,每秒或每10秒进行一次Merge Insert Buffer的操作,不同之处在于每次进行merge操作的页的数量不同,根据srv_innodb_io_capactiy的百分比来决定要合并辅助索引页的数量。

2、两次写(Double Write)

二次写是MySQL为了保证数据完整性引入的另一个特性,要知道MySQL中默认大小为16KB,而Linux操作系统数据块大小为4KB,那么在MySQL数据写入操作系统硬盘的时候就会出现一个这样的问题,一个页的数据要分多次写入到操作系统中,如果这个过程数据库crash,那么会有部分页写入不完整,而MySQL InnoDB redo记录的又是数据块的物理位置(即数据块的偏移量offset)此时又无法使用redo来恢复,造成数据丢失。基于此问题MySQL引入了二次写特性,即所有的写操作先通过memcpy函数将内存中脏页写入cache pool中的Double Write buffer(2M)中,然后通过doublewrite buffer分两次(每次1M)顺序的写入共享表空间物理磁盘中(Double Write物理区域),最后马上调用fsync函数,同步数据到数据盘数据页位置。

Innodb_buffer_pool_pages_flushed变量代表当前从缓冲池刷新到磁盘页的数量,刷新的数量可根据Innodb_dblwr_pages_written来进行统计。

参数skip_innodb_doublewrite可以禁用doublewrite功能,此时可能会发生写失效的问题。

mysql innodb 存储方式 mysql中innodb_sql_04

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

哈希(hash)是一种非常快的查找方式,在InnoDB中B+ Tree的查找次数取决于B+ Tree的高度,在生产中高度一般为3-4层,那么就需要3-4次查询。

自适应哈希索引(Adaptive Hash Index,AHI):InnoDB存储引擎中会监控表上各索引页的查询,若监控到哈希索引可以带来速度提升,则建立哈希索引。

AHI是通过缓冲池的B+树页构造的,建立速度很快,而且不需要对整张表建立哈希索引。InnoDB存储引擎会根据访问的频率和模式来自动的为某些热点页建立哈希索引。

AHI的构造要求:

①对某个页以一种模式连续访问100次以上。同一种模式指的是查询的条件一样,即where条件一致

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

官方文档对于AHI的说明是启用该功能之后效率提升一倍,可通过innodb_adaptive_hash_index来启用或禁用此特性,默认开启。

4、异步IO(Async IO)

异步IO是对应Sync IO来说的,旨在提升磁盘操作性能,减少IO写入的阻塞等待操作。

Sync IO:就是每一次IO操作就需要此操作结束才能继续接下来的操作,结果可想而知,没有那么多IO操作时需要等待上一个IO结束在进行下一步的,大大降低了数据库性能。

Async IO(AIO):就是用户发出一个IO请求之后立即在发出另一个IO请求,当全部IO请求发送完毕,等待所有IO操作的完成。它的优势就在于可以进行IO Merge操作,即将多个IO合并成1个IO,提高IOPS。InnoDB对于AIO提供了内核级别的支持,成为Native AIO,因此在编译和运行MySQL时需要libaio库的支持,同时也需要操作系统对Native AIO 的支持。innodb_use_native_aio参数可以控制Native AIO的开启,Linux中默认开启,InnoDB存储引擎中 read ahead、刷新脏页等磁盘写入操作全部使用AIO方式完成,官方测试恢使用Native AIO恢复速度提高75%。

5、临近页刷新(Flush Neighbor Page)

临近页刷新(Flush Neighbor Page)是InnoDB提供的另一个特性,其工作原理是:当刷新一个脏页时,InnoDB存储引擎会检测该页所在区(extent)的所有页,如果是脏页则一并刷新。此特性可以使用AIO将多个IO操作合并成一个IO操作,在传统机械硬盘有显著的提升。但是在当今固态硬盘有着较高的IOPS,并且对于有些extent中的page可能存在刷脏之后又迅速变为脏页,引发二次刷脏等问题,故InnoDB提供了innodb_flush_neighbors参数控制是否开启此特性,IOPS较高的硬盘建议关闭此特性。