2075 字
约 6 分钟
20
S08-A24 · 连接串里的那十个参数:为什么"默认值"会在生产里出问题

一个服务从"单机 MySQL"迁到"主从 + 代理",连接串只改了 host。上线后三类问题同时出现:读请求偶尔读到旧数据(因为还连在主库/或用了代理的读写分离但会话粘性丢了)、长事务偶发超时(socketTimeout 默认是"无限")、以及连接数打满(maxPoolSize 沿用了本地的 20,而实例数变成了 12)。

连接串是"配置里最少被审视"的部分:它包含"协议语义"(字符集、时区)、"资源上限"(池大小、超时)、以及"路由行为"(读写分离)——三类混在一条字符串里,这个"开发机够用"是问题的根源:开发机单实例、低并发、无长查询,于是"三类参数的默认值"恰好都"够用"——而生产的三件事同时变了。
而默认值往往"在开发机上够用"。

1. 十个参数的三类归属

类 参数
协议语义 characterEncoding、serverTimezone、useSSL
资源上限 maxPoolSize、connectionTimeout、socketTimeout
路由行为 readOnly、allowMultiQueries、rewriteBatchedStatements

第一类错了会"乱码/时区偏移",第二类错了会"超时/打满",第三类错了会"路由到错节点/批量失效"。三类的"发现时机"也不同:语义错在"第一个请求"就暴露(乱码很明显),上限错在"高峰"暴露(平时看不出来),路由错在"特定场景"暴露(写后立即读)。

2. "时区"为什么总是出事

现象 原因
时间差 8 小时 服务端 serverTimezone 与 JVM 不一致
时间差 0 但有夏令时错 用了 CST 这类模糊时区名
存进去是对的、读出来错 两侧解释不同

第二项的"代价"是"数据被写错":差 8 小时的时间戳进了库,而"写的时候不报错"——于是"补数据"成了"改历史"。
第二项是"最阴"的:CST 可以指"中国标准时间"也可以指"美国中部时间"——于是"同一个配置在不同环境解释不同"。

正解是"两边都用明确的时区名":Asia/Shanghai + JVM -Duser.timezone=Asia/Shanghai,且 serverTimezone 显式写。

3. "超时"的三层与默认值

超时 默认 该设多少
connectionTimeout(建连) 30s 3–5s
socketTimeout(读等待) 无限 与"最慢查询"对齐
事务超时(框架层) 无限/60s 小于 socketTimeout

第二项最危险:**默认"无限等"意味着"一个慢查询能把线程永久挂住"**第二项的"表现"是"线程池慢慢耗尽,而 CPU 不高":每个线程都在 read() 上等——于是"看起来像卡死",而"忙"的其实是 DB。
——而它在"平时"完全看不出来。

// 一份"能上生产"的连接串(三类都显式)
jdbc:mysql://db:3306/app?characterEncoding=utf8&serverTimezone=Asia/Shanghai
  &useSSL=false&connectionTimeout=3000&socketTimeout=30000
  &rewriteBatchedStatements=true&useCursorFetch=true
// 池:maxPoolSize 按"实例数 × 单实例并发"倒算,不按"本机能开多少"

"池大小"与"DB 上限"的关系必须"全局核算",否则"每个实例都对、合起来超":

-- DB 侧:先看"允许多少"和"现在多少"
SHOW VARIABLES LIKE 'max_connections';        -- 例如 500
SHOW STATUS LIKE 'Threads_connected';          -- 现在多少
-- 应用侧:12 实例 × 池 20 = 240;再加"运维工具/报表"预留 50 → 290,够
-- 若"池 50":12 × 50 = 600 > 500 → 必然偶发"Too many connections"

4. "池大小"的正确算法

错法 后果
按"本机能开 200"设 12 实例 × 200 = 2400 > DB 上限
按"QPS × 耗时"估 对(Little 定律)
靠"加大池"解决超时 掩盖慢查询

第二项的"单位"要统一:QPS × 秒 = 并发连接数——100 QPS × 0.2s = 20 个;而"池 200"意味着"允许 200 个请求同时挂在 DB 上",那是"放大故障"。
第二项是"正解":需要的连接数 ≈ QPS × 平均持有时间——100 QPS × 200ms = 20。而"池 200"通常是"掩盖"而不是"解决"。

"连接串"应当"只有一份来源",否则"环境之间会漂":

// 三种"来源"的可靠性
1 硬编码在代码里 → 环境之间必然分叉(且密钥进仓库)
2 环境变量 → 好(但"缺一个参数"不报错,默认值悄悄生效)
3 配置中心 + 启动校验 → 最好(缺参数即启动失败)
// 判据:能否在"启动时"断言"这 6 个参数都有值"

5. "路由行为"的三种误设

参数 误设的后果
readOnly=true 用在代理上 读请求被送到"只读节点",但"写后立即读"读不到
rewriteBatchedStatements=true 未开 批量插入变"逐条"(慢十倍)
allowMultiQueries=true 随手开 注入面扩大

第二项是"性价比最高"的一项:**一个参数把"1000 条 insert"合成"一条多值 insert"**第二项的"前提"是"批量接口"(addBatch+executeBatch):逐条 execute 时它没用——于是"开了参数但没改调用方式"是最常见的"没效果"。
——而它默认关闭。

"超时链"要"由内向外递增",否则"内层还没超时、外层已放弃":

// 一条"合理"的超时链(同一请求)
DB socketTimeout   30s  →  单条查询最坏 30s
事务超时           25s  →  比 DB 小,先由框架回滚
HTTP 客户端超时     20s  →  比事务小,先让调用方放弃
网关超时           18s  →  最外层,最先放弃
// 判据:外层 < 内层,于是"由内向外"依次放弃,日志能定位到"哪层放弃的"

6. 排错:五种"连接串问题"

现象 成因 处理方向
时间差 8 小时 时区不一致 显式 Asia/Shanghai
线程永久挂住 socketTimeout 无限 设 30s
连接打满 池 × 实例 > DB 上限 按 QPS×耗時 算
批量插入慢 未开 rewriteBatched 打开
写后读不到 读写分离 + 无粘性 关键读走主库

"字符集"这条也值得单说,因为它的错法最"安静":

// 三处"字符集"必须一致(缺一处就可能乱码)
1 连接串:characterEncoding=utf8
2 库表列:utf8mb4(emoji 需要 4 字节)
3 客户端:JDBC 驱动版本与 MySQL 版本匹配
// 反例:"库是 utf8mb4、连接是 utf8" → emoji/生僻字写入报错或被截断

7. 边界:什么情况"默认值"够用

场景 为什么 做法
本地开发 单实例、无并发 用默认
单实例 + 低流量 池与超时都不紧张 用默认
只做批处理(无长事务) 超时影响小 用默认
托管数据库(平台代管) 平台已调优 跟随平台

最后一条与"变更"有关的边界:连接串是"配置",但它的改动是"行为变更"——改池大小、改超时、改读写分离,都要"灰度 + 可回滚",而不是"改完重启看看"。
判断口径一句话:"这条连接串会不会"被多个实例共用"或"承载长查询"。会(集群 + 复杂查询),三类参数都要显式;不会(本地 + 简单查询),默认值反而"少出错"。

还有一个"运维"现实:连接串里的密码应当"由密钥管理注入",而它常被写进"配置文件 + 版本库"——于是"轮换密码"变成"改代码、发版、重启",而"不改"就"过期即故障"。

8. 自测

  1. 十个参数分哪三类?为什么它们"混在一条字符串里"容易出错?
  2. 为什么 CST 这类时区名"最阴"?正确写法是什么?
  3. socketTimeout 默认"无限"为什么危险?它与"事务超时"的关系是什么?
  4. "池大小"该怎么算?为什么"加大池"常常是"掩盖"?
  5. rewriteBatchedStatements 为什么"性价比最高"?
  6. 判断"是否该显式设置"的那个问题是什么?
S08-A24 · 连接串里的那十个参数:为什么"默认值"会在生产里出问题
https://www.clxhxhhr.top/posts/6657/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。