编程 MySQL 报 1461 不是连接数问题:max_prepared_stmt_count 的 16382 配额被谁吃掉了

2026-10-25 00:05:49

MySQL 报 1461 时,先找到占用 max_prepared_stmt_count 配额的连接

报错文本,以及它不是连接数问题

两行典型输出:

mysql_stmt_prepare failed! error(1461) Can't create more than max_prepared_stmt_count statements (current value: 16382)
SQLSTATE[42000]: Syntax error or access violation: 1461 Can't create more than max_prepared_stmt_count statements (current value: 16382)

SQLSTATE 42000,错误名 ER_MAX_PREPARED_STMT_COUNT_REACHED。42000 属于语法或访问违规类,容易让人先去检查 SQL 写法;而 1461 的实际含义是配额已经用满。

两个常见误判。第一,别把它当成连接数问题:连接数完全正常,照样会报 1461。第二,它也不属于某个会话的内部问题:配额是全局的,一个会话把额度用满,所有会话都建不了新语句。

代理层是另一回事,不是这个报错。

配额怎么算

  • 是全局配额,不是每连接配额。
  • 默认 16382,不是 16384,少 2。
  • 有效范围 0 到 1048576,动态参数,改完立即生效,不用重启。
  • 每条 prepared statement 至少占 8KB 内存。
  • 设成 0 表示禁止使用 prepared statement。
  • 设计目的之一是防 DoS:不限量地打开 prepared statement 足以耗尽内存。

服务端预处理走二进制协议 COM_STMT_PREPARE / COM_STMT_EXECUTE,C API mysql_stmt_prepare、Connector/J、Connector/NET、mysqli 都用这条路。SQL 级接口 PREPARE / EXECUTE / DEALLOCATE PREPARE 效率低一些,但计入同一个上限,别以为换个写法就绕开了。

prepared statement 是会话级的;会话被 kill 而没 deallocate 时,服务器会自动 deallocate。但在会话内部它是“全局”的:存储例程里创建的语句,在例程结束时不会释放。

prepare 阶段解析 query,把 stmt 放进 thd->stmt_map,返回 STMT_ID、参数个数和结果集 metadata;execute 阶段只发 STMT_ID、NULL 位图和参数值,不重新解析。注意 prepare 阶段会 open 语句涉及的表(不取锁),DDL 会被阻塞一下。元数据变化(DDL)后,下次执行会自动 reprepare。

常见几类消耗配额的情况

1. 服务端预处理开了,客户端语句缓存没开。 JDBC 只写 useServerPrepStmts=true 而缺 cachePrepStmts=true,每次 prepareStatement() 都发一次 COM_STMT_PREPARE 新建一条,同一连接上同一条 SQL 也一样。补上 cachePrepStmts=true,同时看两个容易踩的默认值:prepStmtCacheSize 默认 25 偏小,prepStmtCacheSqlLimit 默认 256,长 SQL 直接不进缓存。cacheResultSetMetadata=true 视情况加。

2. SQL 文本唯一化,缓存永远不命中。 把值拼进 SQL(WHERE id = 1 而不是 WHERE id = ?)、动态表名、动态 IN 列表长度变化、每行带不同注释——缓存按 SQL 文本命中,文本一变就是一条新语句。

3. prepare 后不 close。 Go 里 stmt 不 Close、Java 不 close()、Laravel/Eloquent 在循环里不断产生新语句(StackOverflow 46311538 的案例:循环跑到撞上 16382,最后用 DB::getPdo()->exec()/query() 绕开)。

4. 连接活得太久。 HikariCP max-lifetime 30 分钟甚至不限,连接活几小时,泄漏一点点累积。dba.stackexchange 283511 是个 C3P0 案例(MySQL 8.0.21 / Connector 8.0.18),把 maxIdleTimeExcessConnections 设成 300 后释放了 10GB 内存,但作者自己承认这只是权宜之计。

5. 压测。 sysbench 1.0.20 需要的上限(阿里云文档,来自源码分析):oltp_read_only ≥ threads×tables×5 + threads;oltp_write_only ≥ threads×tables×4 + threads;oltp_read_write ≥ threads×tables×9 + threads;oltp_insert 不用 prepared statement。256 线程 × 100 表的 read_only 是 128256,压测前就得调大。

估算参照公开案例:500 连接 × 每连接 50 条 = 25000,已经超过 16382。某线上实例跑到 15847/16382,97%。单连接最多出现过 2000+ 条语句,空闲几小时的连接仍持有数千条。内存口径:元数据 1–2KB + 执行计划缓存 2–10KB,参数缓冲不定,50000 条大约 250–500MB(他人博客口径,未实测)。

观测与定位顺序

先确认当前值和当前总量:

SHOW GLOBAL VARIABLES LIKE 'max_prepared_stmt_count';
SHOW GLOBAL STATUS LIKE 'Prepared_stmt_count';
SHOW GLOBAL STATUS LIKE 'com_stmt%';

Prepared_stmt_count 接近上限就该报警。Com_stmt% 看 prepare / execute / close 的差值。经典案例(iammysql,MySQL 5.5):Com_stmt_prepare 34159,Com_stmt_close 0,Com_stmt_execute 43795——大量 prepare 之后根本没有 close。这个形态基本能定性。

但 Prepared_stmt_count 只是全局计数,它不告诉你是哪个连接吃掉的。要按连接排行,用 performance_schema.prepared_statements_instances,每条语句一行,OWNER_THREAD_ID 关联 performance_schema.threads:

SELECT t.PROCESSLIST_ID, t.PROCESSLIST_USER, t.PROCESSLIST_HOST,
       COUNT(*) AS stmt_count, SUM(p.SUM_TIMER_EXECUTE) AS total_exec
FROM performance_schema.prepared_statements_instances p
JOIN performance_schema.threads t ON p.OWNER_THREAD_ID = t.THREAD_ID
GROUP BY t.PROCESSLIST_ID, t.PROCESSLIST_USER, t.PROCESSLIST_HOST
HAVING stmt_count > 50
ORDER BY stmt_count DESC;

没有 performance_schema 的话,退到 general log 找 Prepare 语句定位应用,或者 tcpdump 抓 COM_STMT_PREPARE。instrument 没开时上面这条查询查不到东西(未实测)。

一个反例值得记住:Com_stmt_reprepare 只增不减是正常行为,它是计数器不是存量。bugs.mysql.com #107243 的提交者把它当泄漏报上去,MySQL 验证团队回复 not a bug,可以调大变量。

止血动作是 SET GLOBAL max_prepared_stmt_count=1000000。另外,kill 掉持有语句的会话,Prepared_stmt_count 会掉下来(blog.mrpk.info 的案例:kill 两个会话后从 16382 掉到 0)。

处理顺序

先调大配额止血:

SET GLOBAL max_prepared_stmt_count = 65535;

动态生效,不用重启。托管库走参数组或控制台,危险操作走变更流程。华为云的建议值是 65535,阿里云提醒压测完记得调回去。

然后修根因:补客户端语句缓存、把拼接的值改成占位符、循环里 close、缩短连接寿命。最后一条要注意 wait_timeout 只对空闲连接有效,对活跃连接无效。

往下调的风险要说清楚:已有语句不受影响;但当前值一旦超过新上限,就不能再新建,直到老语句被释放。所以别在高峰期往下调。

内存代价也别忘了:每条至少 8KB,调大配额的同时也放大了攻击面。

两种情况即使调大也没有用

PHP PDO 的 PDO::ATTR_EMULATE_PREPARES 默认是 true。 此时 prepare() 根本不发给 MySQL,execute 时才把值拼进 SQL 发过去(pingkai.cn 用 strace 验证过)。Prepared_stmt_count 不会涨,调 max_prepared_stmt_count 完全无效。要真正走服务端预处理,得显式设成 false。附带一句:PHP 8.1 起 PDO 默认把 int/float 原生返回,当年很多人关 emulate 是为了类型,这个理由现在已经不成立了。

关 emulate 的坑:pdo_mysql 若不是 mysqlnd 编译(老 libmysql),会出现 64 位整数被截断、LOCK TABLES 报 2030 “This command is not supported in the prepared statement protocol yet”、必须先 fetch 完或 closeCursor 才能发下一条查询。dba.stackexchange 274803 还有一例:关掉 emulate 后开始出现 2006 MySQL server has gone away,改回 true 就消失(未复现,原因未定)。

MariaDB。 10.x 同错误码 1461、同语义,排查思路照搬。

配置片段

JDBC:

jdbc:mysql://host:3306/db?useServerPrepStmts=true&cachePrepStmts=true&prepStmtCacheSize=250&prepStmtCacheSqlLimit=2048&cacheResultSetMetadata=true

PDO:

new PDO($dsn, $u, $p, [PDO::ATTR_EMULATE_PREPARES => true]);

默认即 true,也可以事后 setAttribute。

Go:

db.SetConnMaxLifetime(5 * time.Minute)
db.SetMaxOpenConns(n)

go-sql-driver/mysql 的 interpolateParams=true 在客户端插值、不发 COM_STMT_PREPARE;有 16MB 参数上限,需自行验证,另外依赖连接的 charset 设置。

MySQL 侧:SET GLOBAL max_prepared_stmt_count=65535;,或者写进 my.cnf(需重启)。

收尾

服务端预处理的价值在“同一条 SQL 反复执行很多次”。一次性查询、短连接、PHP-FPM 每请求新建连接的场景,关掉更省事。别把 max_prepared_stmt_count 当性能开关。

推荐文章

程序员茄子在线接单