mysql原生max_queries_per_hour不按小时重置,5.7.6+默认禁用校验,8.0+彻底移除运行时逻辑;真限流需用proxysql或应用层redis+lua实现带ttl的计数器。

MySQL 原生的 MAX_QUERIES_PER_HOUR 看起来能实现“每小时限流”,但实际行为和你预期的很可能不一致——它不是按自然小时或滑动窗口重置,而是累计计数器;在 5.7.6+ 默认跳过检查,8.0+ 彻底移除运行时校验。真要可靠限流,得绕开它。
为什么 MAX_QUERIES_PER_HOUR 常常不生效
这不是配置错了,而是机制本身有隐性失效条件:
- MySQL 5.7.6+ 默认禁用
mysql.user表中max_questions字段的运行时检查,即使你写了WITH MAX_QUERIES_PER_HOUR 100,服务端也不会校验 - MySQL 8.0 彻底移除了该字段的运行时逻辑,语法保留只为兼容,
CREATE USER或ALTER USER设置后查mysql.user表能看到值,但连接进来执行 SQL 时完全不触发限流 - 即使旧版本(如 5.7.20)设成功了,它统计的是「用户生命周期内总语句数」,不是“每小时”——达到阈值后永远卡住,除非手动
ALTER USER ... WITH MAX_QUERIES_PER_HOUR 0清零 -
FLUSH PRIVILEGES对CREATE/ALTER USER无效(它自动刷新),但很多人误以为要刷,结果白忙活
怎么验证你当前的 MAX_QUERIES_PER_HOUR 是否真在工作
别只看命令是否执行成功,要实测:
- 用目标用户连上 MySQL,反复执行
SELECT 1(至少比设置值多 2–3 次) - 如果没报
ERROR 1226 (42000): User 'xxx' has exceeded the 'max_questions' resource,说明限流根本没启用 - 查表确认:运行
SELECT user, host, max_questions FROM mysql.user WHERE user = 'your_user';,值存在 ≠ 生效 - 注意拼写:写成
MAX_QUERY_PER_HOUR(少 s)或MAX_QUESTIONS_PER_HOUR(旧别名)会导致静默忽略或语法错误
真正能落地的替代方案:ProxySQL + 时间窗口计数
这是目前最轻量、可控、且适配 MySQL 生态的生产级方案。核心是把限流逻辑从服务端移到代理层:
- 在
mysql_query_rules中加规则,匹配用户名 + SQL 类型(如^SELECT),并开启apply_rate_limit(ProxySQL 2.4+ 支持) - 用
stats_mysql_commands_counters表按小时聚合Questions计数,配合外部脚本每小时清零或告警 - 更稳的做法:用 Lua 脚本在 ProxySQL 内部维护 Redis 键
user:app_user:hourly_qps,每次查询前INCR并EXPIRE 3600,超阈值直接拒绝 - 避免踩坑:不要依赖
mysql_query_rules.cache_ttl模拟时间窗口——它只控制缓存,不阻断请求
应用层自己控:Redis + Lua 原子计数最灵活
如果你无法引入 ProxySQL,或者需要精确控制窗口(比如滑动 60 分钟而非整点),应用层是最直接的选择:
- 每次查询前,执行 Lua 脚本:
local c = redis.call("INCR", KEYS[1]); redis.call("EXPIRE", KEYS[1], ARGV[1]); return c - KEYS[1] 是
user:api_user:qps_window,ARGV[1] 是 3600 - 返回值 > 阈值(如 100)就拒绝本次查询,返回 HTTP 429 或数据库错误
- 注意 ORM 隐式查询:Spring Boot 启动时的
SELECT DATABASE()、SHOW VARIABLES也会走这套逻辑,得提前预留额度 - 连接池复用同一个账号时,所有连接发出的语句都会计入同一计数器——这点和原生
MAX_QUERIES_PER_HOUR行为一致,但你能掌控重置时机
关键点在于:MySQL 服务端没有真正的“每小时重置”能力。所谓“每小时限制”,本质是外部系统维护一个带 TTL 的计数器。别在 mysql.user 表里死磕,那条路在 5.7.6+ 之后基本走不通了。











