Skip to content

Commit 8627578

Browse files
committed
docs:优化MySQL执行计划分析+MySQL查询缓存详解
1 parent de0f5f5 commit 8627578

File tree

4 files changed

+156
-47
lines changed

4 files changed

+156
-47
lines changed

docs/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
home: true
33
icon: home
44
title: JavaGuide(Java 面试 & 后端通用面试指南)
5-
description: JavaGuide 是一份面向后端学习与面试的指南,以 Java 面试为核心,同时覆盖数据库/MySQL、Redis、分布式、高并发、高可用、系统设计等通用后端知识,适用于校招/社招复习。
5+
description: JavaGuide 是一份 Java 面试和后端通用面试指南,同时覆盖数据库/MySQL、Redis、分布式、高并发、高可用、系统设计等通用后端知识,适用于校招/社招复习。
66
heroImage: /logo.svg
77
heroText: JavaGuide
88
tagline: Java 面试 & 后端通用面试指南,覆盖计算机基础、数据库、分布式、高并发与系统设计

docs/database/mysql/mysql-query-cache.md

Lines changed: 52 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ head:
1010
content: MySQL查询缓存,Query Cache,MySQL缓存机制,缓存失效,MySQL 8.0,查询性能优化,MySQL内存管理
1111
---
1212

13-
缓存是一个有效且实用的系统性能优化的手段,不论是操作系统还是各种软件和网站或多或少都用到了缓存
13+
缓存是一个有效且实用的系统性能优化手段,无论是操作系统,还是各类应用软件与 Web 服务,均广泛采用了缓存机制
1414

1515
然而,有经验的 DBA 都建议生产环境中把 MySQL 自带的 Query Cache(查询缓存)给关掉。而且,从 MySQL 5.7.20 开始,就已经默认弃用查询缓存了。在 MySQL 8.0 及之后,更是直接删除了查询缓存的功能。
1616

@@ -73,58 +73,58 @@ mysql> show variables like '%query_cache%';
7373

7474
我们这里对 8.0 版本之前`show variables like '%query_cache%';`命令打印出来的信息进行解释。
7575

76-
- **`have_query_cache`** 该 MySQL Server 是否支持查询缓存,如果是 YES 表示支持,否则则是不支持
76+
- **`have_query_cache`** 该 MySQL Server 是否支持查询缓存,如果是 YES 表示支持,否则表示不支持
7777
- **`query_cache_limit`** MySQL 查询缓存的最大查询结果,查询结果大于该值时不会被缓存。
78-
- **`query_cache_min_res_unit`** 查询缓存分配的最小块的大小(字节)。当查询进行的时候,MySQL 把查询结果保存在查询缓存中,但如果要保存的结果比较大,超过 `query_cache_min_res_unit` 的值 ,这时候 MySQL 将一边检索结果,一边进行保存结果,也就是说,有可能在一次查询中,MySQL 要进行多次内存分配的操作。适当的调节 `query_cache_min_res_unit` 可以优化内存。
79-
- **`query_cache_size`** 为缓存查询结果分配的内存的数量,单位是字节,且数值必须是 1024 的整数倍。默认值是 0,即禁用查询缓存
78+
- **`query_cache_min_res_unit`** 查询缓存分配的最小块的大小(字节)。当查询进行的时候,MySQL 把查询结果保存在查询缓存中,但如果要保存的结果比较大,超过 `query_cache_min_res_unit` 的值,此时 MySQL 将在检索结果的同时保存数据,也就是说,有可能在一次查询中,MySQL 要进行多次内存分配的操作。适当的调节 `query_cache_min_res_unit` 可以优化内存。
79+
- **`query_cache_size`** 为缓存查询结果分配的内存的数量,单位是字节,且数值必须是 1024 的整数倍。MySQL 5.7 官方文档显示默认值为 `1048576`(1 MB),设置为 0 时禁用查询缓存。不同小版本的默认值存在差异,建议在配置文件中显式指定,不依赖默认行为
8080
- **`query_cache_type`** 设置查询缓存类型,默认为 ON。设置 GLOBAL 值可以设置后面的所有客户端连接的类型。客户端可以设置 SESSION 值以影响他们自己对查询缓存的使用。
81-
- **`query_cache_wlock_invalidate`**:如果某个表被锁住,是否返回缓存中的数据,默认关闭,也是建议的
81+
- **`query_cache_wlock_invalidate`**:如果某个表被锁住,是否返回缓存中的数据,默认处于关闭状态,生产环境通常建议保持此默认配置
8282

83-
`query_cache_type` 可能的值(修改 `query_cache_type` 需要重启 MySQL Server)
83+
`query_cache_type` 可能的值`query_cache_type` MySQL 5.6/5.7 中是动态变量,**但有前提**:若实例启动时 `query_cache_type=0`,服务器会跳过查询缓存互斥锁的分配,此时通过 `SET GLOBAL` 动态修改将报错,必须修改配置文件并重启;若启动时非 0,则可通过 `SET GLOBAL query_cache_type=N` 在线生效,无需重启)
8484

8585
- 0 或 OFF:关闭查询功能。
8686
- 1 或 ON:开启查询缓存功能,但不缓存 `Select SQL_NO_CACHE` 开头的查询。
8787
- 2 或 DEMAND:开启查询缓存功能,但仅缓存 `Select SQL_CACHE` 开头的查询。
8888

8989
**建议**
9090

91-
- `query_cache_size`不建议设置的过大。过大的空间不但挤占实例其他内存结构的空间,而且会增加在缓存中搜索的开销。建议根据实例规格,初始值设置为 10MB 到 100MB 之间的值,而后根据运行使用情况调整。
92-
- 建议通过调整 `query_cache_size` 的值来开启、关闭查询缓存,因为修改`query_cache_type` 参数需要重启 MySQL Server 生效
91+
- `query_cache_size` 不建议设置得过大。过大的空间不但挤占实例其他内存结构的空间,而且会增加在缓存中搜索的开销。建议根据实例规格,初始值设置为 10MB 到 100MB 之间的值,而后根据运行使用情况调整。
92+
- 建议通过将 `query_cache_size` 设置为 0 来禁用查询缓存,而非仅依赖 `query_cache_type`。两者虽都是动态变量,但 `query_cache_size=0` 会完全跳过缓存内存分配和检查路径,禁用更彻底
9393

9494
8.0 版本之前,`my.cnf` 加入以下配置,重启 MySQL 开启查询缓存
9595

9696
```properties
9797
query_cache_type=1
98-
query_cache_size=600000
98+
query_cache_size=614400
9999
```
100100

101-
或者,MySQL 执行以下命令也可以开启查询缓存
101+
或者,当实例启动时 `query_cache_type` 非 0 的情况下,也可以通过以下命令在线开启查询缓存(若启动值为 0 则该命令会报错,需修改配置文件后重启):
102102

103-
```properties
104-
set global query_cache_type=1;
105-
set global query_cache_size=600000;
103+
```sql
104+
set global query_cache_type=1;
105+
set global query_cache_size=614400;
106106
```
107107

108108
手动清理缓存可以使用下面三个 SQL:
109109

110110
- `flush query cache;`:清理查询缓存内存碎片。
111111
- `reset query cache;`:从查询缓存中移除所有查询。
112-
- `flush tables` 关闭所有打开的表,同时该操作会清空查询缓存中的内容。
112+
- `flush tables;` 关闭所有打开的表,同时该操作会清空查询缓存中的内容。
113113

114114
## MySQL 缓存机制
115115

116116
### 缓存规则
117117

118-
- 查询缓存会将查询语句和结果集保存到内存(一般是 key-value 的形式,key 是查询语句,value 是查询的结果集),下次再查直接从内存中取。
118+
- 查询缓存会将查询语句和结果集保存到内存(一般是 key-value 的形式,其中 Key 是由查询语句文本、当前所在的 Database、客户端字符集以及协议版本等环境参数共同计算生成的 Hash 值,Value 则是查询的结果集),下次再查直接从内存中取。
119119
- 缓存的结果是通过 sessions 共享的,所以一个 client 查询的缓存结果,另一个 client 也可以使用。
120-
- SQL 必须完全一致才会导致查询缓存命中(大小写、空格、使用的数据库、协议版本、字符集等必须一致)。检查查询缓存时,MySQL Server 不会对 SQL 做任何处理,它精确的使用客户端传来的查询
120+
- SQL 必须完全一致才会导致查询缓存命中(大小写、空格、使用的数据库、协议版本、字符集等必须一致)。检查查询缓存时,MySQL Server 不会对 SQL 做任何处理,它精确地使用客户端传来的查询
121121
- 不缓存查询中的子查询结果集,仅缓存查询最终结果集。
122122
- 不确定的函数将永远不会被缓存, 比如 `now()``curdate()``last_insert_id()``rand()` 等。
123123
- 不缓存产生告警(Warnings)的查询。
124-
- 太大的结果集不会被缓存 (< query_cache_limit)
124+
- 结果集超过 `query_cache_limit`(默认 1 MB)时不会被缓存
125125
- 如果查询中包含任何用户自定义函数、存储函数、用户变量、临时表、MySQL 库中的系统表,其查询结果也不会被缓存。
126126
- 缓存建立之后,MySQL 的查询缓存系统会跟踪查询中涉及的每张表,如果这些表(数据或结构)发生变化,那么和这张表相关的所有缓存数据都将失效。
127-
- MySQL 缓存在分库分表环境下是不起作用的
127+
- MySQL 缓存在分库分表环境下几乎不起作用。原因在于:查询通常经由中间件(如 ShardingSphere、MyCat)路由到不同的 MySQL 实例,各实例维护各自独立的 Query Cache;中间件在路由时往往会改写 SQL(添加分片键条件等),导致改写后的语句与原始语句 Hash 值不一致,缓存无法命中
128128
- 不缓存使用 `SQL_NO_CACHE` 的查询。
129129
- ……
130130

@@ -141,22 +141,22 @@ SELECT SQL_NO_CACHE id, name FROM customer;# 不会缓存
141141

142142
MySQL 查询缓存使用内存池技术,自己管理内存释放和分配,而不是通过操作系统。内存池使用的基本单位是变长的 block, 用来存储类型、大小、数据等信息。一个结果集的缓存通过链表把这些 block 串起来。block 最短长度为 `query_cache_min_res_unit`
143143

144-
当服务器启动的时候,会初始化缓存需要的内存,是一个完整的空闲块。当查询结果需要缓存的时候,先从空闲块中申请一个数据块为参数 `query_cache_min_res_unit` 配置的空间,即使缓存数据很小,申请数据块也是这个,因为查询开始返回结果的时候就分配空间,此时无法预知结果多大
144+
当服务器启动的时候,会初始化缓存需要的内存,是一个完整的空闲块。当查询开始返回结果时,由于此时无法预知完整的结果集有多大,MySQL 会先向内存池申请一个大小为 `query_cache_min_res_unit` 的基础数据块。如果结果集超出该块容量,则会在生成结果的过程中持续按需申请新的数据块,并将其通过链表拼接起来
145145

146146
分配内存块需要先锁住空间块,所以操作很慢,MySQL 会尽量避免这个操作,选择尽可能小的内存块,如果不够,继续申请,如果存储完时有空余则释放多余的。
147147

148-
但是如果并发的操作,余下的需要回收的空间很小,小于 `query_cache_min_res_unit`,不能再次被使用,就会产生碎片
148+
随着并发读写的进行,不同大小的缓存块被无序且随机地释放,加上分配时剩余的微小空间(小于 `query_cache_min_res_unit`)无法被复用,内存池中会迅速产生大量不连续的空闲内存块(类似操作系统层面的外部碎片),进而触发更频繁的内存整理消耗
149149

150150
## MySQL 查询缓存的优缺点
151151

152152
**优点:**
153153

154154
- 查询缓存的查询,发生在 MySQL 接收到客户端的查询请求、查询权限验证之后和查询 SQL 解析之前。也就是说,当 MySQL 接收到客户端的查询 SQL 之后,仅仅只需要对其进行相应的权限验证之后,就会通过查询缓存来查找结果,甚至都不需要经过 Optimizer 模块进行执行计划的分析优化,更不需要发生任何存储引擎的交互。
155-
- 由于查询缓存是基于内存的,直接从内存中返回相应的查询结果,因此减少了大量的磁盘 I/O 和 CPU 计算,导致效率非常高
155+
- 由于查询缓存是基于内存的,直接从内存中返回相应的查询结果,因此减少了大量的磁盘 I/O 和 CPU 计算**但此优势仅在低并发且读多写少的静态场景下成立**;在多核高并发环境下,`LOCK_query_cache` 全局互斥锁的激烈竞争会导致大量线程处于等锁状态(可通过 `SHOW PROCESSLIST` 看到 `Waiting for query cache lock`),实际 TPS/QPS 反而大幅下降
156156

157157
**缺点:**
158158

159-
- MySQL 会对每条接收到的 SELECT 类型的查询进行 Hash 计算,然后查找这个查询的缓存结果是否存在。虽然 Hash 计算和查找的效率已经足够高了,一条查询语句所带来的开销可以忽略,但一旦涉及到高并发,有成千上万条查询语句时,hash 计算和查找所带来的开销就必须重视了
159+
- MySQL 会对每条接收到的 SELECT 类型的查询进行 Hash 计算,然后查找这个查询的缓存结果是否存在。虽然 Hash 计算和查找本身的 CPU 开销微乎其微,但 Query Cache 底层依赖单一全局互斥锁(`LOCK_query_cache`)来保证并发安全。一旦涉及到高并发,成千上万条查询语句同时争抢该互斥锁进行缓存检查或写入,极其激烈的锁冲突和线程上下文切换开销将成为致命的性能瓶颈
160160
- 查询缓存的失效问题。如果表的变更比较频繁,则会造成查询缓存的失效率非常高。表的变更不仅仅指表中的数据发生变化,还包括表结构或者索引的任何变化。
161161
- 查询语句不同,但查询结果相同的查询都会被缓存,这样便会造成内存资源的过度消耗。查询语句的字符大小写、空格或者注释的不同,查询缓存都会认为是不同的查询(因为他们的 Hash 值会不同)。
162162
- 相关系统变量设置不合理会造成大量的内存碎片,这样便会导致查询缓存频繁清理内存。
@@ -165,14 +165,38 @@ MySQL 查询缓存使用内存池技术,自己管理内存释放和分配,
165165

166166
在 MySQL Server 中打开查询缓存对数据库的读和写都会带来额外的消耗:
167167

168-
- 读查询开始之前必须检查是否命中缓存。
169-
- 如果读查询可以缓存,那么执行完查询操作后,会查询结果和查询语句写入缓存。
170-
- 当向某个表写入数据的时候,必须将这个表所有的缓存设置为失效,如果缓存空间很大,则消耗也会很大,可能使系统僵死一段时间,因为这个操作是靠全局锁操作来保护的。
171-
- 对 InnoDB 表,当修改一个表时,设置了缓存失效,但是多版本特性会暂时将这修改对其他事务屏蔽,在这个事务提交之前,所有查询都无法使用缓存,直到这个事务被提交,所以长时间的事务,会大大降低查询缓存的命中。
168+
- **读操作需持锁检查**:读查询开始前必须检查缓存命中,这需要获取 `LOCK_query_cache` 共享锁。高并发下,大量读请求同时争抢锁会形成排队。
169+
- **缓存写入开销**:若读查询可缓存,执行后需将结果写入缓存,涉及内存分配和链表拼接操作,同样需要持有锁。
170+
- **写操作触发全局失效**:向表写入数据时,必须使该表所有缓存失效。这需要获取独占锁扫描整个缓存区,`query_cache_size` 越大持锁时间越长。Query Cache 的单一全局互斥锁设计导致写操作会阻塞所有其他读写请求,这也是 MySQL 8.0 移除它的首要原因。
171+
- **InnoDB 长事务加剧问题**:MVCC 特性下,事务提交前相关缓存无法使用。长事务不仅降低缓存命中率,写操作触发的独占锁还会阻塞对**其他不相关表**的缓存读取。
172+
173+
可以通过以下命令查看查询缓存的使用情况,判断是否值得开启:
174+
175+
```sql
176+
SHOW STATUS LIKE 'Qcache%';
177+
```
178+
179+
关键指标说明:
180+
181+
| 状态变量 | 含义 |
182+
| :--------------------- | :----------------------------------------------------------------- |
183+
| `Qcache_hits` | 缓存命中次数 |
184+
| `Qcache_inserts` | 写入缓存的查询次数 |
185+
| `Qcache_not_cached` | 未被缓存的查询次数(不可缓存或未命中) |
186+
| `Qcache_lowmem_prunes` | 因内存不足而被淘汰的缓存条目数,持续升高说明缓存空间不足或碎片严重 |
187+
| `Qcache_free_memory` | 缓存剩余空闲内存(字节) |
188+
189+
命中率参考公式:
190+
191+
```
192+
命中率 = Qcache_hits / (Qcache_hits + Qcache_inserts + Qcache_not_cached)
193+
```
194+
195+
若命中率长期低于 50%,说明工作负载不适合 Query Cache,建议关闭。此外,还需关注 `Qcache_lowmem_prunes``Qcache_inserts` 的比值:若比值极高,意味着刚写入缓存的数据很快因内存碎片或空间不足被剔除,此时开启缓存是纯负收益。`Qcache_lowmem_prunes` 持续增长时,可执行 `FLUSH QUERY CACHE` 整理内存碎片,或适当降低 `query_cache_min_res_unit` 的值。
172196

173197
## 总结
174198

175-
MySQL 中的查询缓存虽然能够提升数据库的查询性能,但是查询同时也带来了额外的开销,每次查询后都要做一次缓存操作,失效后还要销毁。
199+
MySQL 中的查询缓存虽然能够提升数据库的查询性能,但查询缓存机制本身也引入了额外的管理开销,每次查询后都要做一次缓存操作,失效后还要销毁。
176200

177201
查询缓存是一个适用较少情况的缓存机制。如果你的应用对数据库的更新很少,那么查询缓存将会作用显著。比较典型的如博客系统,一般博客更新相对较慢,数据表相对稳定不变,这时候查询缓存的作用会比较明显。
178202

@@ -182,7 +206,7 @@ MySQL 中的查询缓存虽然能够提升数据库的查询性能,但是查
182206
- 查询(Select)重复度高。
183207
- 查询结果集小于 1 MB。
184208

185-
对于一个更新频繁的系统来说,查询缓存缓存的作用是很微小的,在某些情况下开启查询缓存会带来性能的下降。
209+
对于一个更新频繁的系统来说,查询缓存的作用是很微小的,在某些情况下开启查询缓存会带来性能的下降。
186210

187211
简单总结一下查询缓存不适用的场景:
188212

0 commit comments

Comments
 (0)