上一篇介绍了 MySQL 的日志,这一篇将介绍内存管理和磁盘管理相关的内容。
MySQL 的数据都是存在磁盘中的,我们要更新一条记录的时候,得先要从磁盘读取该记录,然后在内存中修改这条记录。修改完这条记录后会缓存起来,下次有查询语句命中了这条记录,就可以直接读取缓存中的记录,不需要再从磁盘获取数据了。
MySQL 对数据的增删改查都是在内存中完成的,即在 Buffer Pool 中完成的。
缓存池是 Innodb 存储引擎设计并实现的,是 InnoDB 中的一块内存区域,默认大小是 128M。MySQL 启动后会初始化 Buffer Pool。
InnoDB 会把存储的数据划分为若干个「页」,以页作为磁盘和内存交互的基本单位,一个页的默认大小为 16KB。因此,Buffer Pool 同样需要按「页」来划分。
Buffer Pool 除了缓存「索引页」和「数据页」,还包括了 Undo 页,插入缓存页、自适应哈希索引、锁信息等等。
为了更好的管理这些在 Buffer Pool 中的缓存页,InnoDB 为每一个缓存页都创建了一个控制块,控制块信息包括「缓存页的表空间、页号、缓存页地址、链表节点、锁信息、LSN 信息」等等。
控制块也是占有内存空间的,它是放在 Buffer Pool 的最前面,接着才是缓存页,控制块和缓存页之间灰色部分称为碎片空间。
为了能够快速找到空闲的缓存页,可以使用链表结构,将空闲缓存页的「控制块」作为链表的节点,这个链表称为 Free 链表(空闲链表)。
为了能快速知道哪些缓存页是脏的,于是就设计出 Flush 链表,它跟 Free 链表类似的,链表的节点也是控制块,区别在于 Flush 链表的元素都是脏页。
为了提高缓存命中率,MySQL 改进了 LRU(Least Recently Used) 算法,将 LRU 划分了 2 个区域:old 区域 和 young 区域。young 区域在 LRU 链表的前半部分,old 区域则是在后半部分。
old 区域占整个 LRU 链表长度的比例可以通过
innodb_old_blocks_pc
参数来设置,默认是 37,代表整个 LRU 链表中 young 区域与 old 区域比例是 63:37。
改进过后的 LRU 算法可以解决两个问题:预读失效和 Buffer Pool 污染。
预读机制:程序是有空间局部性的,靠近当前被访问数据的数据,在未来很大概率会被访问到。所以,MySQL 在加载数据页时,会提前把它相邻的数据页一并加载进来,目的是为了减少磁盘 IO。
但是可能这些被提前加载进来的数据页,并没有被访问,相当于这个预读是白做了,这个就是预读失效。
划分这两个区域后,预读的页就只需要加入到 old 区域的头部,当页被真正访问的时候,才将页插入 young 区域的头部。如果预读的页一直没有被访问,就会从 old 区域移除,这样就不会影响 young 区域中的热点数据。
当某一个 SQL 语句扫描了大量的数据时,在 Buffer Pool 空间比较有限的情况下,可能会将 Buffer Pool 里的所有页都替换出去,导致大量热数据被淘汰了,等这些热数据又被再次访问的时候,由于缓存未命中,就会产生大量的磁盘 IO,MySQL 性能就会急剧下降,这个过程被称为 Buffer Pool 污染。
Buffer Pool 污染并不只是查询语句查询出了大量的数据才出现的问题,即使查询出来的结果集很小,也会造成 Buffer Pool 污染。
比如,在一个数据量非常大的表,执行了这条语句:
select * from user where name like "%a%";
可能这个查询出来的结果就几条记录,但是由于这条语句会发生索引失效,所以这个查询过程是全表扫描的,接着会发生如下的过程:
为了解决这个问题,MySQL 对进入到 young 区域条件增加了一个停留在 old 区域的时间判断。只有在 old 区域停留时间超过一定时间,才会被插入到 young 区域头部。
另外,MySQL 针对 young 区域其实做了一个优化,为了防止 young 区域节点频繁移动到头部。young 区域前面 1/4 被访问不会移动到链表头部,只有后面的 3/4被访问了才会。
把脏数据刷回磁盘的技术又称 checkpoint 技术。
MySQL 的脏页落盘是由后台线程定期异步执行的。
在 MySQL 的使用过程中,可能会出现抖动(突然变得很慢,且 CPU 资源被大量占用),很大可能就是在刷盘,即情况 12。
写缓存,change buffer 是 buffer pool 的一部分,当需要修改的数据页不在缓存池内时,会在 change buffer 中记录数据变更,等未来数据被读取时,再将数据 merge 到缓存池中。
在 MySQL5.5 之前,叫插入缓冲(insert buffer)只针对 insert 做了优化;现在对 delete 和 update 也有效,叫做写缓冲(change buffer)。
使用 change buffer 之前:
使用 change buffer 之后:
使用 change buffer,在更新频率高、查询频率低的场景下(且不是更新完马上查询),相当于可以减少一次磁盘随机读开销。(相当于只有步骤 12,和少量的步骤 34)
但是如果所有更新后面,都马上要对这个记录进行查询,那么 change buffer 反而会起到副作用。
merge 时机:
change buffer 只能用于非唯一普通索引页(non-unique secondary index page)。
因为如果索引设置了唯一属性,在进行修改操作时,InnoDB 必须进行唯一性检查。
比如,要插入(4,400)记录,要先判断表中是否已存 k=4 记录,而这必须要将数据页读入内存才能判断。如果都已经读入到内存,那直接更新内存会更快,就没必要使用 change buffer。
MySQL 中有以下几种可能出现空间碎片的情况:
使用 delete 删除一条记录,只是在 B+ 树中将记录标记为删除状态(通过隐藏列中的 deleted_bit),删除后的记录不会消失,且可以被复用。
如果一个数据页上的所有记录都被删除了,那么整个数据页就可以被复用了。而且如果相邻的两个数据页的利用率都很小,系统就会把这两个页上的数据合并到其中一个页上,另外一个数据页就会被标记为可复用。
所以只是删除数据,占用的磁盘空间并不会减少,甚至可能增加。因为 delete 操作还会写入 redo log 和 undo log,从而占用更多的磁盘空间。
truncate 用于清空表内的数据,但是不会删除表本身。立刻释放磁盘空间。
drop 用于删除整个表/库,包括表的结构、属性、索引等。立刻释放磁盘空间。
速度:drop>truncate>delete
经过大量增删改的表,可能存在大量的空洞(数据页中可复用或未被使用的记录)。目前,能够回收表空间的办法仅有一个,就是重建表,手段包括但不限于 optimize,alter table 等。alter table 的有些操作只能靠 rebuild 表来完成。
使用 delete 语句误删数据行时,可以用 Flashback 工具通过闪回把数据恢复过来,即修改 binlog 的内容,拿回原库重放。
能够使用这个方案的前提是,需要确保 binlog_format=row 和 binlog_row_image=FULL。
恢复数据比较安全的做法是恢复出一个备份,或者找一个从库作为临时库,然后在这个临时库上执行这些操作,确认过数据后再恢复回主库,免得出现对数据的二次破坏。
设置 sql_safe_updates=on,当 delete 和 update 语句中没有写 where 条件,或者 where 条件里面没有包含索引字段的话,这条语句的执行就会报错。
使用 drop table 或者 truncate table 语句误删数据表时,或者使用 drop database 语句误删数据库时,主要有两种方式可以恢复。
方案一:使用全量备份加增量日志(实时备份 binlog)。
恢复的流程大概如下:
如果是误删表,不能指定恢复某个表,所以恢复的速度很慢,且由于数据量很大,存在回复时间不可控的问题。
方案二:延迟复制备库。专门搭建延迟复制的备库,只要在延迟时间内发现问题,就能直接用备库快速恢复数据。
使用 rm 命令误删整个 MySQL 实例时,对于高可用的集群而言,只要选出一个新的主库保证整个集群的正常工作,然后再把节点数据恢复,再接入集群即可。
本文介绍了 MySQL 内存管理和磁盘管理。在内存管理部分,有一篇文章写的非常全面:(十二)MySQL之内存篇:深入探寻数据库内存与Buffer Pool的奥妙!
下一节将介绍 MySQL 存储过程和触发器。