mysql 8.0+ 中 max_queries_per_hour 已彻底失效,服务端移除校验逻辑,执行 alter user 后仍不触发 error 1226;5.7 默认跳过检查,仅靠启动参数勉强启用但极不稳定;真正可靠方案是 proxysql 中间件或应用层 redis+lua 原子计数。

MySQL 8.0+ 中 MAX_QUERIES_PER_HOUR 已失效,不能靠它实现每小时查询限流;5.7 默认不生效,强行启用极不稳定。真要落地,必须用 ProxySQL 或应用层 Redis + Lua。
为什么 MAX_QUERIES_PER_HOUR 在大多数 MySQL 部署里形同虚设
它不是“配置了就生效”的功能,而是依赖服务端运行时校验逻辑——MySQL 8.0 彻底移除了对 max_questions 字段的检查代码,哪怕你成功执行了 ALTER USER 'u'@'%' WITH MAX_QUERIES_PER_HOUR 60,后续所有 SELECT 1 仍畅通无阻,不会触发 ERROR 1226 (42000)。5.7 虽保留字段,但默认跳过校验;只有加启动参数(如 --old-passwords=0)并配合“唤醒式”操作才可能激活,但官方不推荐、线上难维护。
更关键的是:这个值在 mysql.user 表里能查到,只是“存着好看”,服务端根本不读它做判断。
别试 GRANT ... WITH MAX_QUERIES_PER_HOUR,语法已废弃
MySQL 8.0+ 明确标记该 GRANT 用法为废弃,执行会报错或静默忽略。唯一合法入口是 CREATE USER 或 ALTER USER 语句中的 WITH 子句,例如:
CREATE USER 'api_user'@'%' IDENTIFIED BY 'pwd' WITH MAX_QUERIES_PER_HOUR 120;
但再次强调:这行命令在 8.0+ 里只是语法兼容占位,不产生实际限流效果。
- 拼写错误常见:
MAX_QUERY_PER_HOUR(少 s)、MAX_QUESTIONS_PER_HOUR(旧别名,5.7+ 不再识别)都会导致失败 - 设为
0才表示不限制;留空、省略或设为NULL都不等于“解除限制” - 隐式查询(如 ORM 启动时的
SELECT DATABASE()、SHOW VARIABLES)全被计入额度,容易未业务先超限
真正能上线的两个方案:ProxySQL 和 Redis + Lua
二者都绕开了 MySQL 自身不可靠的资源限制机制,把计数逻辑放在可控位置:
-
ProxySQL 方案:适合不想改应用、需灰度控制的场景。在
mysql_query_rules中按username匹配^SELECT类 SQL,结合stats_mysql_commands_counters轮询统计每小时Com_select增量,超阈值后用KILL终止会话。注意它不支持原生滑动窗口,需自行轮询 + 清零或用 Lua 脚本模拟 -
Redis + Lua 方案:适合已有 Redis 的应用。key 设为
user:api_user:hourly_qps,每次查询前执行原子脚本:INCR并EXPIRE 3600,返回值 > 阈值则拒绝。必须复用 Redis 连接池,否则网络延迟会拖慢所有 MySQL 查询
无论选哪个,上线前务必用真实流量验证:循环执行 SELECT 1,确认是否真返回 ERROR 1226(ProxySQL)或被应用层拦截(Redis),不能只看 SHOW GRANTS FOR 'user'@'%' 显示配置存在就认为 OK。
时间窗口和统计粒度常被忽略的关键点
MAX_QUERIES_PER_HOUR(如果真生效)统计的是“任意连续 60 分钟内语句总数”,不是自然小时;而 MAX_UPDATES_PER_HOUR 等却是按自然小时(整点起)滚动重置——这两个行为在文档里混说,实操中极易混淆。更重要的是:它们都不区分 SQL 内容、WHERE 条件或执行耗时,一条慢 SELECT 和一条快 SELECT 消耗相同额度;存储过程内部的 SELECT 也不计入调用者额度,只算最外层语句。这些边界情况,不压测根本发现不了。











