1、InnoDB概述
InnoDB是一个高性能、高可用、高可扩展,是MySQL 第一个完整支持ACID事务的存储引擎,其特点是行锁设计、支持MVCC、支持外键、提供一致性非锁定读,同时被设计用来最有效的利用以及使用内存和CPU。所以,从MySQL 5.5开始,InnoDB作为默认的存储引擎。因为有InnoDB的存在,才使Mysql数据库更有魅力。
2、InnoDB体系架构
2.1 内存
本节主要讲述内存区域的划分,主要包含缓冲池、重做日志缓冲等,如下图所示。
2.1.1 缓冲池(Buffer Pool)
缓冲池是内存的一块区域,作用是通过内存的速度来弥补磁盘速度较慢对数据库的影响,在对数据库读取页操作时,首先判断该页是否在缓冲池中,若在,直接返回,否则读取磁盘。对于修改操作,首先修改缓冲池的页,然后再以一定频率刷新到磁盘。
缓冲池的大小影响数据库性能,建议缓冲池大小设置为机器内存的50%-60%,通过参数innodb_buffer_pool_size设置缓冲池大小。具体来看,缓存的类型包括索引页、数据页、undo页、插入缓冲、自适应哈希索引、锁信息、数据字典。
Free List
Free列表是记录那些空闲的缓存页的,缓存页和磁盘页大小一样,都是16k,为了更好的管理这些页,给每个页设置一个控制信息块,每个页对应一个控制块,这个控制块记录了该页所属的表空间号、页号、缓存页在Buffer Pool中的地址等信息。数据库初始化时,把空闲的缓存页对应的控制块作为一个节点,插入到Free列表中,每当从磁盘中加到一个页到内存中的时候,就从这个列表中取出一个空闲缓存页,先将磁盘中的这个页的信息(表空间号 页号)写入到这个空闲缓存页所对应的控制块中,然后将这个节点从列表中pop出,表示这个页已经被使用了。
LRU List
从Free List分到的缓存页会加入到LRU List 中,LRU List 通过改进的LRU算法(Least Recently Used,最近最少使用)管理,即最频繁的放在前端,最少使用的放在尾端。LRU列表加入了midpoint位置,新读到的页,放置在midPoint位置,该位置可以通过参数innodb_old_blocks_pct控制,默认37%,待数据在尾端等待innodb_old_blocks_time时间后,默认1s,会被加入到前端。当缓存池不够使用时,首先释放尾端的页。
LRU流程:
要访问数据页P3,由于P3在young区域,因此和优化前的LRU算法一样,将其移到链表头部,变成状态2;
之后要访问一个新的不存在于当前链表的数据页,这时候依然是淘汰掉数据页Pm,但是新插入的数据页Px,是放在LRU_old处;
处于old区域的数据页,每次被访问的时候都要做下面这个判断:
若这个数据页在LRU链表中存在的时间超过了1秒,就把它移动到链表头部;
如果这个数据页在LRU链表中存在的时间短于1秒,位置保持不变。
怎么知道数据页有没有被缓存?数据库还会有一个哈希表数据结构,用表空间号+数据页号,作为一个key,然后缓存页的地址作为value,当要使用一个数据页的时候,通过“表空间号+数据页号”作为key去这个哈希表里查一下,如果没有就读取数据页,如果已经有了,就说明数据页已经被缓存了。
思考:我们写SQL的时候,只知道表+行的概念,但是在MySQL内部操作的时候,是表空间+数据页的概念,请问他们的区别和联系?
Flush List
当对缓存页内容修改时,缓存页和磁盘的数据页内容不一致,这个页称为脏页(dirty page),而Flush List中的页即为脏页列表。
InnoDB修改数据的时候是WAL(write ahead log) 机制,即先写log 再写数据,目的是为了保证操作的持久化。即先将对数据的改动写入到 log buffer中,然后再在内存中记录数据的变化,写入log buffer 后,就会将这个缓存页对应的控制块加入到Flush列表上,数据库会通过Checkpoint机制将脏页适时刷回磁盘,Flush List 结构同Free List。
2.1.2 Change Buffer(Insert Buffer)
InnoDB 对于辅助索引的更新或插入操作并不是一次更新到索引页中,而是先判断是否在缓存,若在,直接更新缓存,若不在,则先放入Change Buffer中,然后再以一定频率或情况和索引页合并,这时通常将多个更新合并到一个操作中,大大提高了辅助索引更新的性能。可以通过参数innodb_change_buffer_max_size来控制Change Buffer最大使用的内存数量,默认25,表示最多使用1/4的缓冲池内存空间。该参数最大有效值为50。
思考,为啥不能是唯一索引?
何时merge channge buffer?
辅助索引页被读取到缓存;
Insert Buffer Bitmap显示辅助索引页已无可用空间;
Master Thread。
Insert Buffer Bitmap会记录索引页的可用数量,及该记录是否缓存在Change Buffer中。
2.1.3 Redo Log Buffer(重做日志缓存)
内存中还有一个区域是重做日志缓存(redo log buffer),当做数据变更时,首先将数据写入日志缓存(redo log buffer),然后再按照一定频率刷新到日志文件,重做日志缓存一般不需要设置太大,因为一般情况每秒会刷新一次,因此只要保证每秒产生的事务量在这个缓存大小内即可,该值可通过参数innodb_log_buffer_size控制,默认8M。
重做日志写入后,缓存池的数据会按照Checkpoint机制写入磁盘。如下图所示:
日志缓冲写入磁盘分为三种情况,通过参数innodb_flush_log_at_trx_commit 配置:
innodb_flush_log_at_trx_commit = 0;每次事务提交将日志写入日志缓冲,每秒将日志缓冲区写入log file,并同时flush到磁盘。
innodb_flush_log_at_trx_commit = 1;每次事务提交将日志缓冲区写入log file,并同时flush到磁盘。
innodb_flush_log_at_trx_commit = 2;每次事务提交将日志缓冲区写入系统文件缓冲,每秒flush一次到磁盘。
2.2 后台线程
后台线程的主要作用是负责刷新内存池的数据,保证缓冲池的内存缓存的是最近的数据,同时将已修改的数据刷新到磁盘文件。InnoDB是多线程模型,后台有多个不同的后台线程,处理不同的任务,包括Master Thread、IO Thread、Purge Thread、Page Clean Thread。
Master Thread
Master Thread是非常核心的后台线程,负责将缓存池数据异步刷新到磁盘,包括日志缓存、脏页刷新、undo页回收等。
Master Thread具有最高的线程优先级别。其内部由多个循环(loop)组成:主循环(loop)、后台循环(backgrounp loop)、刷新循环(flush loop)、暂停循环(suspend loop),Master Thread会根据数据库运行的状态在各种循环之间进行切换,下图为完整的伪代码;
名词解释:
innodb_io_capacity:磁盘io吞吐量,默认200
buf_get_modified_ratio_pct:当前缓冲池脏页的比例
innodb_max_dirty_pages_pct:配置文件中的脏页比例,默认75%
innodb_adaptive_flushing:自适应刷新,innodb通过buf_flush_get_desired_flush_rate函数来判断最合适的刷新脏页的数量
innodb_purge_batch_size:控制每次full purge 回收的脏页数量,默认20
IO Thread
InnoDB内部使用大量异步io(AIO)处理IO请求,可以极大提高数据库性能,而io thread的任务就是处理io请求,共有4个io thread,分别是read、write、insert buffer和log io thread,可用使用innodb_read_io_threads和innodb_write_io_threads来设置read thread和write thread,默认分别是4个。如下图:
也可以通过命令 show engine innodb status\G;查看thread;
Purge Thread
事务提交之后,其所使用的undo页便不再需要,因此需要purge thread回收已经分配并使用的undo页,在mysql5.6之前,purge thread仅在master中完成,在mysql5.6之后,purge操作作为独立的线程执行,以此减轻master thread工作,提高性能,支持多个purge thread,目的加快undo页的回收,默认线程数为4。
可以在mysql配置文件添加如下命令来启用独立的purge thread。
innodb_purge_threads = 4
Page Cleaner Thread
page cleaner thread作为一个独立线程来清空脏页,提高innodb性能。
3、InnoDB关键特性
Checkpoint机制
对于InnoDB存储引擎而言,是通过LSN(Log Sequence Number)来标记版本的,LSN是一个一直递增的整型数字,表示事务写入到日志的字节总量。在Innodb事务日志中,Innodb每次取最老的modified page(last checkpoint)对应的LSN,再将此脏页的LSN作为Checkpoint点记录到日志文件,意思就是“此LSN之前的LSN对应的日志和数据都已经刷新到redo log。
当mysql crash的时候,Innodb扫描redo log,从last checkpoint开始将 redo log刷新到数据页,直到last checkpoint对应的LSN等于redo log的最新LSN,则恢复完成。
checkpoint的目的:
缩短数据库恢复时间;
缓冲池不够用时,将脏页刷新到磁盘;
重做日志不可用时,刷新脏页。
当数据库宕机时,不需要恢复所有的日志,只需要将checkpoint之后的日志刷新回磁盘,从而缩短时间。
当缓冲池不够用时,会把LRU尾端的页溢出,如果该页是脏页,会强制执行checkpoint,将脏页刷到磁盘。
重做日志是循环使用的,若重做日志还未使用,则强制执行checkpoint,将缓冲池的页至少刷新到重做日志的位置。
在InnoDB存储引擎内部,有两种Checkpoint,分别为:Sharp Checkpoint、Fuzzy Checkpoint。
Sharp Checkpoint发生在数据库关闭时将所有的脏页都刷新回磁盘,这是默认的工作方式,即参数innodb_fast_shutdown=1。但是若数据库在运行时也使用Sharp Checkpoint,那么数据库的可用性就会受到很大的影响。故在InnoDB存储引擎内部使用Fuzzy Checkpoint进行页的刷新,即只刷新一部分脏页,而不是刷新所有的脏页回磁盘。
Fuzzy Checkpoint:
1、Master Thread Checkpoint;
2、FLUSH_LRU_LIST Checkpoint;
3、Async/Sync Flush Checkpoint;
4、Dirty Page too much Checkpoint;
1、Master Thread Checkpoint
以每秒或每十秒的速度从缓冲池的脏页列表中刷新一定比例的页回磁盘,这个过程是异步的,此时InnoDB存储引擎可以进行其他的操作,用户查询线程不会阻塞。
2、FLUSH_LRU_LIST Checkpoint
因为InnoDB存储引擎需要保证LRU列表中需要有一定数量的空闲页可供使用。倘若没有可用空闲页,那么InnoDB存储引擎会将LRU列表尾端的页移除。如果这些页中有脏页,那么需要进行Checkpoint,而这些页是来自LRU列表的,因此称为FLUSH_LRU_LIST Checkpoint。这个检查被放在了一个单独的Page Cleaner线程中进行,并且用户可以通过参数innodb_lru_scan_depth控制LRU列表中可用页的数量,该值默认为1024,如:
3、Async/Sync Flush Checkpoint
指的是重做日志缓存不可用的情况,这时需要强制将一些页刷新回磁盘。简单说,当脏页比例大于75%,触发Async Flush,当脏页比例大于90%,触发Sync Flush。在mysql5.6之前,Async Flush会阻塞当前用户线程,Sync Flush会阻塞所有用户线程,mysql5.6之后,这部分刷新操作放在page clean thread中,都不会阻塞用户线程。
4、Dirty Page too much
即脏页的数量太多,导致InnoDB存储引擎强制进行Checkpoint。其目的总的来说还是为了保证缓冲池中有足够可用的页。其可由参数innodb_max_dirty_pages_pct控制:
innodb_max_dirty_pages_pct值为75表示,当缓冲池中脏页的数量占据75%时,强制进行Checkpoint,刷新一部分的脏页到磁盘。
Double Write(两次写)
Partial page write(部分写):
数据写入磁盘是以页为单位进行的,如果写一部分之后,发生服务器宕机了,只有一部分写是成功的,这种情况下就是partial page write。
什么是double write:
简单来说,就是在写数据页之前,先把这个数据页写到一块独立的物理文件位置(ibdata),然后再写到数据页。这样在宕机重启时,如果出现数据页损坏,通过该页的副本来还原该页。double write由两部分组成,一部分是内存中的doublewrite buffer,大小为2M,另一部分是磁盘共享表空间连续的128页, 即2个区,同样2M。
double write工作流程:
当mysql将脏数据刷新到数据页的时候, 先使用memcopy 将脏数据复制到内存中的double write buffer ,之后通过double write buffer再分2次,每次写入1MB到共享表空间,然后马上调用fsync函数,同步到磁盘上,避免缓冲带来的问题,在这个过程中,doublewrite是顺序写,开销并不大,在完成doublewrite写入后,在将double write buffer写入各表空间文件,这时是离散写入。
如果发生了极端情况(断电),InnoDB再次启动后,发现了一个Page数据已经损坏,那么此时就可以从doublewrite buffer中进行数据恢复了。
Flush Neighbor Page(刷新邻接页)
刷新邻接页的工作原理是:当刷新一个脏页时,InnoDB会检测该页所在区的所有页,如果是脏页,那么一起刷新,这样做好处显而易见,可以通过AIO将多个IO写入操作合并成一个IO操作。
同时该机制带来一个问题:是不是将脏页写入之后,该页又脏了?
故,Innodb提供参数innodb_flush_neighbors,来判断是否开启该特性,对于传统机械硬盘建议开启,对于固态硬盘建议关闭。
MySQL5.7版本默认为1,MySQL 8.0版本默认为0,0:关闭 1:开启。