MySQL查询缓存,query cache,是MySQL希望能提升查询性能的一个特性,它保存了客户端查询返回的完整结果,当新的客户端查询命中该缓存,MySQL会立即返回结果。
要了解MySQL查询缓存,最好先对MySQL查询执行流程有个基本概念。图1展示了MySQL服务器执行客户端查询查询请求的执行流程。
- 客户端发送一条查询给MySQL服务器;
- MySQL服务器开启了查询缓存开关时,服务器先检查查询缓存,如果命中了缓存,则立即返回存储在缓存中的结果,否则进入下一个阶段(缓存开关关闭或者未命中);
- MySQL服务器进行SQL解析、预处理、再由优化器生成对应的执行计划;
- 执行优化器生成的执行计划,调用存储引擎的API来执行查询;
- 将结果返回给客户端。在这个阶段也会将查询结果存放到查询缓存中;
很明显,设置查询缓的目的就是为了减少MySQL服务器重复执行相同的查询,减小服务器压力。而且查询缓存对客户端是完全透明的,应用程序无须关心MySQL是通过查询缓存返回的结果还是实际执行返回的结果。
虽然查询缓存对客户端透明,但在做查询时还是需要了解查询缓存的工作原理,才能更有效地利用它。主要包括:“MySQL如何判断缓存命中”“MySQL如何失效缓存”“查询缓存的内存管理”。
MySQL如何判断缓存命中
MySQL判断缓存命中的方法很简单:缓存存放在一个引用列表中,通过一个哈希值引用,这个哈希值包括了如下因素:查询本身、当前要查询的数据库、客户端协议的版本等一些其他可能会影响返回结果的信息。
当判断缓存是否命中时,MySQL不会解析、“正规化”或者参数化查询语句,而是直接使用SQL语句和客户端发送过来的其它原始信息。任何字符上的不同,例如空格、注释,都会导致不能命中缓存。所以在编写SQL语句时,需要特别注意这点。
查询缓存中的缓存数据是在查询执行引擎返回查询结果的阶段设置的,但不是所有的查询都会被缓存。查询语句中如果包含一些不确定的数据时,查询结果是不会被缓存的,例如查询语句中包含:NOW()、CURRENT_DATE()等。因为每次执行这类带了不确定数据的查询所返回结果可能是不同的。
MySQL如何失效缓存
写操作会导致查询缓存失效。因为对某个表写入数据的时候,对这个表查询的返回结果可能会发生变化,前面说过MySQL不会解析查询语句,MySQL实现上就是简单粗暴的把这个表的所有缓存都设置失效。
查询缓存的内存管理
查询缓存是完全存储在内存中的。除此之外,还需要缓存很多别的管理维护相关的数据,用来确定哪些内存目前是可用的、哪些是已经用掉的、哪些用来存储数据表和查询结果之前的映射、哪些用来存储查询字符串和查询结果。
MySQL用于查询缓存的内存被分成一个个的数据块,数据块是变长的。每个数据块中,存储了数据块类型、大小和存储数据本身,还外加指向前一个和后一个数据块的指针。数据块类型有:存储查询结果、存储查询和数据表的映射、存储查询文本等。
服务器启动时,先初始化查询缓存需要的内存。这个内存池初始是一个完整的空闲块。当有查询结果需要缓存的时候,MySQL先从大的空间块中申请一个数据块用于存储数据。这个数据块的大小需要大于参数query_cache_min_res_unit的配置,虽然有时实际需要的内存空间并没有那么大。这么做的原因是:MySQL是边计算边返回查询结果的,也就意味着MySQL无法预知查询结果到底有多大,而分配内存块是个非常慢的操作,所以设定了一个申请下限,权衡时间和空间,最大限度满足大多数查询需要申请内存块的需求。
当需要缓存一个查询结果的时候,先选择一个尽可能小的内存块(也可能选择较大的,看不同查询的策略),然后将结果存入其中。如果内存块全部用完了,但仍有剩余数据需要存储,MySQL会申请一个新的数据继续存储查询结果。当查询完成时,MySQL会释放剩余未用完的内存空间。
图2展示了上述的内存分配过程。类似操作系统中的内存管理,当并行多次分配内存之后,数据块之间会产生内存碎片。当query_cache_min_res_unit设置不合理时,会导致查询缓存内存池的内存利用率低。
MySQL查询缓存的目的是为了提升查询性能,但它本身也是有性能开销的。需要在合适的业务场景下(读写压力模型)使用,不合适的业务场景不但不能提升查询性能,查询缓存反而会变成MySQL的瓶颈。
查询缓存的开销主要有:
- 读查询在开始前必须先检查是否命中缓存;
- 如果这个读查询可以被缓存,那么当完成执行后,MySQL若发现查询缓存中没有这个查询,会将其结果存入查询缓存,这会带来额外的系统消耗;
- 当向某个表写入数据的时候,MySQL必须将对应表的所有缓存都设置失效。如果查询缓存非常大或者碎片很多,这个操作就可能带来很大的系统消耗。
通常来说在数据库写占比较大的情况,查询缓存的开销会大于性能提升带来的好处。但大多数业务数据库写都占了较大比例,通过测试发现开启查询缓存会降低MySQL的性能。所以大多数云厂商提供的MySQL实例默认是关闭了查询缓存开关的。例如腾讯云MySQL,查询缓存开关见图3。
查询缓存提供了一些配置参数。参数说明如下:
query_cache_type
是否打开查询缓存。可以设置OFF、ON或DEMAND、DEMAND表示只有在查询语句中明确写入sql_cache的语句才放入查询缓存。这个变量可以是会话级别的也可以是全局级别的。
query_cache_size
查询缓存使用的总内存空间,单位是字节。这个值必须是1024的整倍数,否则实际分配的数据会和指定的大小有区别。
query_cache_min_res_unit
在查询缓存中分配内存块时的最小单位。
query_cache_limit
MySQL能够缓存的最大查询结果。如果查询结果大于这个值,则不会被缓存。因为查询缓存在数据生成的时候就开始尝试缓存数据,所以只有当结果全部返回后,MySQL才知道查询结果是否超出限制。
如果超出,MySQL则增加状态值qcache_not_cached,并将结果从查询缓存中删除。如果你实现知道有很多这样的情况发生,那么建议在查询语句中加入sql_no_cache来避免查询缓存带来的额外消耗。
query_cache_wlock_invalidate
如果某个数据表被其他的链接锁住,是否仍然从查询缓存中返回结果。这个参数默认是OFF,这可能在一定程序上会改变服务器的行为,因为这使得数据库可能返回其他线程锁住的数据。将参数设置成ON,则不会从缓存中读取这类数据,但是这可能会增加锁等待。对于绝大多数应用来说无需注意这个细节,默认的通常没有问题。
MySQL查询缓存虽本意上是提升查询性能,但大多数情况下它反而会成为性能瓶颈,所以我们大多数时候都是把这个特性关闭的。我们也有非常多的查询缓存的替代方案,比如redis,缓存由业务层来处理。
基于这点,在MySQL8.0版本发布时已经把查询缓存特性给去掉了。虽然以后用不上查询缓存了,但是了解了解它的原理和问题还是挺有好处的。
参考文档:
- 《高性能MySQL》
- MySQL 5.7/8.0 Reference Manual