@boothsun
2018-02-18T18:14:05.000000Z
字数 2812
阅读 1290
MySQL
很多数据库产品都能够缓存查询的执行计划,对于相同类型的SQL就可以跳过SQL解析和执行计划生成阶段。MySQL在某些场景下也可以实现,但是MySQL还有一种不同的缓存类型:缓存完整的SELECT查询结果,也就是“查询缓存”。
MySQL查询缓存保存查询返回的完整结果。当查询命中该缓存,MySQL会立刻返回结果,跳过了解析、优化和执行阶段。
查询缓存系统会跟踪查询中涉及的每个表,如果这些表发生变化,那么和这个表相关所有的缓存查询都将失效。这种机制效率看起来比较低,因为数据表变化时很有可能对应的查询结果并没有变更,但是这种简单实现代价很小,而这点对于一个非常繁忙的系统来说非常重要。
很显然,对应频繁更新的表,查询缓存是不适合的,而对于一些不常改变数据且有大量相同SQL查询的表,查询缓存会节约很大的性能。
查询缓存对上游是完全透明的。
缓存存放在一个引用表中,通过一个哈希值引用,这个哈希值包括了如下因素,即查询本身、当前要查询的数据库、客户端协议的版本等一些其他可能会影响返回结果的信息。
当判断缓存是否命中时,MySQL不会解析也不会对查询语句进行任何的改变,而是直接使用SQL语句和客户端发送过来的其他原始信息。任何字符上的不同,例如空格、注释 —— 任何的不同 —— 都会导致缓存的不命中。
当查询语句中有一些不确定的数据时,则不会被缓存。例如包含函数NOW()或者CURRENT_DATE()的查询不会被缓存。类似的,包含CURRENT_USER或者CONNECTION_ID()的查询语句因为会根据不同的用户返回不同的结果,所以也不会被缓存。事实上,如果查询中包含任何用户自定义函数、存储函数、用户变量、临时表、MySQL库中的系统表,或者任何包含列级别权限的表,都不会被缓存。
打开查询缓存对读和写操作都会带来额外的消耗。
MySQL会将用于查询缓存的内存分成一个个的数据块,这些数据块是变长的。每一个数据块中,存储了自己的类型、大小和存储的数据本身,还外加指向前一个和后一个数据块的指针。数据块的类型有:存储查询结果、存储查询和数据表的映射、存储查询文本,等等。不同的存储块,在内存使用上并没有什么不同,从用户角度来看无须区分它们。
当服务器启动的时候,它先初始化查询缓存需要的内存。这个内存池初始是一个完整的空闲块。这个空闲块的大小就是你所配置的查询缓存大小再减去用于维护元数据的数据结构所消耗的空间。
当有查询结果需要缓存的时候,MySQL先从大的空间块中申请一个数据块用于存储结果。这个数据块需要大于参数query_cache_min_res_unit的配置,即使查询结果远远小于此,仍需要至少申请query_cache_min_res_unit空间。因为需要在查询开始返回结果的时候就分配空间,而此时是无法预知查询结果到底多大的,所以MySQL无法为每一个查询结果精确分配大小恰好匹配的缓存空间。
因为需要锁住空间块,然后找到合适大小数据块,所以相对来说,分配内存块是一个非常慢的操作。MySQL尽量避免这个操作的次数。
MySQL的“分配内存块”,并不是指通过函数malloc()向操作系统申请内存,这个操作只在初次创建查询缓存的时候执行一次。这里“分配内存块”是指在空闲列表中找到一个合适的内存块,或者从正在使用的、待淘汰的内存块中回收再利用。也就是说,MySQL自己管理了一大块内存,而不依赖于操作系统的内存管理。
下面我们结合一张图来看看查询结果存储的过程:
当有查询结果需要缓存的时候,先从空闲空间A申请一个数据块B,然后逐步向数据块B写入数据,当数据块B全部用完后,再次向空闲空间A申请一个数据块C,然后又向C写入数据,如此往复,值到查询结果全部写入缓存。在查询结果全部写入完成后,数据块C还剩余部分空间D,这个剩余的空间D将被释放,并入到空闲空间A,而此时不会产生碎片,那么碎片是如何产生的呢?
在上面我们可以看到,当只有一条查询结果需要缓存时,即使分配的内存块在最后仍然有剩余,也不会产生碎片,而是并入到了空闲空间中。我们应当注意到这是在没有并发的情况下,不会产生碎片,而在并发查询的时候就可能产生碎片了。
现在我们假设查询结果都很小,小于query_cache_min_res_unit设置的值,而此时并发产生了两条查询结果,都需要缓存结果。那么此时会有两个数据块在写入数据,写入完成后,MySQL开始回收剩余的数据块空间时会发现,第一个数据块和第二个数据块中间会有一个空隙(这个空隙是由第一个数据块的剩余空间产生),而空隙又小于query_cache_min_res_unit的值不能被再次使用,从而产生了碎片。如下图所示:
我们可以看到在并发的时候,查询1的数据库的剩余空间无法并入到空闲空间中,而又小于query_cache_min_res_unit的值无法再次使用,产生了碎片。查询2的数据块的剩余空间则并入到了空闲空间中,得到了释放。
另外由于缓存失效,可能导致留下太小的数据块无法再后续的缓存中使用,也会产生碎片。
使用查询缓存产生的碎片无法避免,但是我们可以运用以下方式减少碎片,从而减少内存空间的浪费。