Druid连接池在Maven项目中的参数配置、监控与排障指南
1. 连接池不是可选项是 JDBC 项目的入场券这篇聊的是在 Maven 中使用 Druid 数据库连接池这件事从依赖坐标怎么定、参数怎么算一直讲到监控台怎么开、线上出问题怎么排查。如果你手头有一个基于 Maven 构建的 Java 项目正在用原生的DriverManager取连接或者用着某个默认连接池但从来没看过它的参数那这篇内容基本可以照着抄。文章面向的是已经能写 CRUD、但对连接池内部机制只停留在“配个 URL 就能跑”这一层的开发者也照顾到需要给团队定参数基线、要处理线上连接打满告警的同学。我先把结论摆前面连接池解决的不是“性能优化”这种锦上添花的问题而是“系统能不能扛住并发”的生死线问题。一个没有连接池的 Web 应用在几十个并发请求下就会开始出现响应时间毛刺上百并发基本上就是雪崩。原因不复杂后面细说。而 Druid 之所以在很多项目里成为默认选择也不是因为它性能最强而是因为它把“监控”和“诊断”这两件事做进了连接池本身这对排障的价值远超那一点点吞吐差异。1.1 每次新建连接成本到底花在哪儿很多人对“建连接很贵”这句话没有体感觉得不就是 new 一个对象吗。DriverManager.getConnection()背后干的事情远比想象中多先要跟数据库服务端建立 TCP 连接三次握手然后发送认证包服务端校验用户名密码、查询账号权限、加载该账号可见的库表元数据再初始化会话变量、字符集、时区、事务隔离级别最后才是返回一个可用的连接句柄。整个过程在网络条件正常的情况下通常要几毫秒到几十毫秒跨机房或者 TLS 加密的情况下会更久。单看一次几十毫秒好像不算什么但问题在于它是串行阻塞的。一个请求进来从建立连接到执行 SQL 再到关闭连接如果建连占了 20msSQL 只花了 2ms那 90% 的时间都浪费在“开门”上。并发一上来连接建立的耗时还会因为服务端要维护大量握手而进一步拉长形成正反馈式的恶化。连接池的思路就很朴素连接用完不真的关掉而是还回池子里下一个请求直接拿现成的。这就像去银行办事每次去都重新开卡建档和提前办好一张卡反复用效率差着数量级。池子里的连接数是有上限的这个上限同时保护了应用不会把线程全耗在等连接上和数据库不会被瞬间打满max_connections。这里有个常见误解连接池不是越大越好。连接数超过数据库的处理能力之后多出来的连接只会在服务端排队反而增加上下文切换开销。1.2 为什么最后落到 Druid 上Java 生态里的连接池选择不算少常见的有 HikariCP、DBCP2、C3P0、Druid还有 Tomcat JDBC Pool。每一款都有自己的定位选型的时候要看的不是“谁跑分高”而是“我的团队需要什么”。连接池性能表现监控能力代码体量适用场景HikariCP极高基础指标需接 Micrometer精简追求极致吞吐、已有完善监控体系DBCP2中等弱中等老项目沿用、依赖简单C3P0偏低弱较大历史项目维护Druid良好内置 Web 监控台、SQL 统计、慢 SQL较大需要自带诊断能力、SQL 审计、中小团队快速排障我选 Druid 的核心原因有三个。第一是内置监控台挂上去就能看到当前活跃连接数、等待线程数、累计执行 SQL 次数、慢 SQL 明细不需要额外接一套 APM 就能定位“连接被谁借走没还”这类问题这对没有专职运维的团队价值极大。第二是SQL 层面的统计和防火墙wall过滤器能拦掉一些明显的注入式写法stat过滤器能把执行次数、最慢耗时、执行错误都记下来。第三是诊断日志足够啰嗦出问题时它会明确告诉你“discard connection”“wait millis 60000, active 20, maxActive 20”比某些池子静默超时好排查太多。代价也要说清楚Druid 的代码量比 HikariCP 大一个数量级极端高并发下的吞吐确实略逊它对配置项的容忍度也不高配错了不会报错只会在运行时表现为各种奇怪现象。所以在 Maven 里引它版本和参数都得认真对待。2. Maven 依赖怎么引、版本怎么定2.1 最小依赖集合与坐标纯 Java 项目里Druid 本身只需要一个坐标没有强制传递依赖这一点很省心。在pom.xml的dependencies里加上dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency数据库驱动要单独引Druid 不会帮你带。以 MySQL 为例新版驱动坐标已经从mysql:mysql-connector-java迁移到了com.mysql:mysql-connector-j老坐标仍然能用但已经不推荐dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId version8.3.0/version scoperuntime/scope /dependency驱动的作用域建议写runtime因为业务代码里正常情况下不应该出现任何com.mysql.*的 import所有操作都通过javax.sql.DataSource和标准 JDBC 接口完成。写runtime能在编译期就把这种耦合挡住。如果你用的是 Spring Boot还有一个更省事的选择druid-spring-boot-starter它把DruidDataSource的自动装配、监控 Servlet 的注册、配置项前缀映射都做好了dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.20/version /dependency这里有个版本对齐的坑starter 的版本要和 druid 核心包的版本保持一致不要出现 starter 是 1.2.20、核心包被别的依赖拽到 1.1.x 的情况。用mvn dependency:tree -Dincludescom.alibaba:druid查一下实际生效的版本比看 pom 里写的更可靠。2.2 版本选择与依赖冲突排查版本号这块我的建议是用当前最新的 1.2.x 稳定版但不要用刚发布一周以内的版本。连接池这种东西一旦出问题影响面是全站没必抢第一个吃螃蟹。1.2.x 系列相比 1.0.x 修复了大量keepAlive、phyTimeoutMillis相关的边界问题还补了不少ValidConnectionChecker的实现老版本在 MySQL 8 上容易出现连接有效性判断失效的情况。依赖冲突最常见的三种表现第一种项目里同时存在druid和druid-spring-boot-starter两者版本不一致运行时可能加载到旧的核心类。解决办法是在父 pom 里用dependencyManagement统一锁定版本子模块只写 groupId 和 artifactId。第二种某些中间件或者老框架会传递依赖一个很老的druid版本比如 1.0.9。这个时候mvn dependency:tree会看到同一条路径上出现两个版本Maven 按“最近优先”原则只保留一个很可能保留的是你不想要的那个。显式在依赖里排除掉传递版本即可。dependency groupId某个中间件坐标/groupId artifactIdxxx/artifactId exclusions exclusion groupIdcom.alibaba/groupId artifactIddruid/artifactId /exclusion /exclusions /dependency第三种日志门面冲突。Druid 支持slf4j过滤器如果你的项目里同时有 log4j 和 logback可能出现ClassNotFoundException或者日志重复输出。这种情况把filters里的slf4j保留同时确保只有一个日志实现即可。一个很实用的自检习惯本地启动时在日志里搜一下DruidDataSource的初始化日志它会打印实际生效的版本和全部参数。参数和你预期的不一样说明配置没被读到这时候再去看配置文件才有意义。3. 核心参数每一个都对应一个真实场景Druid 的参数表很长但真正决定行为的就是那么十来个。我按“连接数量”“有效性检测”“回收与保活”三组来讲每一组都说明它解决的是什么问题以及数值该怎么算。3.1 连接数量三兄弟怎么算initialSize、minIdle、maxActive是三个必须一起看的参数单独调某一个基本没意义。initialSize是池子初始化时预先建立的物理连接数。设成 0 意味着完全懒加载第一个请求进来才建连代价是首次访问特别慢设成和minIdle一样启动时就把连接备齐服务一起来就能扛流量。生产环境我一般设 5 到 10不要设成最大值否则应用一启动就给数据库来一次连接风暴多个实例同时重启时数据库可能直接拒绝服务。minIdle是最小空闲连接数池子会保证至少留这么多空闲连接。它和keepAlive强相关后面会讲。maxActive是最大连接数也是最需要计算的参数。计算逻辑不是拍脑袋而是要跟数据库侧的max_connections对齐单实例 maxActive × 应用实例数 其他系统占用 运维预留 ≤ 数据库 max_connections举个具体的例子。数据库max_connections是 500同一套库上还有一个报表服务占了 30 个连接留 50 个给运维和临时排查那么业务侧可用的上限是 420。如果业务应用部署 6 个实例每个实例maxActive就不该超过420 ÷ 6 70实际配置时再打个八折取 50 到 56留出突发余量。单个实例到底需要多少连接还有一个更细的算法连接数 ≈ CPU核心数 × 2 磁盘数。这个公式来自数据库侧的经验值因为连接真正在干活的时间只占其生命周期的一部分大部分时间在等 IO。一个 8 核的机器跑 20 个连接通常已经足够把maxActive设成 200 的收益很小反而会让数据库的连接管理开销变大。maxWait是获取连接的最大等待时间单位毫秒。当池子里没有空闲连接、活跃数已达maxActive时请求线程会在这里排队超过这个时间抛GetConnectionTimeoutException。这个值要跟你的接口超时对齐如果网关超时是 3 秒maxWait设成 60 秒毫无意义线程早就被网关掐了还占着 Tomcat 的工作线程。我一般设 1000 到 3000 毫秒让失败快速暴露出来。提示maxWait调小之后GetConnectionTimeoutException的日志会变多这是好事而不是坏事。它说明连接池在保护系统而不是让请求无限堆积。3.2 超时与有效性检测连接池里最隐蔽的问题不是“拿不到连接”而是“拿到一个已经死掉的连接”。物理连接可能因为数据库重启、中间网络设备清理空闲会话、服务端wait_timeout到期等原因被单方面关闭而池子这边还以为它是活的。检测机制就是解决这个问题的。validationQuery是检测连接是否有效的 SQL。MySQL 用SELECT 1其他数据库按方言调整。虽然新版 Druid 在部分驱动上能自动推断但显式配置更可控也避免驱动升级后行为变化。testWhileIdle建议设为true这是性价比最高的检测方式只有当连接空闲时间超过timeBetweenEvictionRunsMillis时后台线程才会去检测它。检测发生在“用之前”但又不是每次都用兼顾了可靠性和性能。timeBetweenEvictionRunsMillis是后台检测线程的运行间隔默认 60 秒我一般设 30 到 60 秒。这个值必须小于数据库的wait_timeoutMySQL 默认 28800 秒但很多云数据库会改小到几百秒否则连接在被检测之前就已经被服务端踢了。testOnBorrow和testOnReturn我建议都设false。设成true意味着每次借出、归还都要执行一次validationQuery高并发下这就是白白多出来的往返开销。有了testWhileIdle加phyTimeoutMillis这两个开关没有打开的必要。phyTimeoutMillis是物理连接的最大存活时间超过这个时间的连接会被丢弃重建默认 -1 表示不限制。这个参数专门对付“中间设备静默断开空闲连接”的情况。有些网络设备会在连接空闲 5 分钟或 30 分钟后清理会话表项两端都不知道连接已经断了这时候testWhileIdle检测出来的连接其实是“TCP 层看起来还活着”的假活连接。设一个比设备空闲超时更短的值比如 1800000 毫秒30 分钟能有效规避这类问题。3.3 保活、泄漏回收与 PSCachekeepAlive是 Druid 1.0.28 之后提供的保活开关开启后后台线程会定期对空闲连接发一次轻量探测防止连接因为长时间不用被服务端回收。但它有一个很容易踩的坑keepAlive只对minIdle范围内的空闲连接生效。很多人minIdle设成 0 或者 1然后打开keepAlive发现没效果原因就是池子里的空闲连接早就超过minIdle被判定为“多余”在minEvictableIdleTimeMillis到期后被销毁了根本轮不到保活。想让保活真正起作用minIdle至少设成和日常并发所需连接数相当的量级。removeAbandoned是泄漏回收开关配合removeAbandonedTimeout单位秒和logAbandoned使用。开启后如果一个连接被借出超过指定时间还没归还池子会强制回收它并在日志里打印借出时的堆栈。这个功能在排查泄漏时是神器但有一个必须警惕的副作用长事务会被误杀。如果业务里存在执行几分钟的批量任务而removeAbandonedTimeout设成 180 秒那这个任务手里的连接就会在中途被强行拿走表现为“连接已关闭”异常。所以要么把超时设得远大于最长事务时间要么干脆关掉这个开关改用监控台观察活跃连接趋势。poolPreparedStatements在 MySQL 场景下我建议设false。MySQL 驱动自身有cachePrepStmts机制Druid 的 PSCache 再叠一层容易出现游标泄漏和内存膨胀而且 MySQL 的预编译语句收益本来就有限。这个参数在 Oracle 场景下才有明显价值。与之相关的maxPoolPreparedStatementPerConnectionSize也就一并保持默认或 0。filters是过滤器配置逗号分隔。stat用于统计wall用于 SQL 防火墙slf4j用于把内部日志输出到应用日志。三个都开的话内存占用会上升但换来的是完整的可观测性。需要注意的是wall开启后对 SQL 语法有校验某些动态拼接的 SQL 可能被误判拦截上线前要在预发环境跑一遍全量业务。4. 三种落地方式按项目形态挑同一个连接池在不同的项目结构里配置方式完全不同。这里给三种最常见的方式从最简单的单文件工具类到 Spring Boot 集成按需选择。4.1 纯 Java 硬编码方式适合小工具、命令行程序、单元测试。核心就三步创建DruidDataSource、设置参数、调用init()。import com.alibaba.druid.pool.DruidDataSource; import javax.sql.DataSource; public class DataSourceHolder { private static final DruidDataSource DS new DruidDataSource(); static { DS.setUrl(jdbc:mysql://127.0.0.1:3306/demo?useUnicodetruecharacterEncodingutf8serverTimezoneGMT%2B8useSSLfalse); DS.setUsername(demo); DS.setPassword(******); DS.setDriverClassName(com.mysql.cj.jdbc.Driver); DS.setInitialSize(5); DS.setMinIdle(5); DS.setMaxActive(20); DS.setMaxWait(3000); DS.setTimeBetweenEvictionRunsMillis(60000); DS.setMinEvictableIdleTimeMillis(300000); DS.setValidationQuery(SELECT 1); DS.setTestWhileIdle(true); DS.setTestOnBorrow(false); DS.setTestOnReturn(false); DS.setPoolPreparedStatements(false); DS.setKeepAlive(true); DS.setPhyTimeoutMillis(1800000L); try { DS.setFilters(stat,slf4j); DS.init(); } catch (Exception e) { throw new IllegalStateException(连接池初始化失败, e); } } public static DataSource get() { return DS; } }这段代码有几个细节值得说一下。init()必须显式调用DruidDataSource不会在第一次getConnection()时自动初始化如果漏了这一步initialSize就不生效。setFilters必须在init()之前调用init()之后再设过滤器会抛异常。phyTimeoutMillis是long类型传 int 虽然有自动提升但写法上容易让人误以为是秒。还有一个不那么明显的点这个静态初始化的写法在类加载失败时会抛ExceptionInInitializerError堆栈信息会被吞掉一层。生产环境里建议把init()包一层 try-catch 转成明确的异常日志里能看清到底是 URL 错了还是密码错了。4.2 properties 配置文件加工厂模式项目里不想硬编码就把参数外置到druid.properties用DruidDataSourceFactory加载。这种方式在非 Spring 的传统项目里很常见。driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://127.0.0.1:3306/demo?useUnicodetruecharacterEncodingutf8serverTimezoneGMT%2B8useSSLfalse usernamedemo password****** initialSize5 minIdle5 maxActive20 maxWait3000 timeBetweenEvictionRunsMillis60000 minEvictableIdleTimeMillis300000 validationQuerySELECT 1 testWhileIdletrue testOnBorrowfalse testOnReturnfalse poolPreparedStatementsfalse keepAlivetrue phyTimeoutMillis1800000 filtersstat,slf4j connectionPropertiesdruid.stat.mergeSqltrue;druid.stat.slowSqlMillis2000加载代码Properties props new Properties(); try (InputStream in DataSourceHolder.class.getClassLoader() .getResourceAsStream(druid.properties)) { props.load(in); } DataSource dataSource DruidDataSourceFactory.createDataSource(props);这里的头号大坑是键名大小写。DruidDataSourceFactory是通过反射调用 setter 来赋值的键名必须严格匹配属性名也就是驼峰写法initialSize写成initial-size或者initial_size都不会报错只是静默失效。同理maxActive不能写成max-active。这个坑极其隐蔽因为配置读进去了、池子也起来了就是参数不生效。第二个坑是filters里的过滤器如果加载失败比如类路径下没有对应的实现createDataSource会直接抛异常而不是忽略。这在裁剪依赖的时候容易踩到。第三个是connectionProperties的值本身是分号分隔的键值对写在 properties 文件里时要注意特殊字符转义问题。稳妥的做法是尽量简化只保留mergeSql和slowSqlMillis这两个最常用的。实操心得写完 properties 之后在启动日志里核对 Druid 打印的参数快照重点看maxActive、minIdle、validationQuery三项。这三项对不上基本就是键名写错了。4.3 Spring Boot 下的 starter 集成Spring Boot 项目用 starter 是最省事的配置写在application.yml里自动装配会处理掉大部分细节。spring: datasource: type: com.alibaba.druid.pool.DruidDataSource druid: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/demo?useUnicodetruecharacterEncodingutf8serverTimezoneGMT%2B8useSSLfalse username: demo password: ****** initial-size: 5 min-idle: 5 max-active: 20 max-wait: 3000 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 validation-query: SELECT 1 test-while-idle: true test-on-borrow: false test-on-return: false pool-prepared-statements: false keep-alive: true phy-timeout-millis: 1800000 filters: stat,slf4j connection-properties: druid.stat.mergeSqltrue;druid.stat.slowSqlMillis2000 stat-view-servlet: enabled: true url-pattern: /druid/* login-username: monitor login-password: ****** allow: 127.0.0.1 reset-enable: false web-stat-filter: enabled: true url-pattern: /* exclusions: *.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/* filter: stat: enabled: true log-slow-sql: true slow-sql-millis: 2000 wall: enabled: false注意这里有两套写法容易混filters: stat,slf4j是 Druid 原生的过滤器开关而filter.stat.enabled是 starter 提供的细粒度配置。两者可以并存但filter下面的配置更细能单独控制慢 SQL 阈值和是否打印日志。wall我默认关掉因为它的误伤率取决于业务 SQL 的写法需要单独验证后再开。还有一个隐含前提使用druid-spring-boot-starter时项目里不要同时引入spring-boot-starter-jdbc里的默认连接池配置类去覆盖DataSource。如果启动日志里看到的是HikariPool而不是 Druid 的初始化信息说明type没生效或者被其他自动配置抢先了。排查方法是看DataSource的实际类型或者直接看启动时有没有打印 Druid 的参数列表。5. 把监控台开起来这是 Druid 最值钱的部分连接池参数配好了只是能用真正让 Druid 区别于其他池子的是它自带的监控台。挂上去之后连接被谁借走、哪条 SQL 最慢、当前有多少线程在排队全都一目了然。5.1 Servlet 与 Filter 的注册方式非 Spring Boot 的项目在web.xml里注册两个组件servlet servlet-nameDruidStatView/servlet-name servlet-classcom.alibaba.druid.support.jakarta.StatViewServlet/servlet-class init-param param-nameloginUsername/param-name param-valuemonitor/param-value /init-param init-param param-nameloginPassword/param-name param-value******/param-value /init-param init-param param-nameallow/param-name param-value127.0.0.1/param-value /init-param init-param param-nameresetEnable/param-name param-valuefalse/param-value /init-param /servlet servlet-mapping servlet-nameDruidStatView/servlet-name url-pattern/druid/*/url-pattern /servlet-mapping filter filter-nameDruidWebStatFilter/filter-name filter-classcom.alibaba.druid.support.jakarta.WebStatFilter/filter-class init-param param-nameexclusions/param-name param-value*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*/param-value /init-param /filter filter-mapping filter-nameDruidWebStatFilter/filter-name url-pattern/*/url-pattern /filter-mapping这里要特别留意包名。Java EE 迁移到 Jakarta EE 之后Servlet API 的包名从javax.servlet变成了jakarta.servletDruid 对应的类也分了com.alibaba.druid.support.http.StatViewServlet和com.alibaba.druid.support.jakarta.StatViewServlet两套。用错了不会报编译错误只会在访问时 404 或者 500。判断依据很简单项目里 import 的是哪个包就用哪一套。allow参数一定要配最好只允许内网网段或者本机访问。监控台默认没有强认证暴露在公网等于把数据库的 SQL 明细和连接状态公开了。resetEnable设成false也很重要否则任何人都能点一下“重置”把统计数据清零排查问题时数据就没了。Spring Boot 项目不用手写这些靠stat-view-servlet和web-stat-filter两段配置就够了starter 会自动注册ServletRegistrationBean和FilterRegistrationBean。5.2 监控台里该盯哪几个数监控台页面很多但实际排障常用的就那么几个数据源页面的“活跃连接数”曲线。这条线长期贴近maxActive说明连接不够用或者有泄漏。如果它呈现“锯齿状”——涨到顶、掉下来、再涨到顶通常是某个定时任务在批量取连接属于正常如果它一路上涨不回落基本可以确定是泄漏。“等待线程数”。这个值大于 0 就说明有请求在排队等连接持续时间越长问题越严重。它和maxWait是配套看的等待线程数飙升往往伴随GetConnectionTimeoutException。SQL 监控页的“执行时间”排序。按平均耗时或者最大耗时倒序能看到哪些 SQL 拖慢了整体。配合slowSqlMillis配置超过阈值的 SQL 会被单独记录包含完整 SQL 文本和执行次数这是优化索引最直接的输入。“执行数”和“错误数”。错误数突然上涨可能是连接失效导致的也可能是 SQL 本身报错。点进去能看到具体异常。一个实战技巧把/druid/datasource.json这个接口接进你自己的健康检查定期拉取ActiveCount和WaitThreadCount上报到监控系统比人肉登录页面看可靠得多。6. 踩坑实录与排查手册参数和配置讲完了真正耗时间的是出问题之后的定位。下面这几类是我遇到过最频繁的。6.1 连接泄漏池子被慢慢抽干表现很典型系统跑上几个小时或者一两天之后接口开始变慢然后大面积超时重启就恢复过一段时间再来一次。监控台上活跃连接数持续爬升不回落最终顶到maxActive。泄漏的根因几乎都是“借了没还”。JDBC 里需要关闭的资源有三个ResultSet、Statement、Connection任何一个漏关都会导致连接无法归还。常见写法错误有这么几种在 try 块里return导致finally里的关闭逻辑被跳过在 catch 里重新抛异常但没关连接用连接池包装的对象做了多层嵌套只关了最外层。定位手段是把removeAbandoned和logAbandoned打开DS.setRemoveAbandoned(true); DS.setRemoveAbandonedTimeout(300); DS.setLogAbandoned(true);开启之后超过 300 秒还没归还的连接会被强制回收同时在日志里打印借出时的完整堆栈。拿着这个堆栈去代码里搜基本能精确到行。注意这个功能只应该作为排查期的临时手段长期开着有误杀长事务的风险定位到问题后就应该关掉。根治办法还是规范资源管理。Java 7 之后一律用 try-with-resourcesString sql select id, name from demo_user where id ?; try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setLong(1, userId); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { // 处理结果 } } }这个写法无论中间是否抛异常三个资源都会按相反顺序关闭连接一定会归还。团队里如果有代码规范检查把“禁止手动 close”写进规则里比事后排查省事得多。另外一个容易被忽略的泄漏点是事务没提交也没回滚。某些框架里手动开了事务代码走到一半抛异常catch 里只打了日志没回滚连接一直挂在事务中归还时池子做清理会失败这个连接就废了。6.2 数据库侧把连接掐了症状和泄漏很像但成因完全相反活跃连接数并不高但每隔一段时间就集中报一批“连接已关闭”“Communications link failure”的错误过一会儿又恢复正常。这类问题的根源通常在中间链路。数据库服务端的wait_timeout到期会关闭空闲会话负载均衡设备、防火墙、容器网络都有空闲连接清理策略短则 5 分钟长则 1 小时。池子这边不知道连接已经断了下次借出去用的时候直接报错。处理思路是三层防线一起上第一层testWhileIdletrue配合timeBetweenEvictionRunsMillis小于服务端的空闲超时让池子在借出之前就发现死连接。第二层keepAlivetrue配合足够的minIdle让常驻的空闲连接定期被探测不停刷新活跃状态。这里再强调一次minIdle太小的话保活不生效。第三层phyTimeoutMillis设一个比中间设备空闲超时更短的值主动淘汰老连接重建不给设备清理的机会。三层的优先级是这样的如果中间设备超时未知phyTimeoutMillis是最可靠的一道如果服务端wait_timeout明确testWhileIdle就够了。实际配置里我一般三个都开代价只是后台多几个定时任务。排查时可以先确认一件事错误日志里的异常是不是集中在应用长时间无请求之后的第一批请求上。如果是基本可以断定是空闲连接失效而不是池子配置错误。6.3 常见问题速查表现象大概率原因排查动作启动即报GetConnectionTimeoutExceptionURL、账号密码错误或数据库不可达用命令行客户端验证连通性看 Druid 初始化日志的异常根因运行中偶发GetConnectionTimeoutExceptionmaxActive偏小、存在泄漏、慢 SQL 占住连接看监控台活跃连接与等待线程曲线按耗时排序 SQL配置的参数不生效properties 键名大小写错误或配置文件未被加载对比启动日志里打印的参数快照监控台 404Servlet 未注册或用了错误的 javax/jakarta 包检查 web.xml 或 starter 配置确认包名与项目一致监控台能打开但数据全空WebStatFilter未生效或请求被 exclusions 排除检查 filter 的 url-pattern 与排除列表日志大量discard connectionvalidationQuery执行失败连接确实已失效检查网络链路、phyTimeoutMillis、服务端空闲超时定时任务批量执行时连接打满批处理一次性取了过多连接分批处理控制单次批量大小避免和在线业务抢连接批量任务中途报“连接已关闭”removeAbandoned误杀了长事务连接调大removeAbandonedTimeout或关闭该功能内存持续增长poolPreparedStatements开启导致 PSCache 膨胀关闭 PSCache改用驱动自身的语句缓存7. 一份可以直接抄的生产参数基线讲完原理和坑最后给一份我常用的参数组合。这不是万能配置但作为一个起点是可靠的落地时按实际压测结果微调。7.1 参数基线与取值理由参数建议值取值理由initialSize5启动不至于给数据库造成连接风暴又能快速响应首批请求minIdle与日常并发所需相当如 10保证keepAlive有作用对象避免空闲连接被全部淘汰maxActive按max_connections反推通常 20 到 50超过数据库处理能力的连接只会增加排队开销maxWait2000 到 3000 毫秒与接口超时对齐快速失败优于线程堆积timeBetweenEvictionRunsMillis60000小于服务端空闲超时兼顾检测及时性与后台开销minEvictableIdleTimeMillis300000空闲五分钟以上的多余连接才回收避免频繁抖动validationQuerySELECT 1足够轻量能验证连接可用性testWhileIdletrue性价比最高的检测方式testOnBorrow/testOnReturnfalse避免每次借还都多一次往返keepAlivetrue主动维持常驻空闲连接phyTimeoutMillis1800000比多数网络设备的空闲清理阈值短主动重建poolPreparedStatementsfalseMySQL 场景下驱动自带缓存更合适removeAbandoned排查期开启稳定后关闭长事务场景下长期开启有误杀风险filtersstat,slf4j统计与日志wall按业务验证后再开connectionPropertiesdruid.stat.mergeSqltrue;druid.stat.slowSqlMillis2000合并同构 SQL慢 SQL 阈值两秒关于connectionProperties里的mergeSql多说一句。它会把参数不同但结构相同的 SQL 合并统计比如select * from t where id1和select * from t where id2会被归到同一条。开不开取决于你的使用习惯想看“哪类 SQL 最慢”就开想看“具体哪条 SQL 执行最多”就关。合并之后慢 SQL 日志里记录的是带占位符的 SQL定位具体参数值需要看执行明细。7.2 上线前必须做的三件事第一件事压测验证连接数。用接近生产峰值的并发跑一轮看监控台的活跃连接峰值和等待线程峰值。如果等待线程长时间大于 0先别急着调大maxActive去看是不是有慢 SQL 占着连接不放。加连接是最贵也最没技术含量的解法往往治标不治本。第二件事模拟数据库重启。在预发环境手动重启一次数据库观察应用能否自动恢复。这个测试能暴露很多配置问题有的配置在数据库重启后会持续抛异常直到应用重启有的能自动重连。恢复时间主要取决于timeBetweenConnectErrorMillis和connectionErrorRetryAttempts这两个参数默认值一般够用但值得实测一次。第三件事把监控接入告警。连接池这种组件出问题是渐进的等人发现接口变慢已经晚了。把活跃连接数超过maxActive的 80% 告警、等待线程数大于 0 持续一分钟告警、以及GetConnectionTimeoutException的日志计数接进监控能在用户感知之前介入。最后一个提醒改了连接池参数一定要重启验证。Druid 的多数参数在init()之后是只读的运行期修改不会生效也不会报错很容易误以为改成功了。我个人在实际操作中的体会是连接池配置这件事八成的精力应该花在“确认服务端和中间链路的超时是多少”上两成才花在调参数上。见过太多项目把maxActive从 20 调到 200 之后问题依旧最后发现是一条忘加索引的查询占着连接跑了几十秒。参数是护栏真正决定通行效率的还是路上有没有事故。