Druid 连接池与@Transactional 事务超时配置问题总结
一、@Transactional timeout 与 Druid 的关系
两者完全独立,互不感知
| 维度 | @Transactional timeout | Druid removeAbandonedTimeoutMillis |
|---|---|---|
| 归属层 | Spring 事务管理层 | 连接池层 |
| 作用对象 | 事务的执行时长 | 连接从池中借出后的存活时长 |
| 超时后行为 | 标记事务为 rollback-only,下次 JDBC 操作时抛 TransactionTimedOutException | 直接关闭底层 socket,强制回收连接 |
| 相互通信 | ❌ 没有任何通信机制 | ❌ Druid 不知道 Spring 事务的存在 |
关键结论
@Transactional(timeout = 45)设置 45s,不代表连接可以持有 45s- 如果
removeAbandonedTimeoutMillis = 10s,连接在借出 10s 后就会被 Druid 强制关闭,与 Spring 的 45s 毫无关联 - 两个超时时间并不取较小值生效,而是各自独立触发,谁先到谁先动作
正确的设置原则
removeAbandonedTimeoutMillis > 业务中单次连接持有的最长时间 > @Transactional timeout
二、Druid 相关参数详解
2.1 参数汇总
| 参数 | 默认值 | 业内建议值 | 说明 |
|---|---|---|---|
timeBetweenEvictionRunsMillis | 60000ms(1分钟) | 60000ms | 后台检测线程运行间隔,同时也是 keepAlive 心跳间隔,不必太频繁 |
minEvictableIdleTimeMillis | 1800000ms(30分钟) | 300000ms(5分钟) | 连接空闲超过此时间才有资格被回收,建议远小于 MySQL wait_timeout(默认28800s) |
maxEvictableIdleTimeMillis | 25200000ms(7小时) | 900000ms(15分钟) | 空闲连接强制回收时间上限,建议设 15 分钟,防止 MySQL 主动断开后池里还握着死连接 |
removeAbandoned | false | 生产 false,开发 true | 是否开启废弃连接回收,默认关闭 |
removeAbandonedTimeoutMillis | 300000ms(5分钟) | 开发 120s,生产如需开启则 ≥ 1800000ms | 连接借出后超过此时间未归还则认为废弃,核心约束:必须 > 业务最长事务耗时 |
logAbandoned | false | true | 发现废弃连接时打印调用栈日志,不关闭连接,排查泄漏用 |
2.2 removeAbandoned 生产环境是否常开?
不建议在生产环境常开 removeAbandoned=true。
原因:
- 连接持有时长计算从"借出连接"开始,SQL 执行期间同样计时,任何耗时较长的正常操作都可能被误判为泄漏连接而强制关闭
- 开启后每次借还连接都需要操作一个带锁的
activeConnectionsMap,高并发下有额外性能开销 - 连接泄漏属于代码 bug,应在开发测试阶段发现修复,不应靠生产运行时兜底
推荐策略:
| 环境 | removeAbandoned | removeAbandonedTimeoutMillis | logAbandoned |
|---|---|---|---|
| 开发/测试 | true | 120000ms(2分钟) | true |
| 生产 | false | 1800000ms(保留配置但不生效) | true |
2.3 生产环境推荐配置
spring:
datasource:
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 3000 # 获取连接等待超时 3s,快速失败
time-between-eviction-runs-millis: 60000 # 检测线程间隔 60s
min-evictable-idle-time-millis: 300000 # 空闲 5 分钟可回收
max-evictable-idle-time-millis: 900000 # 空闲 15 分钟强制回收
test-while-idle: true # 借出前检测连接有效性(推荐开)
test-on-borrow: false # 每次借出都检测,性能损耗大,不开
test-on-return: false
validation-query: SELECT 1
validation-query-timeout: 1
remove-abandoned: false # 生产不开,防误杀
remove-abandoned-timeout-millis: 1800000 # 即便不开,也配成合理值备用
log-abandoned: true # 记录泄漏日志,帮助排查
三、案例复盘
3.1 案例描述
方法配置:@Transactional(rollbackFor = Exception.class, timeout = 45)
Druid 配置:removeAbandoned = true,removeAbandonedTimeoutMillis = 10000ms(10s)
执行过程:
T=0s 借出连接,事务开始
T=10s ← Druid 后台线程扫描,连接借出已达 10s,判定为废弃连接,强制关闭客户端 socket
T=0~20s 慢查询(花了 20s)—— MySQL 服务端连接仍在执行,客户端 socket 已断
T=20~25s 两个 RPC 调用(5s)—— 不涉及 DB,没有立即报错
T=25s 尝试提交事务 → 报错:Connection is closed ❌
3.2 为什么慢查询能"执行完"却最终报错?
Druid 关闭的是Java 客户端侧的 socket,MySQL 服务端不感知,SQL 在 DB 侧继续跑完。但 Java 侧持有的 Connection 对象已被标记为废弃关闭,等到后续执行 commit 时才真正抛出异常。这就是为什么报错出现在事务提交阶段,而不是慢查询执行阶段。
3.3 根本原因
removeAbandonedTimeoutMillis = 10s 远小于该方法的实际连接持有时间(25s),导致正常使用中的连接被误判为泄漏连接而强制回收。
3.4 解决方案(优先级从高到低)
- 优化慢查询(治本):20s 的查询需要排查索引、分页策略,目标控制在 1s 以内
- 事务内不做 RPC 调用:RPC 耗时不可控,占用数据库连接是反模式
- 生产关闭
removeAbandoned:把remove-abandoned改为false,消除误杀风险 - 如必须开启:
removeAbandonedTimeoutMillis至少要设置为业务最长事务耗时的 2 倍以上
3.5 核心记忆点
removeAbandonedTimeoutMillis计时从连接借出那一刻开始,不管连接是否在执行 SQL,都在计时。SQL running 状态不会暂停计时器。

9182

被折叠的 条评论
为什么被折叠?



