[InnoDB 学习1] 体系架构

 

MySQL体系结构

Management
Service &
Utillties
管理服务和工具组件
Connection Pool
连接池
SQL Interface
SQL接口
Parser Quary Translation
查询分析器
Optimizer
优化器
Caches & Buffers
缓冲
Pluggable Storage Engines
插件式存储引擎
MyISAM InnoDB NDB Archive Federated Memory Merge Partner Community Custom +
File system
物理文件

注:存储引擎是基于表的,而不是数据库。

InnoDB存储引擎简介

InnoDB存储引擎支持事务,其设计目标主要面向在线事务处理(OLTP)的应用。其特点是行锁设计支持外键,并支持非锁定读,即默认读取操作不会产生锁。

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存储引擎体系架构

---------------------------------------
|后台线程 后台线程 后台线程 后台线程 后台线程|
|                                     |
|            InnoDB存储引擎内存池        |
---------------------------------------
                   ^
                   |
                   v
            文件   文件   文件

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

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

后台线程的主要作用是负责刷新内存池中的数据,保证缓冲池中的内存缓存的是最近的数据。

此外将已修改的数据文件刷新到磁盘文件,同时保证在数据库发生异常的情况下InnoDB能恢复到正常运行状态。

后台线程

Master Thread

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

IO Thread

引擎使用大量AIO(Async IO)来处理IO请求,这样可以极大提高数据库的性能。

IO Thread的主要工作是负责这些IO请求的回调(call back)处理。有4类IO Thread,分别是writereadinsert bufferlog IO thread。(InnoDB 1.0.x,read threadwrite thread增多到4个,可调。)

mysql> show variables like 'innodb_%io_threads';
+-------------------------+-------+
| Variable_name           | Value |
+-------------------------+-------+
| innodb_read_io_threads  | 4     |
| innodb_write_io_threads | 4     |
+-------------------------+-------+
mysql> show engine innodb status;
--------
FILE I/O
--------
I/O thread 0 state: waiting for completed aio requests (insert buffer thread)
I/O thread 1 state: waiting for completed aio requests (log thread)
I/O thread 2 state: waiting for completed aio requests (read thread)
I/O thread 3 state: waiting for completed aio requests (read thread)
I/O thread 4 state: waiting for completed aio requests (read thread)
I/O thread 5 state: waiting for completed aio requests (read thread)
I/O thread 6 state: waiting for completed aio requests (write thread)
I/O thread 7 state: waiting for completed aio requests (write thread)
I/O thread 8 state: waiting for completed aio requests (write thread)
I/O thread 9 state: waiting for completed aio requests (write thread)

Purge Thread

事务提交后,其所使用的undolog可能不再需要,因此需要Purge Thread回收已使用并分配的undo页

之前purge操作仅在Master Thread中完成。新版本后,purge操作可以独立到单独的线程中进行,以此减轻Master Thread的工作,从而提高CPU的使用率以及提升存储引擎的性能。

Page Cleaner Thread

作用是将之前版本中脏页的刷新操作都放入到单独线程中来完成。

目的是为了减轻原Master Thread的工作及对用户查询线程的阻塞,进一步提高InnoDB存储引擎的性能。

内存

缓冲池

InnoDB存储引擎是基于磁盘存储的,并将其中的记录按照页的方式进行管理

在数据库系统中,由于CPU速度与磁盘速度之间的鸿沟,基于磁盘的数据库系统通常使用缓冲池技术来提高数据库的整体性能。

原理:

缓冲池就是一块内存区域,通过内存的速度来弥补磁盘速度较慢对数据库性能的影响。

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

写:对于数据库中页的修改操作,则首先修改在缓冲池中的页,然后再以一定频率刷新到磁盘上。(注:页从缓冲池刷新回磁盘的操作并不是在每次页发生更新时触发,而是通过一种称为Checkpoint的机制刷新回磁盘。这是为了提高数据库的整体性能。)

缓冲池中缓存的数据页类型有:索引页、数据页、undo页、插入缓冲(insert buffer)、自适应哈希索引(adaptive hash index)、InnoDB存储的锁信息(lock info)、数据字典信息(data dictionary)等。

新版本允许有多个缓冲池实例。每个页根据哈希值平均分配到不同缓冲池实例中。这样做的好处是减少数据库内部的资源竞争,增加数据库的并发处理能力。

LRU List、Free List、Flush List

LRU List

缓冲池是一个很大的内存区域,其中存放各种类型的页。那么InnoDB存储引擎是怎么对这么大的内存区域进行管理的呢?

通常,数据库中的缓冲池是通过LRU(Latest Recent Used,最近最少使用)算法来进行管理的。即最频繁使用的页在LRU列表的前端,而最少使用的页在LRU列表的尾端。当缓冲池不能存放新读取到的页时,将首先释放LRU列表中尾端的页。

不同的是InnoDB存储引擎对传统的LRU算法做了一些优化。在LRU列表中还加入了midpoint位置。新读取到的页,虽然是最新访问的页,但并不是直接放入到LRU列表的首部,而是放入到LRU列表的midpoint位置。默认配置下,该位置在LRU列表长度的5/8(37%)处。

mysql> show variables like 'innodb_old_blocks_pct';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| innodb_old_blocks_pct | 37    |
+-----------------------+-------+

缓冲池中页的大小默认为16KB

新版本支持压缩页,即将原本16KB的页压缩为1KB、2KB、4KB、8KB。对于非16KB的页,是通过unzip_LRU列表管理的。

通过伙伴算法进行内存的分配。先拆分unzip_LRU列表,再拆分LRU列表。

为什么不采用朴素的LRU算法,直接将读取的页放入到LRU列表的首部呢?

因为若直接将读取到的页放入到LRU的首部,有些操作需要访问表中的许多页,甚至全部页,而这些页通常仅在这次查询操作中需要,并不是活跃的热点数据。如果放入LRU列表首部,很可能热点数据被从LRU列表中移除,而下一次需要时,需要再次访问磁盘。

解决:

InnoDB存储引擎引入了另一个参数进一步管理LRU列表,用于表示页读取到mid位置后需要等待多久才会被加入到LRU列表的热端。

当执行一些SQL操作时,要尽可能是LRU列表中的热点数据不被刷出。

mysql> show variables like 'innodb_old_blocks_time';
+------------------------+-------+
| Variable_name          | Value |
+------------------------+-------+
| innodb_old_blocks_time | 1000  |
+------------------------+-------+

查看缓冲池运行状态:

mysql> select pool_id, hit_rate, pages_made_young, pages_not_made_young from information_schema.innodb_buffer_pool_stats;

pages_made_young显示LRU列表中页移动到前端的次数。 hit_rate值不应该小于95%,否则,需要检查是否是由于全表扫描引起的LRU列表被污染的问题。

Free List

Free列表用处:

LRU列表用来管理已经读取的页,但当数据库刚启动时,LRU列表是空的,即没有任何的页。这时页都存放在Free列表中。

当需要从缓冲池中分页时,首先从Free列表中查找是否可用的空闲页,若有则将该页从Free列表中删除,放入到LRU列表中。

根据LRU算法,淘汰LRU列表末尾的页,将该内存空间分配给新的页。

Flush List

Flush列表作用:

在LRU列表中的页被修改后,称该页为脏页(dirty page),即缓冲池中的页和数据页产生了不一致。

这时数据库会通过CHECKPOINT机制将脏页刷新回磁盘,而Flush列表中的页即为脏页列表。

注:脏页即存在于LRU列表,也存在于Flush列表中。

重做日志

作用

InnoDB存储引擎的内存区域除了有缓冲池外,还有重做日志缓冲(redo log buffer)。

InnoDB存储引擎首先将重做日志信息先放入到这个缓冲区,然后按一定频率将其刷新到重做日志文件。

重做日志缓冲一般不需要设置很大,因为每秒都会将重做日志缓冲刷新到日志文件,因此用户只需要保证每秒产生的事务量在这个缓冲大小之内即可。(默认为16MB)

mysql> show variables like 'innodb_log_buffer_size';
+------------------------+----------+
| Variable_name          | Value    |
+------------------------+----------+
| innodb_log_buffer_size | 16777216 |
+------------------------+----------+

什么时候刷新到磁盘?

以下3种情况会将重做日志缓冲中的内容刷新到磁盘的重做日志文件中:

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

额外的内存池

在对一些数据结构本身的内存进行分配时,需要从额外的内存池中进行申请,当该区域的内存不够时,会从缓冲池中进行申请。

Checkpoint技术

缓冲池的设计目的是为了协调CPU速度与磁盘速度的鸿沟。因此页的操作首先都是在缓冲池中完成的。如果一条DML语句,如Update/Delete改变了页中的记录,那么此时页是脏的,即缓冲池中的页版本要比磁盘的新。数据库需要将新版本的页从缓冲池刷新到磁盘。

若每次一个页发生变化,就将新页刷新到磁盘,那么这个开销是非常大的。

同时,如果在从缓冲池将页的新版本刷新到磁盘时发生了宕机,那么数据就不能恢复了。

为了避免发生数据丢失的问题,当前事务数据库系统普遍采用了Write Ahead Log策略,即当事务提交时,先写重做日志,再修改页。当由于发生宕机而导致数据丢失时,通过重做日志来完成数据的恢复。(这也是事务ACID中D(Durability持久性)的要求)

为什么需要Checkpoint技术?

重做日志无限增大,可以还原整个数据库,但成本高,不便运维。

因此Checkpoint技术的目的是解决以下几个问题:

  • 缩短数据库的恢复时间。
  • 缓冲池不够用时,将脏页刷新到磁盘。
  • 重做日志不可用时,刷新脏页。

当数据库发生宕机时,数据库不需要重做所有的日志,因为Checkpoint之前的页都已经刷新回磁盘。故只需要对Checkpoint后的重做日志进行恢复。

当缓冲池不够用时,根据LRU算法会溢出最近最少使用的页,若此页为脏页,那么需要强制执行Checkpoint,将脏页的新版本刷回磁盘。

重做日志出现不可用的情况是因为当前事务数据库系统对重做日志的设计是循环使用的。若重做日志需要使用,那么必须强制产生Checkpoint,将缓冲池中的页至少刷新到当前重做日志的位置。

对于InnoDB存储引擎而言,其是通过LSN(Log Sequence Number)来标记版本的。LSN是8字节的数字,每个页有LSN,重做日志中也有LSN,Checkpoint也有LSN。

mysql> show engine innodb status \G;
---
LOG
---
Log sequence number 2660223
Log flushed up to   2660223
Pages flushed up to 2660223
Last checkpoint at  2660214

何时触发?

Checkpoint发生的时间、条件及脏页的选择等都非常复杂。

而Checkpoint所做的事情无外乎是将缓冲池中的脏页刷回磁盘。不同之出在于每次刷新多少页到磁盘,每次从哪里取脏页,以及什么时间触发Checkpoint。

在InnoDB存储引擎内部,有2种Checkpoint,分别是:

  • Sharp Checkpoint
  • Fuzzy Checkpoint

Sharp Checkpoint

发生在数据库关闭时,将所有的脏页都刷新回磁盘。(默认)

Fuzzy Checkpoint

若数据库在运行时也使用Sharp Checkpoint,那么数据库的可用性就会受到很大的影响。

故在InnoDB存储引擎内部使用Fuzzy Checkpoint进行页的刷新,即只刷新一部分脏页,而不是刷新所有的脏页回磁盘。

以下是集中可能发生的Fuzzy Checkpoint:

  • Master Thread Checkpoint:差不多以每秒/每十几秒的素多从缓冲池的脏页列表中刷新一定比例的页回磁盘。(异步
  • FLUSH_LRU_LIST Checkpoint:是因为InnoDB存储引擎需要保证LRU列表中需要有差不多100个空闲页可供使用。若没有100个可用空闲页,那么InnoDB存储引擎会将LRU列表尾端的页移除。如果这些页中有脏页,那么需要进行Checkpoint。(单独的Page Cleaner线程中进行)
    mysql> show variables like 'innodb_lru_scan_depth';
    +-----------------------+-------+
    | Variable_name         | Value |
    +-----------------------+-------+
    | innodb_lru_scan_depth | 1024  |
    +-----------------------+-------+
    
  • Async/Sync Flush Checkpoint:重做日志不可用时,需要强制将一些页刷新回磁盘。(保证重做日志的循环使用的可用性。)(单独的Page Cleaner线程中进行)
  • Dirty Page too much Checkpoint:脏页数量太多,必须强制进行Checkpoint。(主要目的是为了保证缓冲池中有足够可用的页。)
    mysql> show variables like 'innodb_max_dirty_pages_pct';
    +----------------------------+-----------+
    | Variable_name              | Value     |
    +----------------------------+-----------+
    | innodb_max_dirty_pages_pct | 75.000000 |
    +----------------------------+-----------+
    

Master Thread

具有最高的线程优先级别。

由多个循环组成:(Master Thread会根据数据库运行状态在其中切换)

  • 主循环(loop)
  • 后台循环(backgroud loop)
  • 刷新循环(flush loop)
  • 暂停循环(suspend loop)

主循环(loop)

主要是2大部分操作:每秒的操作每10秒的操作

通过thread sleep实现,时间并不精确,负载大可能会延迟。

每秒的操作:

  • 日志缓冲刷新到磁盘,即使这个事务还没有提交。(总是)
  • 合并插入缓冲。(可能,IO压力小时)
  • 至多刷新100个InnoDB的缓冲池中的脏页到磁盘。(可能,脏页比率大时)
  • 如果没有用户活动,则切换到background loop。(可能)

注:总是将日志缓冲的内容刷新到重做日志,这可以解释为什么再大的事务提交(commit)时间也是很短的。

每10秒操作:

  • 刷新100个脏页到磁盘。(可能,IO压力小时)
  • 合并至多5个插入缓冲。(总是)
  • 将日志缓冲刷新到磁盘。(总是)
  • 删除无用的Undo页。(总是)
  • 刷新100个/10个脏页到磁盘。(总是)

[伪代码]

/* 1s */
for (int i=0; i<10; i++){
    thread_sleep(1);
    // do log buffer flush to disk
    if (last_one_second_ios < 5)
        // do merge at most 5 insert buffer
    if (buf_get_modified_ratio_pct > innodb_max_dirty_pages_pct)
        // do buffer pool flush 100 dirty page
    if (no_user_activity)
        goto backgroup_loop;
}

/* 10s */
if (last_ten_sencond_ios < 200)
    // do buffer pool flush 100 dirty page
// do merge at most 5 insert buffer
// do log buffer flush to disk
// do full purge
if (buf_get_modified_ratio_pct > 70%)
    // do buffer pool flush 100 dirty page
else
    // buffer pool flush 10 dirty page

注:新版本改善了硬编码的问题,改用百分比控制。

后台循环(backgroud loop)

当没有用户活动,或数据库关闭时,就会切换到这个循环。

操作:

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

flush loop中没事做,就会切换至suspend loop,将Master thread挂起,等待事件的发生。


InnoDB关键特性

  • 插入缓冲(Insert Buffer)
  • 两次写(Double Write)
  • 自适应哈希索引(Adaptive Hash Index)
  • 异步IO(Async IO)
  • 刷新领近页(Flush Neighbor Page)

插入缓冲(Insert Buffer)

Insert Buffer

作用

背景:

通常应用程序中行记录的插入顺序是按照主键递增的顺序进行插入的。因此,插入聚集索引(Primary Key)一般是顺序的,不需要磁盘的随机读取。

但并不是所有的主键都是顺序的,例如UUID。那么插入和辅助索引一样,同样是随机的。即使主键是自增类型,但插入的是指定的值,而不是NULL值(手动指定主键值),那么同样可能导致插入非连续的情况。

不可能每张表上只有一个聚集索引,更多情况下,一张表上有多个非聚集的辅助索引(secondary index)。(比如,用户需要按照name字段进行查找,且这个字段不是唯一的)

这种情况下,产生了一个非聚集的且不是唯一的索引。在进行插入操作时,数据页的存放还是按主键进行顺序存放,但是对于非聚集索引叶子节点的插入不再是顺序的了。这时就需要离散地访问非聚集索引页,由于随机读取的存放而导致了插入操作性能下降。这是由于B+树的特性决定了非聚集索引插入的离散性。

某些情况下,辅助索引的插入也是顺序的,如时间。

解决:

InnoDB存储引擎开创行地设计了Insert Buffer对于非聚集索引的插入/更新操作,不是每一次直接插入到索引页中,而是先判断插入的非聚集索引页是否在缓冲池中。若在,则直接插入;若不在,则先放入到一个Insert Buffer对象中。

类似欺骗,数据库这个非聚集的索引已经插到叶子节点,而实际并没有,只是存放在另一个位置。然后再以一定的频率和情况进行Insert Buffer和辅助索引页子节点的merge(合并)操作。

因为,这时通常能将多个插入合并到一个操作中(因为在一个索引页中),这就大大提高了对于非聚集索引插入的性能。

使用条件

需要同时满足2个条件:

  • 索引是辅助索引(secondary index)。
  • 索引不是唯一的(unique)。

其实,条件与主键相对。

为什么辅助索引不能是唯一的?

因为在插入缓冲时,数据库并不去查找索引页来判断插入的记录的唯一性。如果去查找肯定又会有离散读取的情况发生,从而导致Insert Buffer失去了意义。

Change Buffer

Change Buffer可视为Insert Buffer的升级。有了这个后的新版本,可以对DML操作:INSERT、DELETE、UPDATE都进行缓冲。分别是:Insert Buffer、Delete Buffer、Purge Buffer。

注:和Insert Buffer一样,Change Buffer适用的对象依然是非唯一的辅助索引

对一条记录进行UPDATE操作分为2步:

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

Delete Buffer对应UPDATE操作的第一个过程。

Purge Buffer对应UPDATE操作的第二个过程。

mysql> show variables like 'innodb_change_buffer_max_size';
+-------------------------------+-------+
| Variable_name                 | Value |
+-------------------------------+-------+
| innodb_change_buffer_max_size | 25    |
+-------------------------------+-------+

25%表示Change Buffer使用最多1/4的缓冲池内存空间。

Insert Buffer的内部实现

Insert Buffer的数据结构是一棵B+树。(全局只有一棵,以前是每张表有一棵)

这棵B+树存放在共享表空间中,默认就是ibdata1中。

因此,试图通过独立表空间ibd文件恢复表中数据时,往往会导致CHECK TABLE失败。这是因为表的辅助索引中的数据可能还在Insert Buffer中,也就是共享表空间中。所以通过ibd文件进行恢复后,还需要进行REPAIR TABLE操作来重建表上所有的辅助索引。

Insert Buffer是一棵B+树,因此其也由叶节点和非叶节点组成。非叶节点存放的是查询的search key(键值)。

(search key结构如下)

  • space marker offset

search key一共占用9个字节,其中space(占4个字节)表示待插入记录所在表的表空间id。在InnoDB存储引擎中,每个表有一个唯一的space id,可以通过space id查询得知是哪张表。market(占1个字节)用来兼容老版本的Insert Buffer。offset(占4个字节)表示页所在的偏移量。

当一个辅助索引要插入到页(space, offset)时,如果这个页不在缓冲池中,那么InnoDB存储引擎首先根据上述规则构造一个search key,接下来查询Insert Buffer这棵B+树,然后再将这条记录插入到Insert Buffer B+树的叶子节点中。

对于叶节点的记录的插入,需要进行一定的规则进行构造。

  • space marker offset metadata        

metadata:记录每条记录进入Insert Buffer的顺序等。

额外还有Insert Buffer Bitmap页,用来追踪16384个辅助索引页,也就是256个区(Extent)。每个Insert Buffer Bitmap页都在16384个页的第二个页中。

用于:

  • 记录该辅助索引页的可用空间数量;
  • 记录该辅助索引页是否有记录缓存在Insert Buffer B+树中;
  • 该页是否为Insert Buffer B+树的索引页。

Merge Insert Buffer

Insert Buffer中的记录是何时合并(merge)到真正的辅助索引中的呢?

  • 辅助索引页被读取到缓冲池时。

    例如:这在执行正常的SELECT查询操作,此时需要检查Insert Buffer Bitmap页,然后确认该辅助索引页是否有记录存放于Insert Buffer B+树中。若有,则将Insert Buffer B+树中该页的记录插入到该辅助索引页中。

  • Insert Buffer Bitmap页追踪到该辅助索引页已无可用空间时。
  • Master Thread。

Master Thread中,是根据怎样的算法得知需要合并的辅助索引页?

随机选择Insert Buffer B+树的一个页。该算法在复杂情况下,有更好的公平性。

两次写(Double Write)

-------------------------------------
|                         内存实例   |
| ------ copy  -------------------- |
| |Page| --->  |                  | |
| ------  1    |                  | |
|              |doublewrite buffer| |
| ------ copy  |                  | |
| |Page| --->  |                  | |
| ------       -------------------  |
|                 /              \  |
-------------------------------------
                /                   \
          write/                      \ write
            2 /                         \ 3
             v                            v
--------------------------------          -----------------
| -------------  ------------- |          |                |
| |doublewrite|  |doublewrite| | recovery |    数据库文件    |
| |    (1MB)  |  |    (1MB)  | |--------->|     (.ibd)    |
| -------------  ------------- |    4     |                |
|                    共享表空间  |          ------------------
--------------------------------

double write由两部分组成:一部分是内存中的double write,大小为2MB; 另一部分是物理磁盘上的共享表空间中连续的128个页(即2个区extent),大小同样为2MB。

  1. 在对缓冲池的脏页进行刷新时,并不直接写磁盘,而是会通过memcpy函数将脏页先复制到内存中的double write buffer
  2. 之后通过double write buffer再分两次,每次1MB顺序地写入共享表空间的物理磁盘上,然后马上调用fsync函数,同步磁盘,避免缓冲写带来的问题。

    这个过程中,因为double write页是连续的,因此这个过程是顺序写的,开销并不是很大。

  3. 在完成double write页的写入后,再将double write buffer中的页写入各个表空间文件中,此时的写入则是离散的。
  4. 故障时若操作系统在将页写入磁盘的过程中发生了崩溃,在恢复过程中,InnoDB存储引擎可以从共享表空间中double write中找到该页的一个副本,将其复制到表空间文件,再应用重做日志。
mysql> show global status like 'innodb_dblwr%';
+----------------------------+---------+
| Variable_name              | Value   |
+----------------------------+---------+
| Innodb_dblwr_pages_written | 6325194 |
| Innodb_dblwr_writes        | 100399  |
+----------------------------+---------+

注:若2个写入页的比例远小于64:1,可说明系统写入压力并不是很高。

注:有些文件系统本身就提供了部分写失效的防范机制,如ZFS。此时就不需要启用double write了。

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

B+树的查找次数,取决于B+树的高度,在生产环境中,B+树的高度一般为3~4层,故需要3~4次的查询。

InnoDB存储引擎会监控对表上各索引页的查询。如果观察到建立哈希索引可以带来速度提升,则建立哈希索引,称之为自适应哈希索引。(Adaptive Hash Index, AHI)

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

AHI的要求

  • 要求对这个页的连续访问模式必须是一样的。

    例如:where a=xxxwhere a=xxx and b=xxx,访问模式一样是指查询的条件一样,若交替进行上述2种查询,那么InnoDB存储引擎是不会对该页构造AHI。

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

注:哈希索引只能用来搜索等值的查询(如:select * from table where index_col='xxx'),对于其它查找类型,如范围查找,是不能使用哈希索引的。

官方文档显示,启用AHI后,读取和写入速度可以提高2倍,辅助索引的连接操作性能可以提高5倍。

mysql> show engine innodb status \G;
-------------------------------------
INSERT BUFFER AND ADAPTIVE HASH INDEX
-------------------------------------
Ibuf: size 1, free list len 0, seg size 2, 0 merges
merged operations:
 insert 0, delete mark 0, delete 0
discarded operations:
 insert 0, delete mark 0, delete 0
Hash table size 34679, node heap has 0 buffer(s)
Hash table size 34679, node heap has 0 buffer(s)
Hash table size 34679, node heap has 0 buffer(s)
Hash table size 34679, node heap has 0 buffer(s)
Hash table size 34679, node heap has 0 buffer(s)
Hash table size 34679, node heap has 0 buffer(s)
Hash table size 34679, node heap has 0 buffer(s)
Hash table size 34679, node heap has 0 buffer(s)
0.00 hash searches/s, 0.00 non-hash searches/s

non-hash searches/s是指,范围查找时,不能使用哈希索引的次数。

异步IO(Async IO)

AIO的一个优势是,可以进行IO Merge操作(将多个IO操作合并为1个),可以提高IOPS的性能。

例如:

用户需要访问页的(space, page_no)为:(8,6), (8,7), (8,8)。

每个页的大小为16KB,那么同步IO需要进行3次IO操作,而AIO会判断这3个页是连续的(显然可以(space, page_no)得知)。因此AIO底层会发送一个IO请求,从(8,6)开始,读取48KB的页。

mysql> show variables like 'innodb_use_native_aio';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| innodb_use_native_aio | ON    |
+-----------------------+-------+

InnoDB存储引擎中,如,预读,脏页的刷新,等磁盘的写入操作全部由AIO完成。

官方测试显示,启用Native AIO,恢复速度可以提高75%。

刷新领近页(Flush Neighbor Page)

原理:

当刷新一个脏页时,InnoDB存储引擎会检测该页所在区(extent)的所有页,如果是脏页,那么一起进行刷新。

这样的好处是,通过AIO可以将多个IO写入操作合并为一个IO操作,故该工作机制在传统机械磁盘下有显著的优势

对于固态硬盘建议关闭此特性。(参数:innodb_flush_neighbors)


参考: 《MySQL技术内幕:InnoDB存储引擎》