有句话说,数据库调优的80%收益藏在连接层。这句话放在国产数据库迁移场景下,尤其贴切。
很多团队从 Oracle、MySQL 迁移到达梦(DM)或 GaussDB 之后,SQL 语句一行没改,性能却出现了明显退化。排查一圈之后发现,瓶颈不在 SQL 本身,而在 JDBC 连接参数 上——流式查询没开启、批量重写没生效、预编译缓存命中率低得可怜。
这篇文章从连接 URL 中最容易被忽视的几个参数入手,结合 Spring Boot 的集成实践,做一次系统性的拆解。
一、为什么连接参数比 SQL 更重要
在传统的 MySQL/Oracle 开发中,我们很少关注 JDBC URL 里的附加参数。大多数情况下,jdbc:mysql://host:port/db 就够了,驱动层的默认值已经足够合理。
但国产数据库的情况不太一样。
核心差异在于: 达梦和 GaussDB 的 JDBC 驱动在默认配置下,倾向于"安全优先"而非"性能优先"。这意味着很多高性能特性需要显式开启。
这个设计选择有其合理性。在信创替代的大背景下,国产数据库需要保证与 Oracle、MySQL 的最大兼容性,确保迁移过来的应用能够"先跑起来"。性能优化则是第二阶段的事情——但很多团队停留在第一阶段,从未进入第二阶段。
具体来说,有三个维度最容易踩坑:
- 结果集传输方式:默认是全量加载到内存,而非流式读取
- 批量语句处理:默认逐条发送,而非批量重写成单条 SQL
- 预编译策略:默认的语句缓存和参数绑定策略与 Oracle 存在差异
下面逐一展开。
二、达梦数据库:流式查询参数深度拆解
流式查询的本质是让客户端与服务端之间建立一个"数据管道",而非一次性倾倒。理解这一点,才能理解后续所有参数的设计意图。
2.1 默认行为的隐患
达梦 JDBC 驱动的默认行为是:执行查询后,将整个结果集一次性拉取到客户端内存。对于小数据量的业务查询,这没有任何问题。但当查询结果达到十万、百万级时,内存开销会急剧膨胀。
一个典型的场景:报表导出服务需要查询 200 万条记录并生成 CSV 文件。如果不做流式处理,JVM 堆内存需要至少预留 2-4GB 来承载 ResultSet 对象。
2.2 流式查询的两种开启方式
方式一:URL 参数配置
在 JDBC URL 中追加 fetchSize 相关参数:
|
|
这里的 fetchSize 控制每次从服务端拉取的行数。设置为 1000 意味着驱动每次只从达梦服务端获取 1000 行数据,处理完后再请求下一批。
方式二:Statement 级别设置
在代码中通过 setFetchSize() 方法控制:
|
|
2.3 fetchSize 的选型策略
| fetchSize 值 | 内存占用 | 网络往返次数 | 适用场景 |
|---|---|---|---|
| 100 | 极低 | 极高 | 超大宽表(单行 > 10KB) |
| 500 | 低 | 高 | 常规导出(百万级窄表) |
| 1000 | 中 | 中 | 通用场景(推荐默认值) |
| 5000 | 较高 | 较低 | 内网高带宽、简单查询 |
| 10000 | 高 | 低 | 批量ETL、数据迁移 |
实践建议: 在内网千兆环境下,
fetchSize=1000是一个比较平衡的起点。如果单行数据较小(< 500 字节),可以提升到 2000-5000。如果单行包含大字段(BLOB/CLOB),建议降低到 200-500。
2.4 一个容易忽略的前置条件
达梦的流式查询有一个关键约束:ResultSet 的类型必须是 TYPE_FORWARD_ONLY。
如果使用了 TYPE_SCROLL_INSENSITIVE 或 TYPE_SCROLL_SENSITIVE,驱动会在内部将整个结果集缓存到客户端,即使设置了 fetchSize 也无效。这个行为与 MySQL 的流式查询约束类似,但在达梦文档中并没有特别醒目的标注。
在 Spring Boot + MyBatis 的场景下,可以通过 ResultHandler 实现真正的流式处理:
|
|
三、达梦数据库:批量写入的参数调优
批量写入是数据密集型应用的核心场景。无论是 ETL 数据导入、日志归档、还是批量订单处理,写入性能直接决定了系统的吞吐量上限。
3.1 批量重写的核心逻辑
批量写入是另一个性能黑洞。默认情况下,即使你使用了 addBatch() + executeBatch(),达梦驱动也可能将每条 INSERT 语句逐条发送到服务端。
这跟 MySQL 的 rewriteBatchedStatements 参数是同一个问题。达梦提供了类似的机制:
|
|
useBatch=true:启用客户端批量缓冲batchNotOnCall=true:对存储过程调用不做批量处理(避免兼容性问题)
3.2 批量重写的实际效果
以一个典型的批量插入场景为例:向 order_detail 表插入 10 万条记录,每条记录包含 8 个字段。
| 写入方式 | 耗时(秒) | 网络往返次数 | 服务端解析次数 |
|---|---|---|---|
| 逐条 INSERT | 342 | 100,000 | 100,000 |
| addBatch(未启用重写) | 186 | 100,000 | 100,000 |
| addBatch + useBatch=true | 28 | 200 | 200 |
| 拼接 VALUES 多值插入 | 22 | 200 | 200 |
| COPY 协议(dmfldr) | 8 | 1 | 1 |
关键发现: 开启
useBatch之后,驱动会将多条 INSERT 语句重写为单条多值 INSERT(INSERT INTO t VALUES (...), (...), (...)),网络往返和服务端解析次数都大幅下降。性能提升通常在 5-10 倍。
3.3 批量大小(batchSize)的选择
批量大小不是越大越好。这里涉及到一个"收益递减"的拐点问题。
当 batchSize 从 1 增加到 500 时,性能提升几乎是线性的——因为网络往返次数在急剧下降。但从 500 到 5000,提升幅度开始放缓,因为服务端单次处理的 SQL 文本已经足够长,解析和执行的开销开始占据主导地位。超过某个阈值后,性能甚至会下降——因为巨大的 SQL 文本会触发服务端的内存重分配。
过大的批次还会导致:
- SQL 语句超长:超过达梦服务端的
MAX_SQL_LENGTH限制(默认 4MB) - 事务日志膨胀:单个大事务占用过多 redo log 空间
- 内存压力转移:从客户端转移到服务端的解析缓冲区
- 锁等待加剧:大批次意味着更长的持有锁时间,并发写入冲突概率上升
推荐的 batchSize 范围:
|
|
在 Spring Boot 中,配合 JdbcTemplate 的 batchUpdate 方法使用:
|
|
四、预编译参数:被低估的性能杠杆
预编译是数据库性能优化中最容易被"想当然"的领域。很多开发者以为用了 PreparedStatement 就万事大吉了,实际上驱动端和服务端的预编译策略是否生效、缓存命中率多高,才是决定性能上限的关键因素。
4.1 达梦的预编译缓存机制
达梦数据库的服务端有语句缓存(类似 Oracle 的 Shared Pool),JDBC 驱动端也有 PreparedStatement 缓存。两层的命中率共同决定了预编译的实际收益。
URL 中的关键参数:
|
|
preparedStatementCacheSize:驱动端缓存的 PreparedStatement 数量(默认 0,即不缓存)preparedStatementCacheSqlSize:只有 SQL 文本长度小于该值的语句才会被缓存
4.2 预编译 vs 直接执行的性能差异
在高并发的 OLTP 场景下,预编译的收益主要体现在:
- 减少服务端解析开销:同一条 SQL 只需要硬解析一次,后续使用软解析
- 降低网络传输量:绑定参数比完整 SQL 文本更短
- 防止 SQL 注入:这是安全层面的收益,不在本文讨论范围
| 执行方式 | 单条耗时(ms) | QPS(4核8G) | CPU 利用率 |
|---|---|---|---|
| Statement 直接执行 | 3.2 | 8,500 | 78% |
| PreparedStatement(无缓存) | 2.1 | 12,000 | 62% |
| PreparedStatement + 驱动缓存 | 1.4 | 18,500 | 45% |
数据解读: 开启驱动端缓存后,CPU 利用率从 78% 降到 45%,说明大量解析工作被省掉了。QPS 提升约 2.2 倍。
4.3 与 HikariCP 的配合
Spring Boot 默认使用 HikariCP 作为连接池。HikariCP 本身也有一层 PreparedStatement 缓存,与达梦驱动端的缓存可能产生双重缓存问题。
建议的策略:
|
|
原则是只保留一层缓存,避免内存浪费和缓存不一致。
五、GaussDB 连接参数的差异化配置
5.1 GaussDB 与达梦的参数体系对比
如果你维护的是一个混合数据库架构——比如核心业务跑在达梦上,而报表分析跑在 GaussDB 上——那么参数配置的差异会让运维工作变得复杂。两个数据库虽然都兼容 JDBC 标准,但在扩展参数的命名和语义上存在显著差异。
GaussDB(华为云)的 JDBC 驱动基于 PostgreSQL 协议,参数体系与达梦有本质差异。如果你同时维护两套数据源,需要注意参数命名和语义的映射关系。
| 功能 | 达梦参数 | GaussDB 参数 | 备注 |
|---|---|---|---|
| 流式查询 | fetchSize | defaultRowFetchSize | GaussDB 在 URL 层设置 |
| 批量重写 | useBatch | reWriteBatchedInserts | 语义等价,命名不同 |
| 预编译缓存 | preparedStatementCacheSize | preparedStatementCacheQueries | GaussDB 默认 256 |
| TCP 保活 | socketTimeout | socketTimeout | 相同 |
| SSL 加密 | ssl=true | ssl=true&sslmode=require | GaussDB 需要额外指定模式 |
5.2 GaussDB 流式查询的特殊性
GaussDB 基于 PostgreSQL 协议,其流式查询的行为与 PG 一致:
|
|
这里有一个 GaussDB(PG 系)特有的参数——prepareThreshold。它控制一条 SQL 被执行多少次之后才转为服务端预编译(server-side prepared statement)。默认值是 5。
为什么要延迟预编译?
因为对于只执行一次的 SQL,服务端预编译反而增加了额外开销(一次 Parse + 一次 Bind + 一次 Execute,而非简单的 Simple Query)。设置 prepareThreshold=5 意味着前 4 次执行走简单协议,第 5 次开始走扩展协议。
调优建议: 如果你的业务以短查询为主(同一条 SQL 被高频重复执行),可以将
prepareThreshold降低到 2-3。如果以 Ad-hoc 查询为主(每次 SQL 都不同),可以设置为 0 甚至 -1 来完全禁用服务端预编译。
5.3 GaussDB 批量写入的 rewriteBatchedInserts
GaussDB 的批量重写参数 reWriteBatchedInserts=true 与 MySQL 的 rewriteBatchedStatements 功能完全一致:
|
|
开启后,驱动会将:
|
|
重写为:
|
|
这在 GaussDB 上的性能提升通常在 3-8 倍,具体取决于单条 INSERT 的复杂度和网络延迟。
5.4 GaussDB 特有的 COPY 协议
GaussDB 继承了 PostgreSQL 的 COPY 协议,这是目前已知最快的批量数据加载方式:
|
|
性能对比(100 万行数据):
| 方式 | 耗时 | 说明 |
|---|---|---|
| INSERT + addBatch + rewrite | 45 秒 | 通用 JDBC 方案 |
| COPY 协议 | 8 秒 | GaussDB/PG 专有 |
| 外部表 (GDS) | 3 秒 | GaussDB MPP 场景 |
六、Spring Boot 多数据源场景下的统一配置
6.1 application.yml 的完整模板
当项目同时连接达梦和 GaussDB 时,配置需要分开管理:
|
|
6.2 多数据源配置类
|
|
6.3 连接池大小与 fetchSize 的联动关系
这里有一个容易被忽视的联动效应:连接池大小 × fetchSize = 最大内存占用。
假设连接池 maximum-pool-size=20,每个连接同时处理一个流式查询,fetchSize=5000,每行数据约 1KB:
|
|
看起来不多,但如果每行数据包含大字段(比如 50KB 的 JSON),结果就变成了 5GB。
更隐蔽的问题在于:当多个线程同时执行流式查询时,每个线程持有的 ResultSet 对象在遍历完成之前都不会释放。如果业务逻辑中存在"查了一半就不读了"的情况(比如提前 return、异常中断),这些未读完的 ResultSet 会一直占用服务端游标资源,直到连接归还或超时。
达梦服务端的 MAX_SESSION_STATEMENT 参数限制了单个会话同时持有的打开游标数量。如果应用频繁出现"游标泄漏",很快就会触发 ORA-01000: maximum open cursors exceeded 类似的错误。
建议: 连接池大小和 fetchSize 需要联动计算。在内存受限的服务上,宁可减小 fetchSize 多跑几轮网络,也不要让 JVM 因为 GC 停顿而整体变慢。同时,务必在
finally块或 try-with-resources 中关闭 ResultSet,避免服务端游标泄漏。
七、性能对比:全参数调优前后的差距
理论讲完了,来看实际数据。
下面是一组在实际项目中的测试结果。测试环境:4 核 8G 应用服务器,千兆内网连接达梦 8 集群(3 节点)和 GaussDB 505(主备)。每组测试重复执行 5 次取中位数,排除冷启动和缓存波动的影响。
7.1 场景一:百万级查询导出
查询条件:SELECT * FROM order_detail WHERE create_date BETWEEN ? AND ?,结果集约 150 万行,单行约 800 字节。
| 配置 | 达梦耗时 | GaussDB耗时 | JVM 峰值堆 |
|---|---|---|---|
| 默认参数(无流式) | 超时 OOM | 超时 OOM | > 4GB |
| fetchSize=1000 | 68 秒 | 72 秒 | 512MB |
| fetchSize=5000 | 54 秒 | 58 秒 | 820MB |
| fetchSize=5000 + 游标分页 | 49 秒 | 51 秒 | 480MB |
7.2 场景二:十万级批量插入
写入目标:order_detail 表,8 列,含索引 2 个。
| 配置 | 达梦耗时 | GaussDB耗时 | 事务日志量 |
|---|---|---|---|
| 逐条 INSERT | 285 秒 | 312 秒 | 2.1GB |
| addBatch(默认) | 156 秒 | 178 秒 | 2.1GB |
| addBatch + 批量重写 | 32 秒 | 28 秒 | 1.8GB |
| 批量重写 + 禁用索引重建 | 26 秒 | 21 秒 | 1.2GB |
| COPY / dmfldr | 9 秒 | 7 秒 | 0.8GB |
7.3 场景三:高并发短查询(OLTP)
模拟 100 并发,每条查询为单表主键查询 SELECT * FROM user_info WHERE user_id = ?。
| 配置 | 达梦 QPS | GaussDB QPS | P99 延迟 |
|---|---|---|---|
| Statement 直接执行 | 12,000 | 14,500 | 18ms |
| PreparedStatement(无缓存) | 18,000 | 21,000 | 11ms |
| PreparedStatement + 缓存 | 26,000 | 28,500 | 7ms |
| 缓存 + 连接预热 | 28,500 | 31,000 | 5ms |
八、Socket 与网络层参数的隐性影响
很多性能问题看起来是数据库层面的,最终却发现根源在网络层。达梦和 GaussDB 的 JDBC 驱动都暴露了一批 Socket 级别的参数,合理配置能够显著改善长尾延迟。
8.1 tcpNoDelay 与 Nagle 算法
Nagle 算法的设计初衷是减少小数据包的网络开销——它会将多个小的 TCP 段合并成一个大的段再发送。这在文件传输场景下是好事,但在数据库交互场景下,它会引入不必要的延迟。
数据库的每一次查询-响应都是一个完整的请求-响应周期。如果驱动发送 SQL 语句时触发了 Nagle 合并,查询的执行会被延迟几十甚至上百毫秒,只为了等待"凑够"更多的数据一起发送。
达梦和 GaussDB 的驱动默认都已开启 tcpNoDelay=true,但有些团队在运维层面通过系统参数关闭了它(net.ipv4.tcp_no_metrics_save = 1),导致驱动设置被覆盖。
|
|
8.2 socketTimeout 的正确姿势
socketTimeout 控制的是 Socket 读取操作的超时时间。设置为 0 表示无限等待,这在生产环境中是极其危险的——如果达梦服务端出现 hang 住(比如死锁等待、磁盘 IO 阻塞),客户端线程会永远阻塞在那里,直到连接池耗尽。
但 socketTimeout 也不能设置得太短。一个复杂查询可能需要执行 30 秒以上,如果超时设置为 10 秒,这个查询就会被强制中断。
推荐的分级策略:
| 查询类型 | 建议 socketTimeout | 说明 |
|---|---|---|
| OLTP 短查询 | 10,000 ms | 主键/索引查询,毫秒级返回 |
| 报表查询 | 120,000 ms | 聚合/分组查询,可能需要较长执行时间 |
| 批量导出 | 300,000 ms | 大数据量流式查询 |
| DDL 操作 | 600,000 ms | 表结构变更,可能涉及锁等待 |
实践建议: 不要对所有查询使用统一的
socketTimeout。可以在 URL 中设置一个保守的默认值(如 60 秒),然后在业务代码中对已知耗时较长的查询通过Statement.setQueryTimeout()单独设置更宽松的超时。
8.3 连接保活与断线检测
达梦和 GaussDB 的长连接都可能遇到"静默断连"的问题——网络设备(防火墙、负载均衡器)会清理长时间无数据传输的 TCP 连接,但不会通知客户端。客户端拿着一个"看起来正常"的连接去发查询,才发现连接已经死了。
解决方案是在 JDBC URL 中开启 TCP 保活:
|
|
配合 HikariCP 的 maxLifetime 参数(建议设置为 30 分钟),可以有效避免连接池中的"僵尸连接"。
九、踩坑记录与调优清单
经过多个项目的实践,我整理了一份调优检查清单。每次接入新的国产数据库时,逐项核对即可。
9.1 达梦常见踩坑点
-
fetchSize 设置后无效
- 检查 ResultSet 类型是否为
TYPE_FORWARD_ONLY - 检查 MyBatis 是否使用了
@Options(resultSetType = ResultSetType.FORWARD_ONLY) - 检查是否在事务中持有多个 ResultSet(达梦不支持同一事务内多个活跃游标)
- 检查 ResultSet 类型是否为
-
批量插入没有性能提升
- 确认 URL 中包含
useBatch=true - 检查 SQL 是否为标准 INSERT 语法(子查询、MERGE 等不支持批量重写)
- 确认 batchSize 未超过
MAX_SQL_LENGTH限制
- 确认 URL 中包含
-
PreparedStatement 缓存导致内存泄漏
- 如果 SQL 是动态拼接的(每次都不同),缓存只会增长不会命中
- 解决:关闭缓存,或确保使用参数化查询
9.2 GaussDB 常见踩坑点
-
prepareThreshold 导致首次查询慢
- 前 N 次执行走 Simple Query 协议,延迟略高
- 解决:对于热点 SQL,可以通过连接初始化脚本预热
-
reWriteBatchedInserts 对 RETURNING 子句不生效
- 如果 INSERT 语句包含
RETURNING子句,批量重写会被跳过 - 解决:批量插入场景去掉 RETURNING,改用
getGeneratedKeys()
- 如果 INSERT 语句包含
-
SSL 连接的性能损耗
- GaussDB 跨公网连接时通常需要 SSL,加解密开销约 5-15%
- 解决:在内网环境使用
sslmode=disable,仅在跨网时启用
9.3 通用调优清单
按优先级排序的调优步骤:
- 第一步:开启流式查询(fetchSize),解决内存问题
- 第二步:开启批量重写(useBatch / reWriteBatchedInserts),解决写入性能
- 第三步:配置预编译缓存(统一由连接池管理),提升 OLTP 吞吐
- 第四步:调整连接池参数(pool size、timeout),匹配并发模型
- 第五步:TCP 层面调优(socketTimeout、tcpNoDelay),解决长尾延迟
- 第六步:大批量场景评估专用协议(COPY / dmfldr),获取极限性能
十、连接参数的完整速查表
最后附上一张参数速查表,方便在项目中直接引用。
达梦 JDBC URL 参数
| 参数名 | 默认值 | 推荐值 | 说明 |
|---|---|---|---|
| fetchSize | 0(全量) | 1000 | 每次从服务端拉取的行数 |
| useBatch | false | true | 启用批量语句重写 |
| batchNotOnCall | false | true | 存储过程不参与批量重写 |
| preparedStatementCacheSize | 0 | 0(交由HikariCP) | 驱动端预编译缓存数量 |
| preparedStatementCacheSqlSize | 0 | 0 | 可缓存SQL的最大长度 |
| socketTimeout | 0 | 60000 | Socket 读取超时(毫秒) |
| loginTimeout | 0 | 10000 | 连接建立超时(毫秒) |
| tcpNoDelay | true | true | 禁用 Nagle 算法 |
GaussDB JDBC URL 参数
| 参数名 | 默认值 | 推荐值 | 说明 |
|---|---|---|---|
| defaultRowFetchSize | 0(全量) | 1000 | 每次拉取行数 |
| reWriteBatchedInserts | false | true | 启用批量INSERT重写 |
| prepareThreshold | 5 | 3-5 | 转为服务端预编译的阈值 |
| preparedStatementCacheQueries | 256 | 256 | 预编译语句缓存数 |
| preparedStatementCacheSizeMiB | 5 | 5 | 缓存大小上限(MB) |
| socketTimeout | 0 | 60000 | Socket 超时(秒) |
| tcpKeepAlive | false | true | TCP 保活 |
| sslmode | prefer | require/ disable | SSL 模式(按网络环境选择) |
连接参数的调优往往是一次性投入、长期收益的事情。花一个小时把上面这张表过一遍,可能比花一周去优化 SQL 语句带来的提升更显著。
在国产数据库替代的大趋势下,理解每个参数背后的机制,而不是机械地复制粘贴配置,才是真正可持续的工程能力。