我们先来回顾一下交友平台用户表的表结构:
CREATE TABLE `user` (
`id` int(11) NOT NULL,
`user_id` int(8) DEFAULT NULL COMMENT '用户id',
`user_name` varchar(29) DEFAULT NULL COMMENT '用户名',
`user_introduction` varchar(498) DEFAULT NULL COMMENT '用户介绍',
`sex` tinyint(1) DEFAULT NULL COMMENT '性别',
`age` int(3) DEFAULT NULL COMMENT '年龄',
`birthday` date DEFAULT NULL COMMENT '生日',
PRIMARY KEY (`id`),
KEY `index_un_age_sex` (`user_name`,`age`,`sex`),
KEY `index_age_sex` (`age`,`sex`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
其中,user_introduction字段:用户介绍,里面允许用户填写非常长的内容,所以,我将这个字段的设为varchar(498),加上其他字段,单条记录的长度可能就会比较大了,这时,如果执行下面这条SQL:
select user_id, user_name, user_introduction from user where age > 20 and age < 50
假设用户表中已经存储300w条记录,执行上面的SQL,会发生什么情况呢?
对MySQL有初步了解的同学肯定知道Query Cache,它的作用就是缓存查询结果,通过首次查询时,建立SQL与结果的映射关系,相同SQL再次查询时,可以命中Query Cache,以此来提升后续相同查询的效率。
因此,对于上面的SQL查询,MySQL可以在首次执行这条SQL后,将查询结果写入Query Cache,下次相同SQL执行时,可以从Query Cache中取出结果返回。
但是,你有没有想过,如果满足查询条件的用户数超过10w,那么,这10w条记录能否完全写进Query Cache呢?
今天,我就从Query Cache的结构说起,逐步揭晓答案。
在《导读》中我提到MySQL通过建立SQL与查询结果的映射关系来实现再次查询的快速命中,那么,问题来了:为了实现这样的一个映射关系,总得有个结构承载这样的关系吧!那么,MySQL使用什么结构来承载这样的映射关系呢?
或许你已经想到了:HashMap!没错,MySQL的确使用了HashMap来表达SQL与结果集的映射关系。进而我们就很容易想到这个HashMap的Key和Value是什么了。
那么,一条SQL的查询结果又是如何存放在query_cache_block中的呢?下面我们就结合《导读》中的SQL,来看看一个query_cache_block的结构:
如上图所示,一个query_cache_block主要包含3个核心字段:
used:存放结果集的大小。MySQL通过block在内存中的偏移量 + 这个大小来获取结果集。如上图,假设《导读》中SQL查询的结果为<10001, Jack, I’m Jack>,那么,used为这个查询结果的大小。
type:Block的类型。包含{FREE, QUERY, RESULT, RES_CONT, RES_BEG, RES_INCOMPLETE, TABLE, INCOMPLETE}这几种类型。这里我重点讲解QUERY和RESULT,其他类型你可以自行深入了解。
QUERY:表示这个block中存放的是查询语句。为什么要缓存查询语句呢?
在并发场景中,会存在多个会话执行同一条查询语句,因此,为了避免重复构造《导读》中所说的HashMap的Key,MySQL缓存了查询语句的Key,保证查询Query Cache的性能。
RESULT:表示这个block中存放的是查询结果。如上图,《导读》中SQL的查询结果<10001, Jack, I’m Jack>放入block,所以,block类型为RESULT。
n_tables:查询语句使用的表的数量。那么,block又为什么要存表的数量呢?
因为MySQL会缓存table结构,一张table对应一个table结构,多个table结构组成一条链表,MySQL需要维护这条链表增删改查,所以,需要n_tables字段。
现在我们知道了一个query_cache_block的结构了,下面我简称block。
现在有这么一个场景:
已知一个block的大小是1KB,而《导读》中的查询语句得到的结果记录数有10w,它的大小有1MB,那么,显然一个block放不下1MB的结果,此时,MySQL会怎么做呢?
为了能够缓存1MB的查询结果,MySQL设计了一个双向链表,将多个block串联起来,1MB的数据分别放在链表中多个block里。于是,就有了下面的结构:逻辑块链表。
图中,MySQL将多个block通过一个双向链表串联起来,每个block就是我上面讲到的block结构。通过双向链表我们就可以将一条查询语句对应的结果集串联起来。
比如针对《导读》中SQL的查询结果,图中,前两个block分别存放了两个满足查询条件的结果:<10001,Jack,I’m Jack>和<10009,Lisa,I’m Lisa>。同时,两个block通过双向指针串联起来。
还是《导读》中的SQL案例,已知一个block的大小是1K,假设SQL的查询结果为<10001,Jack,I’m Jack>这一条记录,该记录的大小只有100Byte,那么,此时查询结果小于block大小,如果把这个查询结果放到1K的block里,就会浪费1024-100=924 字节的block空间。所以,为了避免block空间的浪费,MySQL又引入了一个新结构:
如上图,下面的物理块就是MySQL为了解决block空间浪费引入的新结构。该结构也是一个多block组成的双向链表。
以《导读》中的SQL为例,已知SQL查询的结果为<10001,Jack,I’m Jack>,那么,将逻辑块链表和物理块链表结合起来,这个结果在block中是如何表达的呢?
讲完了query_cache_block,我想你应该对其有了较清晰的理解。但是,我在上面多次提到一个block的大小,那么,这个block的大小又是如何决定的呢?为什么block的大小是1K,而不是2K,或者3K呢?
要回答这个问题,就要涉及MySQL对block的内存管理了。MySQL为了管理好block,自己设计了一套内存管理机制,叫做query_cache_memory_bin。
下面我就详细讲讲这个query_cache_memory_bin。
MySQL将整个Query Cache划分多层大小不同的多个query_cache_memory_bin(简称bin),如下图:
说明:
steps:为层号,如上图中,从上到下分为0、1、2、3这4层。
bin:每一层由多个bin组成。其中,bin中包含以下几个属性:
size:bin的大小
free_blocks:空闲的query_cache_block链表。每个bin包含一组query_cache_block链表,即逻辑块链表和物理块链表,也就是《Query Cache Block》中我讲到的两个链表组成一组query_cache_block。
每层bin的个数通过下面的公式计算得到:
bin个数 = 上一层bin数量总和 + QUERY_CACHE_MEM_BIN_PARTS_INC) * QUERY_CACHE_MEM_BIN_PARTS_MUL
其中,QUERY_CACHE_MEM_BIN_PARTS_INC = 1 ,QUERY_CACHE_MEM_BIN_PARTS_MUL = 1.2
因此,如上图,得到各层的bin个数如下:
第0层的大小 = query_cache_size >> QUERY_CACHE_MEM_BIN_FIRST_STEP_PWR2 >> QUERY_CACHE_MEM_BIN_STEP_PWR2
其余层的大小 = 上一层的大小 >> QUERY_CACHE_MEM_BIN_STEP_PWR2
其中,QUERY_CACHE_MEM_BIN_FIRST_STEP_PWR2 = 4,QUERY_CACHE_MEM_BIN_STEP_PWR2 = 2
因此,假设query_cache_size = 25600K,那么,得到计算各层的大小如下:
bin的大小 = 层大小 / 每一层bin个数,无法整除向上取整
其中,QUERY_CACHE_MIN_ALLOCATION_UNIT = 512B
因此,如上图,得到各层bin的大小如下:
通过对MySQL管理Query Cache使用内存的讲解,我们应该猜到MySQL是如何给query_cache_block分配内存大小了。我以上图为例,简单说明一下:
由于每个bin中包含一组query_cache_block链表(逻辑块和物理块链表),如果一个block大小为1K,这时,通过遍历bin找到一个大于1K的bin,然后,把该block链接到bin中的free_blocks链表就行了。具体过程,我在下面会详细讲解。
在了解了query_cache_block、query_cache_memory_bin这两种结构之后,我想你对Query Cache在处理时用到的数据结构有了较清晰的理解。那么,结合这两种数据结构,我们再看看Query Cache的几种处理场景及实现原理。
我们结合《导读》中的SQL,先看一下Query Cache写入的过程:
当一个表发生改变时,所有与该表相关的cached queries将失效。一个表发生变化,包含多种语句,比如 INSERT, UPDATE, DELETE, TRUNCATE TABLE,ALTER TABLE, DROP TABLE, 或者 DROP DATABASE。
为了能够快速定位与一张表相关的Query Cache,将这张表相关的Query Cache失效,MySQL设计一个数据结构:Query_cache_block_table。如下图:
这是一个双向链表,对于一条SQL,如果包含多表联接,那么,就可以将这条SQL对应多张表链接起来,再插入这张链表,比如,我们把user和t_user_view(访客表)联接,查询用户访客信息,那么,在图中,假设逻辑块链表存放就是联表查询的结果,因此,我们就看到user表和t_user_view都指向了该逻辑块链表。
我们来看一下这个结构包含的核心属性:
block:与一张表相关的query_cache_block链表。如上图是user表的query_cache_block_table,该block中的block属性指向了逻辑块block链表,该链表中第1个block包含《导读》中SQL的查询结果<10001, Jack, I’m Jack>。
table:同样以user和t_user_view(访客表)联接,查询用户访客信息为例,这时,我对这个访客信息创建了视图,那么,MySQL如何表达表的关系呢?为了解决这个问题,MySQL引入了table,通过这个table记录视图信息,视图来源表都指向这个table来表达表的关系。如上图,user和t_user_view都指向了user_view,来表示user和t_user_view(访客表)对应的视图是user_view。
和Query Cache的HashMap结构一样,为了根据表名可以快速找到对应的query_cache_block,MySQL也设计了一个表名跟query_cache_block映射的HashMap,这样,MySQL就可以根据表名快速找到query_cache_block了。
通过上面这些内容的讲解,我想你应该猜到了一张表变更时,MySQL是如何失效Query Cache的?
如果query_cache_mem_bin中没有足够空间的block存放Result,那么,将触发query_cache_mem_bin的内存淘汰机制。
这里我借用《Cache写入》的过程,一起来看看Query Cache的淘汰机制:
在Cache淘汰这个场景中,我们重点关注一下第6步,我们看下这个场景:
通过上面的场景描述,我们发现如果Result很大,那么,MySQL将不断扫描物理块block,然后,不停地合并block,这是不小的开销,因此,我们要尽量避免这样的开销,保证Query Cache查询的性能。
有什么办法避免这样的开销呢?
我在最后小结的时候回答一下这个问题。
好了,这篇内容我讲了很多东西,现在,我们来总结一下今天讲解的内容:
数据结构 | 说明 |
---|---|
Query_cache_block | 存放了一条SQL的查询结果 |
Query_cache_mem_bin | query_cache_block的内存管理结构 |
Query_cache_block_table | 一张表对应一个block_table,方便快速失效query cache |
最后,我们再回头看一下文章开头的那个问题:10w条用户记录是否可以写入Query Cache?我的回答是:
我们先对用户表的10w记录大小做个计算:
用户表包含user_id(8),user_name(29),user_introduction(498),age(3),sex(1)这几个字段,按字段顺序累加,一条记录的长度为8+30(varchar类型长度可以多存储1或2byte)+500+3+1=542byte,那么,10w条记录最大长度为542 * 10w = 54200000byte。
如果要将10w条记录写入Query Cache,则需要将近54200K大小的Query Cache来存储这10w条记录,而Query Cache大小默认为1M,所以,如果字段user_introduction在业务上非必须出现,请在select子句中排除该字段,减少查询结果集的大小,使结果集可以完全写入Query Cache,**这也是为什么DBA建议开发不要使用select 的原因,但是如果select 取出的字段都不大,查询结果可以完全写入Query Cache,那么,后续相同查询条件的查询性能也是会提升的,。
调大query_cache_size这个MySQL配置参数,如果业务上一定要求select所有字段,而且内存足够用,那么,可以将query_cache_size调至可以容纳10w条用户记录,即54200K。
调大query_cache_min_res_unit这个MySQL配置参数,使MySQL在第一次执行查询并写入Query Cache时,尽可能不要发生过多的bin合并,减少物理块block链表的合并开销。那么,query_cache_min_res_unit调成多少合适呢?
这需要结合具体业务场景综合衡量,比如,在用户中心系统中,一般会有一个会员中心的功能,而这个功能中,用户查询自己的信息是一个高频的查询操作,为了保证这类操作的查询性能,我们势必会将这个查询结果,即单个用户的基本信息写入Query Cache,在我的回答的第1条中,我说过一条用户记录最大长度为542byte,结合10w条用户记录需要54200K的Query Cache,那么,设置query_cache_min_res_unit = 542byte就比较合适了。
这样,有两点好处:
调大query_cache_limit这个MySQL配置参数,我在本章节中没有提到这个参数,它是用来控制Query Cache最大缓存结果集大小的,默认是1M,所以,10w条记录,建议调大这个参数到54200K。
最后,对比前面《Group By 深度优化,真是绝了!》这篇文章,发现MySQL特别喜欢自己实现内存的管理,而不用Linux内核的内存管理机制(比如:伙伴系统),为什么呢?
如果你觉得写得不错,记得点赞哦!
作者:谦虚的小K
来源:https://juejin.cn/post/6965793803420778510