我遇到过很多次这种报错,每次帮别人排查的时候,第一反应基本都是:这哥们儿又升级MySQL版本了,而且没清理老配置文件。
Unknown system variable 'query_cache_size'这个问题,说白了就是MySQL 8.0版本把查询缓存(Query Cache)整个功能给删掉了,但你的配置文件里还留着老版本的参数设置,启动时MySQL根本不认识这些参数,自然直接报错退出。今天我把这个问题的来龙去脉、处理方式和避坑经验一次性说清楚。
1. 先说清楚:这个报错到底是怎么来的
1.1 一段前尘往事:查询缓存的兴衰
MySQL的查询缓存功能从很早的版本就存在了,它的核心思想特别朴素:如果连续执行两条一模一样的SELECT语句,而且涉及的表数据在这期间没有任何变化,第二次就可以直接把第一次缓存好的结果集返回,省去重新解析SQL、扫描索引、读数据页的整个过程。
在MySQL 5.1到5.7这个时代,这个概念听起来很诱人,尤其对于读多写少、数据基本不怎么变动的业务场景,打开query_cache_type=1,配置一个合适的query_cache_size,确实能在某些场景下带来明显的性能提升。
但查询缓存的实现有一个致命短板:失效太频繁。只要表上发生了任何写操作(INSERT、UPDATE、DELETE哪怕只影响了一行),这张表的所有查询缓存全部作废。也就是说,在写入稍微频繁一点的业务里,缓存刚缓存好一批结果,还没来得及命中几次,就被一次写操作给清空了,吃力不讨好,反而因为维护缓存还要额外消耗CPU和锁资源。
所以在MySQL 8.0正式发布时,官方直接砍掉了这个模块,query_cache_type、query_cache_size、query_cache_limit这一整批参数全部移除,连带着SQL_CACHE、SQL_NO_CACHE这两个SQL提示符也没了。
1.2 报错的两个典型触发场景
第一个场景是启动服务直接失败。这种情况最常见于把MySQL从5.7升级到8.0,或者新安装的8.0实例使用了从5.7拷贝过来的my.cnf配置文件。只要文件里有类似下面这些参数,MySQL 8.0在初始化阶段就会报错退出:
query_cache_type=1 query_cache_size=67108864 query_cache_limit=1048576第二个场景是执行SQL语句时触发。一些老版本的ORM框架,或者早期封装的数据库中间件,内部逻辑为了“优化性能”会主动执行SET SESSION query_cache_type = ON;这类语句,在8.0上同样会得到一模一样的报错。
注意:你在网上搜索这个报错时,能看到很多文章让你“把MySQL升级到5.7.20以上”,这种说法不仅过时而且有害。5.7.20只是修复了查询缓存相关的内存分配Bug,从未等于“支持Oracle的付费功能”。8.0才是彻底移除,必须用完全不同的思路来解决。
2. 最快的解决办法:让配置和版本对齐
2.1 找出并清理配置文件里的残留参数
排查的第一步,是找到MySQL当前实际读取的配置文件。注意,Linux系统中存在多个候选位置,MySQL会按顺序读取,后读取的会覆盖先读取的同名参数。最常见的路径包括:
/etc/my.cnf/etc/mysql/my.cnf/etc/mysql/mysql.conf.d/mysqld.cnf(Ubuntu/Debian常见)$MYSQL_HOME/my.cnf- 编译安装时指定的
basedir目录下的my.cnf
保险起见,用一个命令把实际生效的配置路径打印出来:
mysqld --verbose --help | grep -A 1 "Default options"输出结果会显示“Default options are read from the following files in the given order”,把每个路径都检查一遍,用grep -n "query_cache"找到带query_cache字样的行,直接注释或删除。
我的习惯是先用grep定位,确认无误后再统一编辑。比如:
grep -rn "query_cache" /etc/my.cnf /etc/mysql/找到之后,把这些行整行删掉,或者用#注释掉,二选一。有些同学舍不得删,想注释掉留给以后参考,完全可以,注释不影响启动。
2.2 处理其他相关的“老参数”
除了query_cache_size,8.0里一起被移除的查询缓存参数还包括:
| 参数名 | 作用 | 8.0中的状态 |
|---|---|---|
| query_cache_type | 查询缓存开关 | 已移除 |
| query_cache_size | 缓存总内存大小 | 已移除 |
| query_cache_limit | 单条结果集缓存上限 | 已移除 |
| query_cache_min_res_unit | 缓存块最小单位 | 已移除 |
| query_cache_wlock_invalidate | 写锁期间是否失效缓存 | 已移除 |
另外还有两个经常和它一起打包出现的老参数也值得检查一遍:key_buffer_size(MyISAM索引缓存,8.0中仅对MyISAM表仍有效,不影响InnoDB)和innodb_buffer_pool_size(这个在8.0中仍然有效且是核心参数,千万别删)。
清理完配置文件后,重启MySQL服务:
systemctl restart mysqld或者:
service mysql restart启动成功后登录进入MySQL,执行:
SHOW VARIABLES LIKE 'query_cache%';返回一个空结果集,就说明彻底干净了。
2.3 如果报错来自应用端SQL语句
如果服务能正常启动,但某个应用连接MySQL时报Unknown system variable 'query_cache_size',说明不是配置文件的问题,而是应用在连接初始化阶段执行了设置查询缓存参数的SQL。排查思路是看两个方向:
第一,应用的数据库连接池配置。以Java的HikariCP、Druid为例,有些老项目的connection-init-sql配置里写入了SET SESSION query_cache_type=ON;这种初始化语句。在MySQL 8.0中这属于非法SQL,直接导致连接初始化失败。解决办法是去掉这段初始化SQL。
第二,ORM框架或中间件版本太旧。比如某些老版本的MyBatis、Hibernate方言配置中带了query_cache相关内容,或者TTL Cache中间件中配置了这类语句,升级到适配MySQL 8.0的版本即可。
判断原则很简单:启动都失败,先查my.cnf;启动正常但运行中报错,先查应用和中间件。两条线不要搞反。
3. 从5.7迁移到8.0时的系统性配置清理
3.1 MySQL 8.0的参数兼容性全局变化
如果你正在规划5.7到8.0的升级,query_cache相关参数只是冰山一角。8.0对参数体系做了大范围的调优和精简,涉及字符集、认证插件、排序规则、系统表结构等方方面面。这里列一个常用清单,升级前对照检查:
| 老参数/配置项 | 5.7状态 | 8.0状态 | 处理建议 |
|---|---|---|---|
| query_cache_* | 可用 | 已移除 | 删除配置,改用业务层缓存或优化SQL |
| sql_mode | 默认宽松 | 默认严格(ONLY_FULL_GROUP_BY, STRICT_TRANS_TABLES等) | 升级前先用5.7测试严格模式兼容性 |
| default_authentication_plugin | mysql_native_password | caching_sha2_password | 老客户端需要升级驱动 |
| default_charset | utf8mb3(即utf8) | utf8mb4 | 需显式指定或接受默认 |
| information_schema_stats_expiry | 无 | 86400(默认) | 统计信息过期导致执行计划漂移,可调整 |
| innodb_file_format | Antelope | Barracuda(默认) | 已自动切换,无需处理 |
| log_bin | 默认关闭 | 默认关闭(但binlog格式默认ROW) | 确认备份恢复策略 |
| explicit_defaults_for_timestamp | 有默认值 | 与5.7一致,行为更规范 | 需注意TIMESTAMP字段默认值行为差异 |
这个表不是让你全背下来,而是提示你:升级不是简单地跑一遍mysql_upgrade就完事,至少要把配置逐项检查一遍。
3.2 拿一台测试机做“试运行”
别直接在线上库执行升级,这个坑我踩过一次之后,再也不敢偷懒。
第一步:准备一台与线上配置接近的虚拟机或容器,安装与现网大版本一致的5.7,导出一份线上数据做恢复测试。
第二步:在测试机上停止MySQL 5.7,备份老数据目录(datadir),把二进制文件替换成8.0,启动时先不加载老配置文件,使用mysqld --initialize初始化一个新的数据目录,再把旧数据通过逻辑备份方式导入。导入完成后,打开一个隐藏配置文件的副本,逐个参数测试,看哪些参数导致MySQL拒绝启动。
第三步:全程观察错误日志(/var/log/mysql/error.log),遇到Unknown system variable就删掉对应行,遇到Unknown storage engine就检查引擎兼容性。实测下来,把5.7换成8.0并全量导入数据,大约需要按顺序处理以下几类问题:
- 查询缓存族参数(本次主题)
- 字符集相关的
character_set_server差异 - 旧版
mysql_native_password认证插件导致的客户端连接失败 - 触发器、函数、存储过程中的SQL模式差异
处理完一轮之后,再把测试机上最终版本干净且可用的配置文件作为线上配置的基准,而不是直接拿老文件改来改去。
3.3 迁移后性能不仅没变差,反而可能更快
很多人以为移除查询缓存会导致读性能大幅下降,但实际上8.0用更底层的机制换回了更稳定的整体表现。InnoDB缓冲池(innodb_buffer_pool_size)把热数据页放在内存中,查询时直接读内存页,本身就是一个天然的“页级缓存”,比查询缓存的结果集序列化存储更高效。
而且8.0的优化器更强了,基于代价的索引选择,配合直方图、倒序索引等新特性,很多原本依赖查询缓存“糊弄性能”的场景,在主键索引或二级索引合理的情况下,根本不需要额外缓存层。迁移之后,我的实测经验是,去掉查询缓存反而减少了全局锁竞争,在写入频率较高的业务里,整体吞吐反而更稳定了。
4. 如果你真的需要缓存:三个替代方案
4.1 应用层缓存(最推荐)
查询缓存没了,业务对“热点查询”的缓存需求并没有消失。最靠谱的方案是把缓存放到应用层,用Redis或Memcached来承载。
考虑一个商品信息查询的场景,原本靠MySQL查询缓存扛住“同一个商品详情页被反复查询”的热点流量,现在可以在应用服务里加一层Redis:
1. 请求进来,先查Redis,Key设计为 product:detail:{productId} 2. Redis命中,直接返回,不访问MySQL 3. Redis未命中,查询MySQL,把结果序列化写入Redis,设置过期时间(比如10分钟) 4. 数据发生变更时,主动更新Redis里的对应Key,或者直接删除Key让下次查询重建这样做的好处远不只是补位:查询缓存原来只能缓存完全相同的SQL语句,而应用层缓存可以基于业务语义设计缓存粒度,同样一份商品数据,字段多几个少几个、SQL里换个排序方式,都能命中同一个Redis Key。而且Redis原生支持分布式,多台应用服务器之间可以共享缓存,这比MySQL单机的查询缓存强太多了。
4.2 MySQL 8.0的线程池和Buffer Pool结合改造
如果你的业务对数据库的整体并发能力不满意,与其寄希望于查询缓存,不如把思路放到8.0的新特性上。innodb_buffer_pool_size设置合理(通常为物理内存的60%-70%),热数据基本都在内存中,配合innodb_buffer_pool_instances把缓冲池分成多个实例,减少内部锁竞争;innodb_io_capacity设为2000(SSD场景),让后台刷脏能力跟得上压力。
这些机制加在一起,比一个随时可能被写操作击穿的查询缓存可靠得多。8.0的InnoDB缓冲池本身就能缓存表和索引的数据页,查询走索引定位到页,直接在内存里取结果,这个路径比“查一次SQL再把结果集拷贝一份存起来”轻量得多。
4.3 代理层缓存(ProxySQL、应用网关)
还有一种思路是在数据库前面加一层读写分离代理,比如ProxySQL。它自带查询缓存功能,且缓存粒度可以做到更细,能够识别同一类查询模板(把参数值替换成通配符),不会因为参数值不同就缓存失效。在业务不想大改的情况下,用代理层做缓存是一个折中方案。
不过我不建议为了“替代查询缓存”专门引入一套代理集群,毕竟加一个中间件就意味着增加一层运维负担和故障点。除非你的架构本来就需要读写分离,顺手把查询缓存挂在代理层,那倒是顺理成章。
5. 兼容性排查:如何正确面对“还能不能设置参数”的疑问
5.1 误报排查看这里:不是所有带“cache”的都不能用
移除的是query_cache_*这个命名空间下的参数,但innodb_buffer_pool_size、key_buffer_size、table_open_cache、thread_cache_size在8.0中都是合法参数,很多刚接触8.0的同学看到“缓存”两个字就草木皆兵,把有用的参数也给删了,这反而矫枉过正。
判断一个参数是否适用,最权威的办法是查官方文档,或者直接在MySQL里执行SHOW VARIABLES LIKE '参数名';,能查到的就是当前版本支持的,查不到的就是被移除或者改名的。比如SHOW VARIABLES LIKE 'thread_cache_size';能正常返回,就说明8.0仍然保留它。
5.2 老代码里的SET语句兼容处理
如果你维护的老项目中,有类似这样的一行代码执行:
SET SESSION query_cache_type = ON;在5.7上可以正常运行,在8.0上直接报错。处理方式分两种:
第一种,代码能改就直接删掉这行。因为查询缓存已经没有了,设置这个session变量根本没有任何意义,删掉后一切都好。
第二种,如果代码是编译好的二进制组件,或者不方便发布新版本,可以做一个兼容性包装:在数据库代理层过滤掉这条SET语句。比如ProxySQL的mysql_query_rules里设置一条规则,把SET SESSION query_cache_type开头的语句直接丢弃(DROP)或者返回OK(OK),就能让老客户端在8.0上正常连接。
还有一种更简单粗暴的做法:把数据库用户隔离,老模块连接专用的一个MySQL 8.0账号,通过init_connect参数强制设置SET SESSION query_cache_type = OFF;。但8.0里query_cache_type已经被移除,这个方法只能用于5.7,在8.0中无效。所以归根结底,还是得靠改代码或者代理层处理。
5.3 5.7和8.0开发的兼容写法
如果你的开发环境是5.7,生产环境已经升级到8.0,测试代码里又涉及到查询缓存的设置,最稳妥的做法是写一个兼容检查逻辑,先探测当前MySQL版本号再决定是否执行SET语句:
import pymysql conn = pymysql.connect(host='localhost', user='user', password='pass', database='test') cursor = conn.cursor() # 判断大版本,只有5.x才需要处理查询缓存相关设置 cursor.execute("SELECT VERSION()") version = cursor.fetchone()[0] if version.startswith("5."): cursor.execute("SET SESSION query_cache_type = OFF") # 注意:5.7虽然支持query_cache_type,但默认也可能被禁用,这里只是示例 cursor.close() conn.close()不过这种判断逻辑属于“临时打补丁”,时间长了代码里都是版本分叉,维护成本很高。更推荐的做法是:开发环境也直接统一到8.0,不要为了模拟老版本行为而在新代码里写兼容逻辑。
6. 完整解决过程实录:一步一步复现处理
我把一次完整的处理过程写下来,方便你照着操作。
场景:一台CentOS服务器,MySQL从5.7停机升级到8.0,启动时直接报错。
第一步,看错误日志:
tail -n 50 /var/log/mysql/error.log日志里有一条很直接的记录:
[ERROR] [MY-000067] [Server] unknown variable 'query_cache_size=67108864'关键信息就是unknown variable,后面跟着参数名,意思就是当前版本不认识这个参数。
第二步,全盘搜索配置文件:
grep -rn "query_cache" /etc/ /etc/mysql/ /usr/my.cnf 2>/dev/null搜索结果显示/etc/my.cnf里存在三行:
query_cache_type=1 query_cache_size=67108864 query_cache_limit=1048576第三步,备份配置文件后再修改:
cp /etc/my.cnf /etc/my.cnf.bak sed -i '/query_cache/d' /etc/my.cnf先备份再改,万一改错了还能回滚,别冒险直接改。
第四步,检查是否有其他不兼容参数。既然是从5.7升上来的,顺手把整个配置里可能不兼容的项都排查一遍。重点关注sql_mode、default-authentication-plugin、innodb_file_format等。
第五步,重新启动:
systemctl restart mysqld启动成功后登录验证:
mysql -uroot -p mysql> SHOW VARIABLES LIKE 'query_cache%'; Empty set (0.01 sec)确认无误后,再简单测试几条常用SQL,确认业务功能正常。如果是新升级的实例,重启后建议加一个快照备份,后续出现异常还能快速回退。
7. 常见问题排查速查表
| 问题现象 | 可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
| 启动MySQL直接报unknown variable query_cache_size | 配置文件残留5.7的参数 | grep -rn "query_cache" /etc/my.cnf | 删除或注释对应行 |
| 服务能启动,但应用连接报错 | 应用或中间件初始化SQL含SET语句 | 检查连接池的connection-init-sql、ORM配置 | 去掉SET语句,或升级中间件版本 |
| 5.7升级8.0后,字符集大面积乱码 | 8.0默认字符集改为utf8mb4 | 查看SHOW VARIABLES LIKE 'character_set%'; | 在配置文件中显式指定character_set_server=utf8mb4 |
| 老客户端连不上MySQL 8.0 | 认证插件从mysql_native_password改为caching_sha2_password | 查看SHOW VARIABLES LIKE 'default_authentication_plugin'; | 升级客户端驱动,或为老用户保留mysql_native_password注1 |
| 删了query_cache参数后,热点查询变慢 | 缺少了曾经的缓存层 | 用EXPLAIN分析执行计划,检查索引 | 按4.1节加应用层缓存,或优化索引与SQL |
| 修改配置文件后参数不生效 | 改了非默认读取路径的配置文件 | mysqld --verbose --help查看实际读取顺序 | 修改实际生效的配置文件,然后重启确认 |
注1:这种方式仅作为过渡,新环境建议直接使用caching_sha2_password,老驱动适配后切换回来。
补充一个容易被忽视的细节:修改配置文件后必须重启MySQL,SET GLOBAL即使能临时改运行时变量,也无法覆盖配置文件里的静态启动参数。更关键的是query_cache_*这类被移除的参数,配置文件里根本没对应运行时变量,不重启等于白改,服务起不来反而成了“改了没效果”的错觉根源。
8. 聊聊我自己的处理习惯:让这次报错以后不再出现
这套流程走下来,核心就是一个思路:版本的升级不只是二进制替换,配置文件的兼容性清理是必做功课。
我个人现在再遇到Unknown system variable系列报错,都会先养成立刻确认版本的习惯。不管是新装8.0还是从老版本升级,第一步永远是mysql --version看清楚当前实例的大版本,然后再去找配置文件的问題。这一步能省掉大量无效排查。
另外,处理完query_cache_size之后,建议顺手把所有来自旧版本的配置项整体过一遍。我在实际操作中发现过很多次:一个环境里残留的旧参数往往不止一个两个,一起处理掉,既能解决当前的启动报错,还能规避后续业务上线时的隐性故障。
最后说一个加分项:在升级或整改结束后,养成把干净可用的my.cnf归档保存的习惯。下次再碰到新环境需要初始化,直接把这份已经验证过的配置拿过去改改路径就能用,比从网上东拼西凑一份配置再慢慢调,省力得多。