Connector/J 预处理没走服务端:useServerPrepStmts 默认 false,缓存上限 25/256 怎么改
三个现象同时出现
general_log 里干干净净,只有 Query select 42、Query select 43,没有 Prepare,也没有 Execute。DBA 顺手查了一下 Com_stmt_prepare,计数没动;performance_schema.prepared_statements_instances 里也是空的,结论很直接:库里根本没有预处理语句。
再去连接串里翻,useServerPrepStmts 明明加上了。问题出在写法上:文档写得很清楚,在 URL 里只写属性名而不给值,什么都不会设置。只加 useServerPrepStmts 不会启用服务端预处理,必须写成 useServerPrepStmts=true。官方属性表就在 dev.mysql.com/doc/connector-j 里,默认值一项一项列着。
另一种情况更隐蔽:参数写对了,但 URL 里还挂着 useCursorFetch=false,或者 emulateUnsupportedPstmts 在背后悄悄回退,现象和上面一模一样。
两条路径的分工
Connector/J 同时实现客户端模拟和服务端两套预处理,默认走客户端,原因是早期 MySQL 不支持或有实现问题。要开启服务端,得显式设置 useServerPrepStmts=true。
两条路径在 general_log 里的差别很直观:
- 客户端模拟:参数在本地被拼进 SQL,只发一整条
COM_QUERY,日志里是Query select 42、Query select 43。 - 服务端预处理:先发
Prepare select ?,再发Execute select 42、Execute select 43。
四个开关的默认值与判断依据
| 属性 | 默认值 | 生效版本 | 管什么 |
|---|---|---|---|
useServerPrepStmts | false | 3.1.0 | 是否真正在服务端 Prepare |
cachePrepStmts | false | 3.0.10 | 是否缓存解析/适用性检查/服务端语句本身 |
prepStmtCacheSize | 25 | 3.0.10 | 缓存条目数上限 |
prepStmtCacheSqlLimit | 256 | 3.0.10 | 可缓存的 SQL 长度上限 |
相关的一组默认值也一并记下:useCursorFetch=false、defaultFetchSize=0、rewriteBatchedStatements=false、queryInfoCacheFactory=com.mysql.cj.PerConnectionLRUFactory(8.0.29 之前叫 parseInfoCacheFactory)。
互斥与依赖关系
cachePrepStmts=false时,prepStmtCacheSize和prepStmtCacheSqlLimit设了也不起作用;只有打开cachePrepStmts,后两个参数才有意义。HikariCP 的 wiki 也重复了这一点。cachePrepStmts缓存的并不是一样东西:客户端模拟预处理语句的解析阶段、服务端预处理的“适用性检查”结果,以及服务端预处理语句本身。那条适用性检查要多一次网络往返,缓存之后才可以省掉。useCursorFetch=true且defaultFetchSize>0,或者语句上调了setFetchSize(n>0)时,才会用游标结果集;此时useServerPrepStmts会被自动设为true,因为游标只对服务端预处理语句可用。也就是说,useCursorFetch=true会覆盖useServerPrepStmts=false。emulateUnsupportedPstmts=true是默认值。服务端无法新建预处理语句时——可能是max_prepared_stmt_count到顶,也可能被整体禁用——Connector/J 默认静默回退到客户端模拟,不报错。所以“开了服务端预处理却看不到 Prepare”,先查这个开关。- 大参数改回普通参数有个坑:用
setBinaryStream、setAsciiStream、setCharacterStream、setBlob、setClob设过参数后,数据要到execute()时才发送,流在那之后已经关闭。要把该参数改回非大参数并重新执行,必须先clearParameters()再重新设置所有参数。
验证步骤
- 打开 general_log,看语句是以
Query出现,还是以Prepare/Execute成对出现。 - 查
Com_stmt_prepare、Com_stmt_close状态变量,确认服务端确实收到了 Prepare。 - 查
performance_schema.prepared_statements_instances,看当前存在的服务端语句。 - 核对 URL 里每个属性都带了
=值,尤其是useServerPrepStmts=true这种。
取舍
维基百科 Prepare (SQL) 词条把边界说得很清楚:客户端模拟对只执行一次的查询能减少往返;服务端预处理对多次执行更快;两者在防注入上同样有效;预处理无法在编译时完成全部优化,因为最佳计划可能依赖具体参数值。
HikariCP 给的建议是:prepStmtCacheSize 默认 25 太保守,建议 250–500;prepStmtCacheSqlLimit 默认 256 对 Hibernate 生成的 SQL 太短,建议 2048;cachePrepStmts 默认 false,不打开这个开关,上面两个参数无效;并建议 useServerPrepStmts=true。
阿里云 AnalyticDB 文档列了几种语言只开客户端插值的写法:JDBC 用 useServerPrepStmts=false、MariaDB 相同、Go 用 interpolateParams=true、PDO 用 ATTR_EMULATE_PREPARES=true;同时提示 useCursorFetch=true 会覆盖 useServerPrepStmts=false。
其余几个预处理相关默认值:useStreamLengthsInPrepStmts=true、processEscapeCodesForPrepStmts=true、autoClosePStmtStreams=false、generateSimpleParameterMetadata=false。
站内 #8259 讲的是 PDO 驱动层的模拟预处理(emulate prepares、LIMIT '10' 报 1064、PARAM_INT、同名命名参数 HY093),#8184 讲的是引擎侧 max_prepared_stmt_count=16382 的配额与消耗分型;这里是 Connector/J 在 Java/JDBC 侧的四个开关。