SpringBoot集成数据库连接池的配置经验分享
半夜两点线上告警突然炸了。数据库连接池耗尽应用彻底卡死重启后不到十分钟又复现。查了半天根因竟然是一个再常见不过的配置maximum-pool-size10。这个数字在测试环境跑得风生水起到了生产环境就成了催命符。那次事故之后我花了整整一周时间把SpringBoot集成数据库连接池的每条配置项都翻了个底朝天这里面的门道远比官方文档写的要复杂得多。别急着选Druid先搞懂HikariCP为什么是默认很多人在SpringBoot里首选Druid因为功能全、有监控面板。但如果你问一个踩过坑的人他会告诉你省心才是最大的效率。SpringBoot 2.x之后默认的HikariCP被无数生产环境验证过——它没有Druid那些花哨的SQL拦截和慢查询分析但就一个简单的指标性能。HikariCP的字节码级优化让它比传统连接池快一个数量级这在高并发下就是生死之差。如果你不是需要Druid的防火墙功能或者定制化监控别折腾直接用默认。但默认不代表不用配置。很多人图省事直接把数据源扔给SpringBoot结果就是默认的maximum-pool-size10。这是最坑的默认值之一。连接池的大小不是越大越好而是越“合适”越好。你的服务部署在4核8G的机器上数据库是另一台4核机器10个连接可能刚好但如果你的接口平均耗时200msQPS到100就得排队。算一笔账一个连接一个时刻只能处理一个请求10个连接意味着最多同时10个请求在跑其余全部等待。这就是半夜告警的根源。核心参数不是调参游戏是算出来的很多人喜欢在application.yml里写一堆配置看着很专业实际全是拍脑袋。连接池配置必须基于你的实际业务模型去计算。拿maximum-pool-size来说有一个经验公式核心数 2 有效存储设备数。但这只是起点更实际的做法是压测。你用一个连接跑一次典型业务SQL测出平均耗时然后用公式池大小 并发峰值 平均响应时间 / 1000单位毫秒去算。假设你的峰值并发是200平均响应时间是150ms那池大小就是30。这不是玄学是小学数学。另一个要命的参数是connection-timeout。默认是30000ms也就是30秒。如果你的池子里没有可用连接线程会干等30秒才报错。30秒足够让用户点十次刷新让负载均衡器把超时时间都耗光了。我一般直接设成3000ms宁可快速失败让用户重试也不要让线程挂在那儿当尸体。还有max-lifetimeHikariCP默认是1800000ms30分钟但你要注意它必须小于数据库的wait_timeout。很多数据库默认wait_timeout是8小时但云端RDS常常会主动断掉闲置连接。你设的max-lifetime大于数据库的断开时间就会疯狂报Connection is not available。最小空闲连接数是个陷阱别被“省资源”误导minimum-idle这个参数很多人喜欢设小比如1或者0觉得省资源。这是典型的省小钱亏大钱。当你的应用流量波动剧烈时如果最小连接数太小突发流量来了连接池必须现建连接。建立物理连接的成本非常高TCP握手加认证加MySQL权限检查轻松几十毫秒。在高并发瞬间你不仅要承受这几十毫秒的延迟还可能因为建连接太慢导致后续请求堆积。把minimum-idle设成和maximum-pool-size一样这不是浪费是应对冷启动的保险。连接池的意义就在于复用你连复用都不愿意不如不用连接池。再看看leak-detection-threshold这个参数最容易被忽略但它是排查连接泄漏的神器。默认是0意味着不检测。如果设成60000连接从池中借出超过60秒未归还就会打印警告日志。生产环境我强烈建议开启但别设太低因为有些慢SQL真的会跑很久超过阈值会误报。设到1分钟或2分钟既能抓住泄漏又不会打扰正常业务。从连接池的角度重新理解事务隔离级别你以为连接池只是管理连接的其实它直接决定了事务的隔离级别是否生效。SpringBoot里Transactional注解的isolation属性默认是Isolation.DEFAULT这个DEFAULT是数据库的默认。如果你在配置里用Primary自定义了一个DataSource但没设置spring.datasource.hikari.transaction-isolation那么所有事务都会用数据库默认级别。MySQL默认是可重复读PostgreSQL默认是读已提交。这个坑在于如果你在测试环境用MySQL生产环境切到PostgreSQL事务隔离级别就变了代码行为完全两样。所以要在HikariCP配置里显式指定比如spring: datasource: hikari: transaction-isolation: TRANSACTION_READ_COMMITTED别小看这一行它让事务行为可预期。验证连接是否存活别用默认的“查一查”连接池需要定期验证连接是否有效否则数据库重启后池里的连接全是死连接。HikariCP的connection-test-query默认是SELECT 1。这对MySQL够用但对某些数据库不够。更关键的是validation-timeout它必须小于connection-timeout否则验证逻辑会先于取连接超时触发导致验证超时后直接抛异常。我有一次就把validation-timeout设得比connection-timeout大结果连接池在数据库闪断后彻底无法恢复每次取连接都失效重启都无法解决。调了半天才发现是这个顺序问题。另外HikariCP还有一个keepalive-time参数默认是0。如果你设置了minimum-idle小于maximum-pool-size那么空余的连接在闲置时会被关闭直到达到minimum-idle。这本来没什么但如果你的业务是“白天高峰、半夜低谷、凌晨高峰”比如电商大促就会发生连接反复创建销毁。设置keepalive-time为60000让HikariCP每隔一分钟向空闲连接发一次探测保持连接不被数据库回收能显著减少重建连接的开销。避免把连接池当缓存用后归还的规矩你的代码里有没有写过这样的逻辑在Service里取了一个连接然后放到ThreadLocal里再跨几个方法用如果有恭喜你你正在制造连接泄漏。连接池的资源必须遵循“租用-归还”模式Spring的DataSourceUtils已经帮你管理了事务连接的生命周期。但如果你用了原生JDBC或者某些框架的Connection直接操作一定要在finally里close()。别以为连接池自己会清理连接池只能检测到超时未归还的连接但无法自动释放正在被持有的连接上的锁。我曾经遇到过一个线程持有连接不还导致这个连接上的事务锁一直不释放数据库的锁等待飙到几百秒整个表都锁死。最实用的排查方式在配置里开启leak-detection-threshold并且用JConsole或者Arthas去查看连接池的当前活跃连接数。一旦发现活跃数长期等于maximum-pool-size那就说明有连接被借出后没还。这时候去线程堆栈里找那些持有HikariProxyConnection的线程就能顺着找到泄漏点。监控与预警比调参更重要配置再合理看不到运行数据也是白搭。HikariCP自带metrics只需要引入micrometer-registry-prometheus就可以通过/actuator/metrics/hikaricp.connections.active等端点暴露实时指标。我封装了一个监控项当活跃连接数连续3分钟超过最大值的80%时发出预警当等待获取连接的平均时间超过500ms时预警当连接池创建连接的数量在一分钟内有增加时预警。最后一条其实最有用——正常运行的连接池几乎不会创建新连接一旦出现创建连接的频率飙升说明要么有连接泄漏要么数据库在重启要么流量瞬间暴涨。这几个信号比CPU、内存都更灵敏。在生产环境我把这些指标接入了钉钉告警和Grafana看板。有一回半夜收到预警点开看连接池创建数在每分钟几十个而活跃连接数并不高。最后定位到是另一个项目的定时任务在凌晨3点批量跑SQL每次跑完没关闭连接每次都等超时被强杀。那个项目用的是DruidDruid默认的removeAbandonedTimeout没开导致连接一直挂着。换了HikariCP以后这类问题在代码层就直接暴露出来了。配置的连接字符集和时区直接影响性能很多人连配置连接池时只关注连接数忽略URL参数。比如MySQL如果你的jdbcUrl没带characterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai那会引发两个问题乱码和SSL握手开销。SSL握手的开销在连接建立时是一次性的但连接池长期复用连接这个一次性会被摊薄反而无所谓。真正要命的是useServerPrepStmtstruecachePrepStmtstrue这两个参数。如果不开启MySQL每次执行预编译语句都会重新发送给服务端连接池复用的连接也省不了预编译的解析时间。开启后PreparedStatement会被缓存在连接上对于频繁执行相同SQL的系统性能提升非常可观。我在一个报表系统上做了对比开启缓存后相同查询耗时从15ms降到3ms。还有rewriteBatchedStatementstrue。如果你用JdbcTemplate批量插入这个参数能让多条语句合并成一次网络传输。开启后批量插入性能提升10倍都有可能。这些参数看起来跟连接池无关但它们是连接池里每个连接的属性是享受连接池复用红利的前提。数据源多组配置别踩优先级的地雷如果你的应用有读写分离或者多数据源SpringBoot的自动配置会变得很麻烦。官方文档说得很清楚你一旦自定义了DataSource默认的DataSourceAutoConfiguration就不会生效。但很多人不知道的是spring.datasource.hikari.的配置只对自动配置的那个数据源生效。如果你自定义了DataSource并且用Primary标注那这些配置项就全成了摆设。我见过一个项目在application.yml里写了spring.datasource.hikari.maximum-pool-size50但代码里又手动new了一个HikariDataSource没设任何参数结果生产环境连接池只有默认的10。这个坑特别隐蔽因为日志里看不出区别只有压测时才发现吞吐不对。所以自定义数据源时一定要用ConfigurationProperties(prefix spring.datasource.hikari)显式绑定配置。或者更简单的方式用spring.datasource.type指定HikariDataSource所有属性通过spring.datasource.hikari注入。多点一个注解少花一天排查时间。读超时和连接池的关系你忽略了吗还有一个容易混淆的概念连接池大小和数据库连接超时是两码事。connection-timeout是获取连接的超时而socketTimeout是数据库读和写的超时。很多人在应用层配置了connection-timeout但数据库的socketTimeout是默认0意味着无限等待。如果有个慢SQL卡了五分钟而你的连接池最大连接数只有10那5个这样的慢SQL就占满了所有连接。解决方法是在连接池的jdbcUrl里加上socketTimeout30000让数据库端强制限制每条SQL的执行时间。这个参数比Spring的Transactional(timeout 30)更底层、更硬核。因为Transactional超时会触发事务回滚但连接仍可能被占用而socketTimeout是数据库主动断开连接会被立即释放。最终的“经验”是什么说一千道一万连接池配置没有银弹只有不断压测、监控、调整的循环。但有一个原则是通用的每次调整一个参数就要在压测环境里跑一遍你的峰值负载并且把响应时间、TP99、连接池活跃数、等待时间这几个指标拉出来看。别相信任何博客给的标准配置——包括我这篇。因为你的数据库性能、机器规格、业务模型都不一样。但方法论是共通的让连接池的连接数尽量保持稳定让获取连接的等待时间趋近于零让连接创建频率趋近于零。做到这三点你的连接池就是健康的。最后送你一个自检清单你的maximum-pool-size是基于哪个公式算出来的minimum-idle是多少connection-timeout是3000ms以下吗leak-detection-threshold开了吗keepalive-time设了吗transaction-isolation显式指定了吗socketTimeout在URL里吗如果这些问题你都能斩钉截铁地回答那么恭喜你你不再是那个半夜被连接池告警叫醒的人了。