最近在学习《Mysql技术内幕:InnoDB 存储引擎》一书,将第二章关于 InndoDB 存储引擎的概述及相关特性总结如下:
以下是 InnoDB 存储引擎主要几个关键特性,后面会做一一详解:
InnoDB 是多线程模型,主要分为以下三个线程:
Master Thread 是最高的线程优先级别,其内部由多个循环组成:主循环/后台循环/刷新循环/暂停循环,会在这多个循环之间切换。
- 主循环(loop)主要以每一秒和每十秒的频率执行刷新日志缓存,合并插入缓存,刷新脏页缓存,删除无用 undo 页等操作
- 如果当前没有用户活动,则进入后台循环流程(backgroud loop),主要执行删除无用的 undo 页,合并插入缓存
- 如果没有什么事情可以做了,便进入了暂停循环(suspend loop),等待事件循环唤起
主要用于对于异步 IO 请求的处理结果的回调处理,包括 write/read/insert buffer/log 四种 IO 线程,其中 write/read 线程的个数可以通过参数 innodb_read_io_threads 和 innodb_write_io_threads 参数进行调节。
回收已经使用并分配的 undo 页,从 Master Thread 线程中分离出来,提高 CPU 的利用率。innodb 1.2 版本以后,可以通过参数 innodb_purge_threads 来设置 Purge 线程的个数
负责脏页的刷新,innodb 1.2 版本以后从 Master Thread 线程中分离出来,提高 CPU 的利用率,减轻 Master Thread 的压力。
为了提高数据库的性能,引入缓存池的概念,通过参数 innodb_buffer_pool_size 可以设置缓存池的大小,参数 innodb_buffer_pool_instances 可以设置缓存池的实例个数。缓存池主要用于存储以下内容:
缓存分配管理:
LRU 列表只存放索引缓存和数据缓存,其它插入缓存,哈希索引等是不会被存在 LRU 列表中的
innoDB 中缓存池中页的大小默认是 16KB,使用 LRU 算法对缓存池进行管理,在普通 LRU 算法的基础上做了优化,加入 midpoint 位置,每次并不是直接插入到 LRU 列表头部,而是放在 midpoint 位置,默认情况下是在 列表的 5/8 处,可以通过参数 innodb_old_blocks_pct 设置 midpoint 的位置。midponit 之前的列表是 new 列表,存放的活跃的热点数据,midpoint 之后的列表是 old 列表。当该页面被再次访问时,便将改页面移动到列表头部。但是我们可以通过参数 innodb_old_blocks_time 设置只有超过一定时间的 old 列表中的数据才会被移动到热点数据。
LRU 列表中的页面被修改后,称之为脏页,数据库会通过 CHECKPOINT 机制将脏页刷新到磁盘,而 Flush 便是脏页的存放列表。脏页既存储在 LRU 列表中,也存放在 Flush 列表中。
数据库刚启动的时候, LRU 列表和 Flush 列表都是空的,所有的页都存放在 Free 列表中,需要分配 LRU 页的时候,Free 列表不为空的情况下,从 Free 列表中获取,否则淘汰 LRU 尾部的列用于分配新的页。
- Master Thread 会每秒进行刷新
- 每个事务提交时会刷新
- 重做日志缓存空间小于 1/2 时会强制刷新
Checkpoint 技术主要是用于将缓存区的脏页刷新到磁盘,及时将缓存脏页数据刷新到磁盘,有以下好处:
以下四种情况下会执行刷新脏页到磁盘:
Master Thread 会以每一秒或者每十秒的速度将一定比例的脏页刷新到磁盘
至少要保证 LRU 列表中有 100 个空闲页可供使用,如果小于 100 个空闲页的时候,会将列表末端的页移除,如果有脏页则刷新
当重做日志不可用的情况下,会强制将一些脏页刷新到磁盘
当脏页的数量太多,导致 innoDB 存储引擎强制 Checkponit,通过参数 innodb_max_dirty_pages_pct 控制,默认是 75%
聚焦索引:
- 聚集索引按照主键递增顺序连续插入,是唯一的,一般是顺序存放,不需要磁盘的随机读取
- 如果聚集索引插入了指定值而非 NULL 值,可能导致插入并非连续的情况
- 非聚集索引是指不是唯一的索引,B+ 树的存放特性决定了其插入的离散型
Insert Buffer 用于非聚集索引的插入和更新操作。先判断插入的非聚集索引是否在缓存池中,如果在则直接插入,否则插入到 Insert Buffer 对象里。再以一定的频率进行 Insert Buffer 和辅助索引叶子节点的 merge 操作,将多次插入合并到一个操作中,提高对非聚集索引的插入性能。可以通过 innodb_change_buffer_max_size 参数设置 Insert Buffer 的最大内存使用量,默认是 1/4,最大可以设置为 1/2,如果 Insert Buffer 内存太大的情况下会影响其它缓存的内存使用。
使用 Insert Buffer 要满足以下两个条件:
- 索引是辅助索引
- 索引不唯一,如果索引唯一还要去查找索引页进行检查唯一性,就失去了 Insert Buffer 离散插入的性能
Insert Buffer 中的记录何时被插入到真正的辅助索引页(Merge Insert Buffer)中:
- 当辅助索引页被读取到缓存池时,检查 Insert Buffer Bitmap 页是否有该页对应的 Insert Buffer,有则合并。
- 当每个辅助索引页的可用空间小于 1/32 时,强制进行一次合并操作。
- Master Thread 线程会每秒或者每 10 秒进行一次 Merge Insert Buffer。
Insert Buffer 的内部实现:
- Insert Buffer 存放在一颗 B+ 树中,非叶子节点存放的是search key(表空间,页的偏移量等信息),叶子节点存放的真正的记录
- Insert Buffer Bitmap 用来标记每个辅助索引页的可用空间以及该页是否已经在缓存中等信息,这样可以保证保证每次 Merge Insert Buffer 成功
如果数据库发生宕机时,可以通过重做日志对该页进行恢复,但是如果该页本身已经损坏了,进行重做恢复是没有意义的。因此引入了"二次写"方案,提高数据页的稳定性。
doublewrite 的整个流程如下:
InnoDB 会监控对表上各个索引页的查询,如果观察到通过哈希索引可以带来性能提升,则自动建立哈希索引。AHI 通过缓存池的 B+ 树页构造而来,因此建立速度很快。
对于(a, b)这样的联合索引,访问模式有以下两种:
1. where a = xxx
2. where a = xxx and b = xxx
为了提高磁盘操作的性能, InnoDB 存储引擎采用异步 IO(AIO)的方式来处理磁盘操作:
- 0 表示完成所有的 pull merge 和 merge insert buffer,并将脏页刷新到磁盘操作以后再关闭
- 1 表示只完成将脏页刷新到磁盘即关闭
- 2 只是将日志写入文件就关闭数据库,下次启动时可以根据 redo log 进行恢复