从 MySQL 获取数据,是从磁盘读取的吗?(buffer pool)

Sherwin.Wei Lv7

从 MySQL 获取数据,是从磁盘读取的吗?(buffer pool)

回答重点

在 MySQL 中,获取数据并不总是直接从磁盘读取。MySQL 使用缓存机制,比如 InnoDB 存储引擎,会将常用的数据和索引缓存在内存中,以提高读取性能。当查询数据时,系统首先会检查缓存(如缓冲池),如果数据存在于内存中,则直接从内存中读取;如果不在,则会从磁盘读取并加载到缓存中。

扩展知识

MySQL 中的缓存

MySQL 从缓存中读取所指的缓存,实际上包含了两个缓存:

1)查询缓存(MySQL 8.0 已废除):在 MySQL 8.0 之前,MySQL 提供了查询缓存功能,用于缓存查询结果。如果相同的查询(同一个查询 SQL)再次执行,并且表没有发生任何变化(这个条件比较苛刻,所以后续废除了),则 MySQL 可以直接从查询缓存中返回结果,而无需重新执行查询。具体的实现类似用一个 map 存储了之前的结果,key 是 SQL,value 为结果,SQL 执行的时候,先去这个 map 看看通过 key 是否能找到值,如果找到则直接返回。

2)InnoDB 缓冲池(buffer pool):这是 InnoDB 存储引擎的核心缓存组件。缓冲池缓存了数据页、索引页和其他相关信息。查询数据时,MySQL 首先在缓冲池中查找,如果找到则直接返回数据,否则从磁盘读取数据页并将其缓存到缓冲池中。

查询缓存和 buffer pool 大致的结构关系如下:

image.png

数据页和索引页

InnoDB 存储引擎将表数据和索引以页为单位存储,每页通常为 16KB。当需要读取某条记录时,MySQL 会加载包含该记录的整个数据页到缓冲池中,从而减少频繁的磁盘 I/O 操作。

所以要记住,MySQL 是以页为单位加载数据的,而不是记录行为单位。

从磁盘读取数据

当数据不在内存缓存中时,MySQL 需要从磁盘读取数据。注意是以页为单位从磁盘获取数据,这里还有个额外的知识点,因为以页为单位,使得顺序遍历数据的速度更快,因为后面的数据已经被加载到缓存中了!

这也符合空间局部性

buffer pool 知识

其实 buffer pool 就是内存中的一块缓冲池,用来缓存表和索引的数据

我们都知道 mysql 的数据最终是存储在磁盘上的,但是如果读存数据都直接跟磁盘打交道的话,这速度就有点慢了。

所以 innodb 自己维护了一个 buffer pool,在读取数据的时候,会把数据加载到缓冲池中,这样下次再获取就不需要从磁盘读了,直接访问内存中的 buffer pool 即可。

包括修改也是一样,直接修改内存中的数据,然后到一定时机才会将这些脏数据刷到磁盘上。

看到这肯定有小伙伴有疑惑:直接就在内存中修改数据,假设服务器突然宕机了,这个修改不就丢了?

别怕,有个 redolog 的存在,它会持久化这些修改,恢复时可以读取 redolog 来还原数据,这个我们后面的面试题再详盘,今天的主角是 buffer pool 哈。

回到 buffer pool,其实缓冲池维护的是页数据,也就是说,即使你只想从磁盘中获取一条数据,但是 innodb 也会加载一页的数据到缓冲池中,一页默认是 16k。

当然,缓冲池的大小是有限的。按照 mysql 官网所说,在专用服务器上,通常会分配给缓冲池高达 80% 的物理内存,不管分配多少,反正内存大小正常来说肯定不会比磁盘大。

也就是说内存放不下全部的数据库数据,那说明缓冲池需要有淘汰机制,淘汰那些不常被访问的数据页。

按照这个需求,我们很容易想到 LRU 机制,最近最少使用的页面将被淘汰,即维护一个链表,被访问的页面移动到头部,新加的页面也加到头部,同时根据内存使用情况淘汰尾部的页面。

通过这样一个机制来维持内存且尽量让最近访问的数据留在内存中。

看起来这个想法不错,但 innodb 的实现并不是朴素的 LRU,而是一种变型的 LRU。

image.png

从图中我们可以看出 buffer pool 分为了老年代(old sublist)和新生代(new sublist)。

老年代默认占 3/8,当然,可以通过 innodb_old_blocks_pct 参数来调整比例。

当有新页面加入 buffer pool 时,插入的位置是老年代的头部,同时新页面在 1s 内再次被访问的话,不会移到新生代,等 1s 后,如果该页面再次被访问才会被移动到新生代

这和我们正常了解的 LRU 不太一样,正常了解的 LRU 实现是新页面插入到头部,且老页面只要被访问到就会被移动到头部,这样保证最近访问的数据都留存在头部,淘汰的只会是尾部的数据。

那为什么要实现这样改造的 LRU 呢?

innodb 有预读机制,简单理解就是读取连续的多个页面后,innodb 认为后面的数据也会被读取,于是异步将这些数据载入 buffer pool 中,但是这只是一个预判,也就是说预读的页面不一定会被访问。所以如果直接将新页面都加到新生代,可能会污染热点数据,但是如果新页面是加到老年代头部,就没有这个问题。

同时大量数据的访问,例如不带 where 条件的 select 或者 mysqldump 的操作等,都会导致同等数量的数据页被淘汰,如果简单加到新生代的话,可能会一次性把大量热点数据淘汰了,所以新页面加到老年代头部就没这个问题。

那 1s 机制是为了什么呢?

这个机制其实也是为了处理大量数据访问的情况,因为基本上大数据扫描之后,可能立马又再次访问,正常这时候需要把页移到新生代了,但等这波操作结束了,后面还有可能再也没有请求访问这些页面了,但因为这波扫描把热点数据都淘汰了,这就不太美丽了。

于是乎搞了个时间窗口,新页面在 1s 内的访问,并不会将其移到新生代,这样就不会淘汰热点数据了,然后 1s 后如果这个页面再次被访问,才会被移到新生代,这次访问大概率已经是别的业务请求,也说明这个数据确实可能是热点数据。

经过这两个改造, innodb 就解决了预读失效和一次性大批量数据访问的问题

至此,对 buffer pool 的了解就差不多了。

Comments