引言
MySQL是基于磁盘工作的,这句几乎刻在了每个后端程序员DNA里,但它真的对吗?其实答案并不能盖棺定论,你可以说MySQL是基于磁盘实现的,这点我十分认同,但要说MySQL是基于磁盘工作,这点我则抱否定的态度,至于为什么呢?这跟咱们本章的主角:Buffer Pool有关,Buffer Pool是什么?还记得咱们在《MySQL架构篇》中聊到的缓存和缓冲区么,其中所提到的写入缓冲区就位于Buffer Pool中。
不过在具体分析它之前,先结合MySQL的整体架构,来聊一聊MySQL的内存。
一、结合MySQL架构聊一聊它的内存
经过《MySQL架构篇》的学习后,咱们已经熟悉了MySQL的整体架构,也包括在《SQL执行篇》中,站在一条SQL的角度进一步加深了对各组件的印象,但其中并未讲明白MySQL的工作时的内存结构,哪本章就先来盘一盘这块内容。
闲话少说,先上个图,毕竟正所谓:“开局一张图,装备全靠打”,如下:
注意观察,实际MySQL启动后内存结构略显复杂,但大体可分为MySQL工作组件、线程本地内存、MySQL共享内存、存储引擎缓冲区四大板块。
实际上MySQL内存模型和JVM类似,JVM内存主要会划分为线程共享区和线程私有区,而上图中的MySQL内存区域,左边则是线程私有区域,每条工作线程中都会分配的区域,各线程之间互不影响,而右边的三大板块,则属于线程共享区域,即所有线程都可访问的内存。
当然,线程共享区这块也会细分,右边上面的两个板块,都属于MySQL-Server层使用的内存,也就意味着这两块内存是所有引擎都共享的区域,而最下面这个区域,每个存储引擎都不相同,也就是InnoDB会构建自己的Buffer缓冲区,MyISAM也会构建自己的缓冲区。
Ok~,大概理解MySQL的内存构成后,下面一起简单聊一聊这四个内存区域。
1.1、MySQL Server - 工作组件
这个比较容易理解,也就是对应着MySQL架构图中的组件层,如下:
因为后续客户端连接时,都需要经过一系列的连接工作,处理SQL时也需要经过一系列的解析、验证、优化工作,所以MySQL会在启动时,会先将这些工作组件初始化到内存中,方便后续处理客户端的操作。
这里很简单,只需要额外注意一点即可,就是数据库的连接池中,存的到底是什么?存的实际上就是数据库连接对象,MySQL内部的连接对象,其中包含了客户端连接信息,如客户端IP、登录的用户、所连接的DB....等这类信息,同时这些连接对象在内部会绑定一条工作线程,因此你也可以将它理解成是一个线程池!MySQL复用连接的本质,实则是在复用线程,出现一个新的客户端连接时,首先会根据客户端信息为其创建连接对象,然后再复用连接池中的空闲线程。
但MySQL工作线程的本地内存中包含了很多东西,所以下面简单聊一聊。
1.2、工作线程的本地内存
工作线程的本地内存区域,也被称之为线程私有区,即MySQL在创建每条线程时,都会为其分配这些内存:
但这一大长排到底是啥意思呢,简单的说明一下其中每个区域的含义:
-
thread_stack:线程堆栈,主要用于暂时存储运行的SQL语句及运算数据,和Java虚拟机栈类似。
-
sort_buffer:排序缓冲区,执行排序SQL时,用于存放排序后数据的临时缓冲区。
-
join_buffer:连接缓冲区,做连表查询时,存放符合连表查询条件的数据临时缓冲区。
-
read_buffer:顺序读缓冲区,MySQL磁盘IO一次读一页数据,这个是顺序IO的数据临时缓冲区。
-
read_rnd_buffer:随机读缓冲区,当基于无序字段查询数据时,这里存放随机读到的数据。
-
net_buffer:网络连接缓冲区,这里主要是存放当前线程对应的客户端连接信息。
-
tmp_table:内存临时表,当SQL中用到了临时表时,这里存放临时表的结构及数据。
-
bulk_insert_buffer:MyISAM批量插入缓冲区,批量insert时,存放临时数据的缓冲区。
-
bin_log_buffer:bin-log日志缓冲区,《日志篇》提到过的,bin-log的缓冲区被设计在工作线程的本地内存中。
可以看到,在工作线程的本地内存中,除开最基本的线程堆栈外,MySQL还往内部“塞了”一堆东西,这些东西在不同的SQL运行时,都有各自的作用,但基本上是为了更好的保存临时数据而设计的。
思考一个问题:对于上面列出的各类缓冲区,为什么要为每条线程都分配专属的内存,而不是直接在共享内存中搞一块大的空间,然后提供给所有线程来操作呢?其实答案很简单,因为这些数据本身就是一条线程在执行SQL时产生的临时数据,其他线程压根不会去用到另一条线程的临时数据,所以这些临时数据没有必要被共享。
除开上述原因外,将这些缓冲区都放在线程本地内存中,还有一点最大的好处:能够提升多线程并发执行的性能!这句话怎么理解呢?很简单,如果把上述的各个缓冲区放在共享内存中,然后提供给线程存放执行时的临时数据,因为多线程的缘故,所以同一时刻、同一快内存有可能出现多条线程一起操作,那就会出现线程不安全的问题,想要解决就只能加锁将多线程串行化,这自然会在很大程度上影响性能!因此将这些存临时数据的缓冲区,设计在本地内存中才最合适。
不过也并非所有数据都适合放在线程的本地内存中,有一些多条线程之间都会访问的数据,如果再放到本地内存中,就会造成很大的冗余性,比如典型的索引根节点数据,每条线程都有可能会通过索引查询数据,因此每条线程都“缓存”一份放在自己的内存中,就会占用大量的内存空间,这样反而弊大于利。
下面再一起聊聊MySQL的共享内存区域,以及其中到底会存哪些数据。
1.3、MySQL共享内存区
先简单介绍一下上述给出的几个内容:
-
Key Buffer:MyISAM表的索引缓冲区,提升MyISAM表的索引读写速度。
-
Query Cache:查询缓存区,缓冲SQL的查询结果,提升热点SQL的数据检索效率。
-
Thread Cache:线程缓存区,存放工作线程运行期间,一些需要被共享的临时数据。
-
Table Cache:表数据文件的文件描述符缓存,提升数据表的打开效率。
-
Table Definition Cache:表结构文件的文件描述符缓存,提升结构表的打开效率。
上面的这几个线程共享区域还比较容易理解,对于最后两个文件描述符缓存,大家可能存在些许疑惑,这里可以先看一下《FD-文件描述符的定义》,文件描述符本质上就是一个指针,指向一个具体的数据。这里为何要在内存中存放表数据文件、表结构文件的FD缓存呢?主要是为了提升性能,先来看一个问题:
比如我现在想要操作zz_users表的数据,那首先是不是得找到这张表?但表的位置可能分布在磁盘的任何一处,总不能触发磁盘IO把整个磁盘检索一遍,然后确定表的位置吧?所以内存中直接设计了一个缓存区,专门缓存这些表数据文件的磁盘位置,要对某张表进行操作时,直接去文件描述符缓存中找,然后根据其中记录的地址,去磁盘中固定的位置上操作表数据。
表结构的文件描述符缓存,作用也是相同的,比如现在要增加一个索引,或者修改一个字段,也不会把磁盘全部扫描一遍,而是直接根据内存中的文件描述符,去操作磁盘中对应位置的表结构文件。
但是这两个文件描述符缓存,更多的是为MyISAM引擎设计的,关于具体原因稍后再说。
1.3.1、MySQL8.x为什么移除了查询缓存?
OK~,再简单聊一下QueryCahce查询缓存,这块的设计思想是非常好的,也就是利用热点探测技术,对于一些频繁执行的查询SQL,直接将结果缓存在内存中,之后再次来查询相同数据时,就无需走磁盘,而是直接从查询缓存中获取数据并返回。
听起来似乎还不错呀,好像确实能带来不小的性能提升呢?但实则很鸡肋,为啥?看个例子。
select * from zz_users where user_id=1; select * from zz_users where user_id = 1; 复制代码
比如上述这两条SQL语句,在我们看来是不是一样的?确实,都是在查询ID=1的用户数据,但奇葩的事情出现了,MySQL的查询缓存会把它当做两条不同