《MySQL是怎样运行的》知识总结
单表访问方法
表的连接原理
优化 基于查询成本的优化
优化需要的统计数据
优化 基于规则的优化
Explain详解
InnoDB缓冲区
事务
redo日志
undo 日志
MVCC原理
MySQL 锁
对于InnoDB存储引擎来说,我们存储的用户数据及索引(聚簇索引、普通索引)、各种的系统数据都是以页的形式存储在表空间中,而表空间是InnoDB对文件的抽象,这些数据实际都是存储到磁盘中的。
我们知道跟CPU的速度相比,磁盘的速度是很慢的,所以InnoDB在处理客户端的请求时(比如查看记录),需要将磁盘中的对应的页加载到内存中,将整个页加载到内存后,进行读写操作
,完成操作后并不会立刻将这个页对应的内存空间释放掉,而是缓存起来
,当下一次访问到这个页面时,也就可以直接访问内存中的这个页,减少了IO开销。
为了缓存磁盘的页,InnoDB在MySQL启动时,会向操作系统申请一块连续的内存空间作为缓冲区
,默认情况下,缓冲区大小为128MB,可以在配置文件中指定缓冲区的大小(innodb_buffer_pool_size
启动选项)
缓冲区是一个连续的内存空间,这些内存被划分为多个页
(缓冲页大小为16KB),为了管理这些缓冲页,这些缓存页都对应这一个控制块
(包含缓冲页信息的内存区域),控制块包括了缓冲页表空间、页号,页在缓冲区的地址、链表信息,控制块和缓冲页都存放在缓冲区中。
缓冲区中可能存在剩余空间不足以存放一对控制块和缓冲页,这时就产生了内存碎片。
系统变量innodb_buffer_pool_size
的大小中并不包括控制块占用的内存空间。
在MySQL启动时,需要完成缓冲区的初始化工作,也就是需要向操作系统申请内存空间,并将缓冲区划分为许多对控制块与缓冲页,此时缓冲区是没有加载页面的。
InnoDB将空闲的缓冲页对应的控制块作为一个节点,组成了一个空闲链表。
为了维护这个空闲链表,还需要一个基节点,它包含了链表的头、尾节点、链表中的控制块的数量等信息,基节点占用40字节空间。
当要访问的页不在缓冲区时,就需要将磁盘上的这个页加载到缓冲区中。
那如何判断这个页是否在缓冲区中呢?使用哈希表!
对于页,是根据表空间、页号来定位的,可以使用表空间+页号
作为哈希表的key
,页对应的控制块的地址
作为value
,这样就可以快速访问缓冲区的页信息,如果不能在哈希表中找到这个页,那就从free链表取出一个空闲缓冲页(对应的控制块),将从磁盘取出的页放到这个位置。
当我们修改了缓冲区内页的数据后,它就与磁盘上的不一致了,这个页叫做脏页,同样我们对页进行了修改后,并不会立刻将页写入到磁盘中,而是在未来的时间点上刷新到磁盘上。
如何判断这个页是否被修改呢?使用链表结构将对应的缓冲页(对应的控制块)组织起来。
缓冲区的内存大小是固定的,当缓冲区已经没有空闲的缓冲页后,就需要将缓冲区的缓冲页淘汰,让新的页加载到缓冲区中。
使用缓冲区的目的是为了减少磁盘IO
,理想情况是我们访问的页在缓冲区中,这样也就不需要从磁盘加载页到内存中了,我们希望缓冲区的命中率越高越好
。
为了减少磁盘IO
,我们在淘汰缓冲页时,也就希望淘汰最近最少使用
的缓冲页,使用链表结构完成让常使用的缓冲页放在链表头,让少用的缓冲页放在链表尾
,这样也就形成了LRU链表
。
使用简单的LRU链表是不能达到减少磁盘IO
的目的的,主要原因是:
可能
会在以后读取到一些页面
,就预先读取页面到缓冲区中
innodb_read_ahead_threshold
的值(默认是56),就会触发一次异步读取下一个区
的所有页面
到缓冲区的请求13个连续的页(要求这些页是LRU链表是young区的前1/4部分)
被加载到缓冲区中,就会触发一次异步读取本区
的所有其他页面
到缓冲区的请求(innodb_random_read_ahead
默认值是OFF)降低缓冲区命中率的原因有:
InnoDB在设计LRU链表时,将这个链表分为2部分
随着程序的运行,某个节点所属的区域可能发生变化
innodb_old_blocks_pct
系统变量指明了old区占用的比例,默认是37%
对LRU链表进行了区域划分后,也就可以针对上面降低缓冲区命中率的情况进行优化:
innodb_old_blocks_time
指明了时间间隔(默认1s),对某个处于old区域
的页面进行第一次访问后
,就在它对应的控制块
记录这个访问时间
,如果后续的访问时间
与第一次访问的时间在这个时间间隔内,就不会将old区的这个页面移动到young区的头部。系统变量innodb_old_blocks_time
如果为0,那么每次访问一个页面,就会将页面移动到young头部。
频繁的对链表节点进行移动操作,造成的开销过大,针对这个情况,优化的策略是,只有被访问的缓冲页位于young区
的1/4的后面,才会将这个缓冲页移动到LRU链表的头部,这样可以提高性能
针对LRU链表的优化措施有很多
只要磁盘加载一个页面到缓冲区中,该缓冲页对应的控制块就会被加入到LRU链表
flush链表的节点一定是LRU链表中的节点
为了管理缓冲区的缓冲页,InnoDB还引入了其他链表,包括用于管理解压页
的unzip LRU链表
、管理压缩页
的zip clean 链表,
zip free数组中每一个元素都代表一个链表`,它们组成伙伴系统来为压缩页提供内存空间等。
后台会有专门的线程负责每隔一段时间将脏页刷新到磁盘上,这样不会影响用户线程的请求,刷新方式有:
old区域刷新一部分页面
到磁盘:后台线程定时
从LRU链表尾部扫描一定数量
(innodb_lru_scan_depth)的页面,如果发现脏页,就把它们刷新到磁盘,刷新方式是buf_flush_lru
buf_flush_list
控制块中记录了缓冲页是否被修改的信息
为了更高效地执行脏页的刷新,InnoDB还设计了许多系统变量来控制刷新的过程:
innodb_flush_neighbors
innodb_io_capacity_max
innodb_adaptive_flushing
innodb_max_dirty_page_ptc
后台线程刷新脏页速度比较慢时,可能出现用户线程准备加载页面到缓冲区,而缓冲区没有空闲的页面的情况,这时就尝试查看LRU链表的尾部,释放未修改的缓冲页,如果没有未修改的缓冲页,那就不得不将LRU链表的一个脏页
写入磁盘这种方式是buf_flush_single_page
系统特别繁忙时,会出现用户线程从flush链表
刷新脏页到磁盘的情况,在处理用户请求时区刷新脏页是一种严重降低处理速度的行为
缓冲区本质就是向操作系统申请一块连续的内存空间,在多线程的环境下,访问缓冲区需要加锁,如果缓冲区十分大,并发访问量也比较高,就会影响到缓冲区的处理速度。
针对这种情况,可以将比较大的缓冲区拆分成若干个小的缓冲区,缓冲区之间是独立的(独立申请内存区空间,独立地处理各种链表),这样也就可以提高并发性了。
服务器启动时可以修改系统变量innodb_buffer_pool_instances
指明缓冲区实例的个数,
一个缓冲区实例占用的空间是innodb_buffer_pool_size
÷innodb_buffer_pool_instances
,缓冲区大小小于1G时,无法设置多个缓冲区实例
在MySQL 5.7.5及以后的版本中,支持在服务器运行的过程中,调整缓冲区的大小,但是每次调整缓冲区的大小时,需要重新向操作系统申请一块连续的内存空间,并将原缓冲区的内容复制到新缓冲区中,耗时久。
MySQL不再一次性向操作系统申请一大块内存空间,而是以一个chunk
为单位申请内存空间,缓冲区由多个chunk组成,一块chunk代表一个连续的内存空间。
所以我们在服务器运行期间修改缓冲区大小时,就可以以chunk
为单位,来增加、减少内存空间,而不需要向操作系统申请一块大的内存空间
chunk的大小可以通过系统变量innodb_buffer_pool_chunk_size
修改(默认大小是128M),在服务器运行期间,不允许对这个系统变量进行修改,它代表的是InnoDB向操作系统申请内存空间的大小,如果修改了这个值,就需要将原chunk的控制块、缓冲页复制到新的内存空间中
系统变量innodb_buffer_pool_chunk_size
不包含对应控制块占用内存
innodb_buffer_pool_size
:需要是innodb_buffer_pool_instances
×innodb_buffer_pool_chunk_size
的倍数(保证缓冲区实例中的chunk数量相同)
如果innodb_buffer_pool_size
大于实例数×chunk大小
,但不是整数倍,那服务器会自动向上调整到整数倍
如果缓冲区大小
小于实例数×chunk大小
,chunk大小
会被调整为缓冲区大小
÷实例数
缓冲区还可以存储自适应哈希索引的信息
查看缓冲区状态信息:show engine innodb status\G;