MySQL原理(七):内存管理和磁盘管理

前言

上一篇介绍了 MySQL 的日志,这一篇将介绍内存管理和磁盘管理相关的内容。

内存管理

MySQL 的数据都是存在磁盘中的,我们要更新一条记录的时候,得先要从磁盘读取该记录,然后在内存中修改这条记录。修改完这条记录后会缓存起来,下次有查询语句命中了这条记录,就可以直接读取缓存中的记录,不需要再从磁盘获取数据了。

Buffer Pool

MySQL 对数据的增删改查都是在内存中完成的,即在 Buffer Pool 中完成的。

缓存池是 Innodb 存储引擎设计并实现的,是 InnoDB 中的一块内存区域,默认大小是 128M。MySQL 启动后会初始化 Buffer Pool。

  • 当读取数据时,如果数据存在于 Buffer Pool 中,客户端就会直接读取 Buffer Pool 中的数据,否则再去磁盘中读取。
  • 当修改数据时,如果数据存在于 Buffer Pool 中,那直接修改 Buffer Pool 中数据所在的页,然后将其页设置为脏页(该页的内存数据和磁盘上的数据已经不一致),为了减少磁盘I/O,不会立即将脏页写入磁盘,后续由后台线程选择一个合适的时机将脏页写入到磁盘。

InnoDB 会把存储的数据划分为若干个「页」,以页作为磁盘和内存交互的基本单位,一个页的默认大小为 16KB。因此,Buffer Pool 同样需要按「页」来划分。

Buffer Pool 除了缓存「索引页」和「数据页」,还包括了 Undo 页,插入缓存页、自适应哈希索引、锁信息等等。

在这里插入图片描述

为了更好的管理这些在 Buffer Pool 中的缓存页,InnoDB 为每一个缓存页都创建了一个控制块,控制块信息包括「缓存页的表空间、页号、缓存页地址、链表节点、锁信息、LSN 信息」等等。

控制块也是占有内存空间的,它是放在 Buffer Pool 的最前面,接着才是缓存页,控制块和缓存页之间灰色部分称为碎片空间。

在这里插入图片描述

为了能够快速找到空闲的缓存页,可以使用链表结构,将空闲缓存页的「控制块」作为链表的节点,这个链表称为 Free 链表(空闲链表)。

在这里插入图片描述

为了能快速知道哪些缓存页是脏的,于是就设计出 Flush 链表,它跟 Free 链表类似的,链表的节点也是控制块,区别在于 Flush 链表的元素都是脏页。

LRU

为了提高缓存命中率,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 区域中的热点数据。

Buffer Pool 污染

当某一个 SQL 语句扫描了大量的数据时,在 Buffer Pool 空间比较有限的情况下,可能会将 Buffer Pool 里的所有页都替换出去,导致大量热数据被淘汰了,等这些热数据又被再次访问的时候,由于缓存未命中,就会产生大量的磁盘 IO,MySQL 性能就会急剧下降,这个过程被称为 Buffer Pool 污染

Buffer Pool 污染并不只是查询语句查询出了大量的数据才出现的问题,即使查询出来的结果集很小,也会造成 Buffer Pool 污染。

比如,在一个数据量非常大的表,执行了这条语句:

select * from user where name like "%a%";

可能这个查询出来的结果就几条记录,但是由于这条语句会发生索引失效,所以这个查询过程是全表扫描的,接着会发生如下的过程:

  • 从磁盘读到的页加入到 LRU 链表的 old 区域头部;
  • 当从页里读取行记录时,也就是页被访问的时候,就要将该页放到 young 区域头部;
  • 接下来拿行记录的 name 字段和字符串 xiaolin 进行模糊匹配,如果符合条件,就加入到结果集里;
  • 如此往复,直到扫描完表中的所有记录。

为了解决这个问题,MySQL 对进入到 young 区域条件增加了一个停留在 old 区域的时间判断。只有在 old 区域停留时间超过一定时间,才会被插入到 young 区域头部。

另外,MySQL 针对 young 区域其实做了一个优化,为了防止 young 区域节点频繁移动到头部。young 区域前面 1/4 被访问不会移动到链表头部,只有后面的 3/4被访问了才会。

脏页刷盘

把脏数据刷回磁盘的技术又称 checkpoint 技术。

MySQL 的脏页落盘是由后台线程定期异步执行的。

  1. 当 redo log 满了的情况下,会主动触发脏页刷新到磁盘;
  2. Buffer Pool 空间不足时,需要将一部分数据页淘汰掉,如果淘汰的是脏页,需要先将脏页同步到磁盘;
  3. MySQL 认为空闲时,后台线程回定期将适量的脏页刷入到磁盘;即使非空闲时,也会见缝插针地刷盘;
  4. MySQL 正常关闭之前,会把所有的脏页刷入到磁盘;

在 MySQL 的使用过程中,可能会出现抖动(突然变得很慢,且 CPU 资源被大量占用),很大可能就是在刷盘,即情况 12。

change buffer

写缓存,change buffer 是 buffer pool 的一部分,当需要修改的数据页不在缓存池内时,会在 change buffer 中记录数据变更,等未来数据被读取时,再将数据 merge 到缓存池中。

在这里插入图片描述

在 MySQL5.5 之前,叫插入缓冲(insert buffer)只针对 insert 做了优化;现在对 delete 和 update 也有效,叫做写缓冲(change buffer)。

使用 change buffer 之前:

  1. 当需要更新的数据不在缓存池中时,从磁盘中读取数据页到 buffer pool;(一次磁盘随机读)
  2. 更新数据页;(一次写内存)
  3. 将数据页更新记录到 redo log,redo log 落盘。(一次磁盘顺序写)

使用 change buffer 之后:

  1. 当需要更新的数据不在缓存池中时,不需要从磁盘中读取数据页,而是在写缓存中记录这个变更操作;(一次写内存)
  2. 将数据页更新记录到 redo log,redo log 落盘;(一次磁盘顺序写)
  3. 当访问到该记录时,先从磁盘中读取数据页,再从写缓存中读取变更信息(如果有多个,则依次更新),最后更新到缓存池中,即 merge 操作。(一次磁盘随机读和一次写内存)
  4. 将缓存页和 change buffer 的更新记录到 redo log,redo log 落盘。(一次磁盘顺序写)

使用 change buffer,在更新频率高、查询频率低的场景下(且不是更新完马上查询),相当于可以减少一次磁盘随机读开销。(相当于只有步骤 12,和少量的步骤 34)

但是如果所有更新后面,都马上要对这个记录进行查询,那么 change buffer 反而会起到副作用。

merge 时机:

  • 数据页被读取
  • 后台线程认为数据库空闲时
  • 数据库正常关闭时

change buffer 只能用于非唯一普通索引页(non-unique secondary index page)。

因为如果索引设置了唯一属性,在进行修改操作时,InnoDB 必须进行唯一性检查。

比如,要插入(4,400)记录,要先判断表中是否已存 k=4 记录,而这必须要将数据页读入内存才能判断。如果都已经读入到内存,那直接更新内存会更快,就没必要使用 change buffer。

磁盘空间管理

空间碎片

MySQL 中有以下几种可能出现空间碎片的情况:

  • 修改行数据导致出现行间碎片;
  • 插入和修改数据可能导致页分裂,从而使数据页中有大量空余空间(数据页空余空间很难避免);

删除数据

delete

使用 delete 删除一条记录,只是在 B+ 树中将记录标记为删除状态(通过隐藏列中的 deleted_bit),删除后的记录不会消失,且可以被复用。

如果一个数据页上的所有记录都被删除了,那么整个数据页就可以被复用了。而且如果相邻的两个数据页的利用率都很小,系统就会把这两个页上的数据合并到其中一个页上,另外一个数据页就会被标记为可复用。

所以只是删除数据,占用的磁盘空间并不会减少,甚至可能增加。因为 delete 操作还会写入 redo log 和 undo log,从而占用更多的磁盘空间。

truncate

truncate 用于清空表内的数据,但是不会删除表本身。立刻释放磁盘空间。

drop

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)。

恢复的流程大概如下:

  1. 取最近一次全量备份;
  2. 用备份恢复出一个临时库;
  3. 从日志备份里面,取出备份点之后的日志;
  4. 把这些日志,除了误删除数据的语句外,全部应用到临时库。

如果是误删表,不能指定恢复某个表,所以恢复的速度很慢,且由于数据量很大,存在回复时间不可控的问题。

方案二:延迟复制备库。专门搭建延迟复制的备库,只要在延迟时间内发现问题,就能直接用备库快速恢复数据。

MySQL实例

使用 rm 命令误删整个 MySQL 实例时,对于高可用的集群而言,只要选出一个新的主库保证整个集群的正常工作,然后再把节点数据恢复,再接入集群即可。

最后

本文介绍了 MySQL 内存管理和磁盘管理。在内存管理部分,有一篇文章写的非常全面:(十二)MySQL之内存篇:深入探寻数据库内存与Buffer Pool的奥妙!

下一节将介绍 MySQL 存储过程和触发器。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值