《MySql技术内幕 InnoDb存储引擎》学习笔记【四 InnoDB存储引擎】
目录
四 InnoDB存储引擎
InnoDB存储引擎是事务安全的MySql存储引擎,从MySql5.5版本开始成为默认的表存储引擎,是第一个完整支持ACID事务的MySql存储引擎,其特点是行锁设计、支持MVCC、支持外键、提供一致性非锁定读。
(一)InnoDB体系架构
InnoDB存储引擎是一个多线程的的模型,由多个后台线程和内存池共同组成,后台线程的主要作用是负责刷新内存池中的数据,保证缓冲池中的内存缓存的是最近的数据。此外将已修改的数据文件刷新到磁盘文件中,并保证数据库在发生异常情况时能够恢复到正常状态。
1 后台线程
(1)Master Thread
Master Thread是一个非常核心的后台线程,主要负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性,包括脏页的刷新、合并插入缓冲、UNDO页回收等。
(2)IO Thread
InnoDB中大量使用了AIO(Async IO)来处理写IO请求,极大提高了数据库的性能。IO Thread的工作主要是负责这些IO请求的回调处理。
InnoDB 1.0版本之前共有4个IO Thread,分别是write、read、insert buffer和log IO thread。
在1.0.x 版本开始,read thread和write thread分别增大到了4个。
可以通过innodb_read_io_threads和innodb_write_io_threads参数可设置读写线程数。
(3)Purge Thread
事务提交后,UNDO日志将不再需要,因此需要Purge Thread来回收已经使用并分配的UNDO页,在InnoDB1.1 版本之前,purge操作仅在InnoDB存储引擎的Master Thread中完成,从1.1 版本开始,purge操作在单独的Purge Thrad中进行。
从InnoDB1.2 版本开始,支持多个Purge Thread同时工作。
(4)Page Cleaner Thread
Page Cleaner Thread是在InnoDB1.2.x 版本中引入的,作用是将脏页刷新的操作放到单独的线程中完成。
2 内存
(1)缓冲池
InnoDB存储引擎是基于磁盘存储的,并按照页的方式管理记录。为了缓解磁盘速度和CPU速度间的鸿沟,通过缓冲池进行弥补。
对数据库页读取的操作,首先将从磁盘读到的页放在缓冲池中,这个过程称为将页FIX在缓冲池中,再读取相同页时,首先在缓冲池中读取,若存在则缓存命中,否则到磁盘读取。
对数据库页的修改操作,首先修改在缓冲池中的页,然后再以一定的频率刷新到磁盘上,这里的刷新操作不是发生在每次更新时,而是通过Checkpoint机制刷新到磁盘。
可以通过innodb_buffer_pool_size参数设置缓冲池的大小,单位为字节。
InnoDB的内存数据对象如下图所示:
从图中可以看到,缓冲池中缓存的数据类型有:数据页、索引页、插入缓存、自适应哈希索引、锁信息、数据字典信息。
从1.0.x 版本开始,允许有多个缓冲池实例,每个页根据哈希值平均分配到不同缓冲池实例中,可以通过参数innodb_buffer_pool_instances配置。
(2)LRU List
通常,数据库中的缓冲池是通过LRU算法来管理的。
在InnoDB中,缓冲池中页的大小默认为16KB,同样使用LRU算法进行管理,不同的是InnoDB对LRU算法做了一些优化。
在InnoDB中,LRU列表还加入了midpoint位置,当读取到新的页时,虽然是最新访问的,但并不直接放入LRU列表的首部,而是放入到LRU列表的midpoint位置,在默认配置下,该位置在LRU列表的5/8处,可以通过innodb_old_blocks_pct参数配置。
如上图的配置,表示新读取的页插入到LRU列表的37%的位置。在InnoDB中,把midpoint之后的列表称为old列表,之前的称为new列表。
为了防止进行锁表扫描时导致缓冲池中的热点页被刷新出,InnoDB引入了innodb_old_blocks_time参数进一步管理LRU列表,这个参数表示页读取到mid位置后需要等待多久才会被加入到LRU列表的热端。
(3)Free List
LRU列表用来管理已经读取的页,但数据库刚启动时,LRU列表是空的,这是页都存放在Free列表中,当需要从缓冲池中分页时,首先从Free列表查找是否有可用的空闲页,若有则将该页从Free列表删除,加入LRU列表中。否则,根据LRU算法,淘汰LRU列表末尾的页,将该内存空间分配给新的页。
InnoDB自1.0.x 版本开始支持压缩页的功能,可将原本16KB的页压缩为1KB、2KB、4KB和8KB的页,对于这些非16KB的页,是由unzip_LRU列表管理的。
(4)Flush List
在LRU列表中的页被修改后,称为脏页(dirty page),即缓冲池中的页和磁盘上的页的数据不一致,这是数据库会通过CheckPoint机制将脏页刷新回磁盘,而Flush列表中的页即为脏页,需要注意,脏页既位于Flush列表,又位于LRU列表。
(5)重做日志缓存
InnoDB的内存区除了有缓冲池外,还有重做日志缓冲(redo log buffer)。InnoDB首先将重做日志放到这个缓冲区,然后按一定的频率将其刷新到磁盘重做日志文件,重做日志缓存默认大小为8MB,可通过innodb_log_buffer_size参数配置。
通常,8MB的重做日志缓存可以满足大部分应用。在以下三种情况下,会将重做日志缓存刷新到磁盘重做日志文件中:
- Master Thread每一秒刷新。
- 每个事务提交时刷新。
- 重做日志缓存剩余空间小于1/2时刷新。
(6)额外的内存池
在InnoDB存储引擎中,内存是通过堆的方式管理的,一些数据结构在分配内存时会在额外的内存区域分配,比如缓冲池控制对象(记录LRU、锁、等待等信息),当额外内存区域不够时,会从缓冲池中申请内存。
(二)CheckPoint技术
用户对数据库的操作首先都是在缓冲池中完成的,在一个页称为脏页后,需要将页数据刷新到磁盘数据文件中,这就存在两个问题:
1. 如果只要页发生变化就进行刷新操作,那么性能会很差。
2. 刷新操作过程中发生宕机,数据会丢失。
为了避免数据丢失,当前的数据库系统普遍采用Write Ahead Log策略,即事务提交时,先写重做日志,再修改页。
这样一来,脏页的刷新就需要一定的机制来控制,否则当数据库宕机后,需要根据大量的重做日志进行恢复,因此CheckPoint技术的目的是解决如下问题:
- 缩短数据库的恢复时间。
- 缓冲池不够时,刷新脏页到磁盘。
- 重做日志不可用时,刷新脏页到磁盘,也就是目前事务型数据库的重做日志是重用的,当想要重用一部分重做日志时,如果这些日志已经无效,则直接重用,否则需要先将数据刷新至重做日志的版本,再重用。
在InnoDB中,有两种CheckPoint:Sharp CheckPoint和Fuzzy CheckPoint。
Sharp CheckPoint会将所有脏页都刷新回磁盘,发生在数据库关闭时。
Fuzzy CheckPoint只刷新一部分脏页,发生在数据库运行时,这种机制会有以下几种情况:
- Master Thread CheckPoint:每秒或每十秒刷新一部分脏页到磁盘上,这个过程是一步的,不会阻塞查询线程。
- FLUSH_LRU_LIST CheckPoint:InnoDB需要保证LRU列表中有一定数量的空闲页可用,当空闲页数量不够时,会将LRU列表尾部的页移除,并将被移除的页中的脏页刷新到磁盘上。在1.1.x版本之前,这个操作是由用户查询线程完成的,自InnoDB1.2.x开始放在了Page Cleaner Thread中,且可通过innodb_lru_scan_depth参数控制空闲页数量。
- Asunc/Sync Flush CheckPoint:重做日志不可用时,需要强制将一些Flush列表中的脏页刷新到磁盘。
- Dirty Page too much CheckPoint:当脏页数量太多时,将一部分脏页刷新到磁盘,可以通过innodb_max_dirty_pages_pct参数设置脏页比例。
(三)Master Thread工作方式
Master Thread具有最高的线程优先级,其内部包括main loop、background loop、flush loop和suspend loop,Master Thread根据数据库的运行状态在各循环中切换。
1 1.0.x之前版本
1 main loop
主循环中包括两大部分的操作:每秒的操作和每十秒的操作。
每秒的操作:
- 日志缓冲刷新到磁盘,即使这个事务还没提交(总是)
- 合并插入缓冲(可能):判断若前一秒的IO次数小于5次,则执行操作
- 至多刷新100个脏页到磁盘(可能):脏页比例如果已经超过设置的比例,则执行操作
- 如果没有用户活动,切换到后台循环
每十秒的操作:
- 刷新100个脏页到磁盘(可能):判断若前10秒的IO次数小于200次,则执行操作
- 合并至多5个插入缓存(总是)
- 日志缓冲刷新到磁盘(总是)
- 删除无用的Undo页(总是):每次最多尝试回收20个Undo页
- 刷新100个或10个脏页到磁盘(总是):若脏页比例超过70%,则刷新100个脏页,否则刷新10个脏页。
2 background loop
删除无用Undo页(总是)
合并20个插入缓冲(总是)
跳回主循环(总是)
不断刷新100个页直到符合条件(可能,跳转到flush loop)
3 flush loop
刷新页到磁盘,如果flush loop没有什么可做的了,则却环岛suspend loop,将Master Thread挂起。
2 1.2.x之前版本
1.0.x 版本之前,每次至多刷新100个脏页到磁盘,合并20个插入缓存,,从1.0.x版本开始加入了innodb_io_capacity配置刷新脏页的数量。
脏页的默认比例由90%降到75%。
引入innodb_adaptive_flushing(自适应刷新),用于配置每秒刷新脏页的数量。
1.0.x之前,最多回收20个Undo页,自1.0.x引入innodb_purge_batch_size配置每次回收的Undo页数量。
3 1.2.x版本
将脏页刷新的操作分离到单独的Page Cleaner Thread中。
(三)InnoDB关键特性
InnoDB存储引擎的关键特性包括:插入缓冲(Insert Buffer)、两次写(Double Write)、自适应哈希索引(Adaptive Hash Index)、异步IO(Async IO)、刷新邻接页(Flush Neighbor Page)。
1 插入缓冲
(1)Insert Buffer
我们知道在插入一条数据的同时还需要插入索引,Insert Buffer就是针对索引页设计的一种优化,插入缓存生效的前提是:
- 非聚集索引
- 非唯一索引
在InnoDB中,主键是行唯一的标识,主键列默认是聚集索引(数据行的物理顺序与索引列的逻辑顺序相同),一定是唯一的,在索引页插入一个聚集索引时,一定需要读取其他索引页判断是否唯一,这是主键约束必须付出的代价,不在Insert Buffer的优化范围内。
但我们需要关注一点:通常我们是不会修改主键的,而其他数据列可能会频繁的修改。
在一张表中,我们可能在除主键列以外的其他数据列建立多个非聚集、非唯一索引,那么我们在插入大量数据时就需要插入大量的非聚集索引,Insert Buffer提供了一种优化方式:对于每一次的插入不是直接写到索引页中,而是先判断插入的非聚集索引页是否在缓冲池中,如果在则直接插入;若不在,则先放到Insert Buffer中,再按照一定的频率将索引数据合并到索引页中,这样一来,Insert Buffer中的插入操作通常能够合并到一个操作中(因为在一个索引页中),这样就大大提高了非聚集索引的插入性能。
(2)Change Buffer
InnoDB自1.0.x版本起开始引入了Change Buffer,支持对INSERT、DELETE、UPDATE都进行缓冲,分别对应Insert Buffer、Delete Buffer和Purge Buffer。
(3)Merge Insert Buffer
Insert/Change Buffer实际上也是一棵B+树,我们将Insert/Change Buffer中的索引数据合并到相应的索引页中的操作称为Merge。
当以下情况发生时,会触发Merge操作:
- 索引页被读到缓冲池时,会检查是否有该索引页的记录存放于Insert Buffer中,若有则进行Merge。
- 当索引记录插入Insert Buffer后发现Insert Buffer可用空间小于1/32页,则读取该索引页到缓冲池中,并触发Merge操作。
- Master Thread每一秒或十秒会进行Merge操作。
2 两次写
当InnoDB正在刷新某个脏页到磁盘中,发生了宕机,这种情况称为部分写失效,我们通常的想法是通过重做日志进行恢复,但重做日志中记录的是关于数据页的物理操作,如偏移量800,写入’aaaa’,那么如果在宕机时数据页本身发生了损坏,那么重做则是没有意义的。因此,在进行重做恢复前,要有一个页的副本,先对磁盘数据页进行还原,得到一个正确的版本,然后再进行重做。
double write的过程如下图所示:
这里需要注意的是,既然我们已经有了共享表空间中正确的数据页,为什么在恢复时还需要重做日志呢?
这是因为共享表空间中记录的是一个时刻的正确状态,并不一定是当前最新的正确状态,重做日志文件的操作可能领先于double write区,因此恢复时仍然需要根据重做日志文件进行恢复。
3 自适应哈希索引
自适应哈希索引是指InnoDB会监控对表上各索引页的查询,如果观察到可以通过建立Hash索引提高速度,则自动根据访问的频率和模式为某些热点页建立Hash索引。
自适应哈希索引要求对某个页的连续访问模式必须是相同的,比如 where a = xxx,且满足以下要求:
- 以该模式访问了100次
- 页通过该模式访问了N次,N = 页中记录数 / 16
4 异步IO
当前数据库系统都采用Async IO的方式来处理IO操作。
AIO指的是发出一个IO请求后,不等待该请求完成则继续发送其他IO请求,当所有请求发送完成后,等待所有IO请求的完成。
AIO可以进行IO Merge操作,即将多个IO合并为一个IO,如分别读取多个相邻的16K数据页可以合并为一次读取N * 16K的数据页。
5 刷新邻接页
当刷新一个脏页时,InnoDB会检测该页所在区的所有页,如果是脏页则一起刷新,这样就可以将多个IO操作合并为一个IO操作,这个特性在传统机械硬盘上具有显著的优势,在固态硬盘上不建议使用,可以通过innodb_flush_neighbors参数配置。
特此声明:本系列博客为均为《MySql技术内幕 InnoDb存储引擎》读书笔记,存在错误还请指正
参考资料
《MySql技术内幕 InnoDb存储引擎》