ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Spring数据源配置实战:连接池、多数据源与排错经验总结

Spring数据源配置实战:连接池、多数据源与排错经验总结 做Java后端这些年Spring的DataSource配置几乎是我每次接手新项目都要重新看一遍的东西。说它简单吧确实就是填个URL、用户名和密码但说它复杂生产环境里因为数据源配置翻车的案例我见过太多了——连接池被卡满、多数据源互相干扰、为了防连接泄漏开了removeAbandoned结果应用直接假死。DataSource是应用访问数据库的唯一入口配置是否合理直接决定服务的吞吐上限和排错效率。这篇文章不打算写教科书就聊聊我在实际项目里折腾Spring数据源配置积累下来的经验先讲清楚数据源在Spring容器里到底是怎么被管理的再逐个拆Druid连接池的参数接着重点聊多数据源和事务的坑最后分享几段真实排错过程和一组兜底配置。1. 数据源在Spring容器里到底是怎么被管理的1.1 从DriverManager到DataSource连接池解决的核心问题回忆一下JDBC刚出来时候的写法大部分老程序员都写过类似代码Class.forName(com.mysql.jdbc.Driver); Connection conn DriverManager.getConnection( jdbc:mysql://localhost:3306/demo, root, 123456); Statement stmt conn.createStatement(); // 执行SQL... conn.close();这个方案最大的问题就是慢。每一次getConnection都是一次完整的物理连接建立TCP三次握手、MySQL服务端认证、分配线程和内存一个连接建立下来少说几十毫秒多则几百毫秒。低并发场景无感一旦请求量上来光建立连接的开销就能把数据库打满更别提每个连接还要消耗数据库端的内存和线程资源。连接池的思路很简单提前建好一批连接放在池子里用的时候取用完还回去而不是销毁。这样把创建连接的开销分摊到整个应用生命周期里对数据库来说应用始终维持着一批稳定的连接不会频繁建连断连。DataSource接口就是在这个背景下被设计出来的。它位于javax.sql包是JDBC标准扩展的一部分对外暴露的核心方法就是getConnection()。调用方不需要关心连接是从池子里拿的还是新建的只需要拿到Connection用用完调用close()——在连接池实现中这个close()是归还连接而不是真正关闭连接。打个比方连接池就像共享单车平台getConnection()是扫开锁close()是把车还到停车点。如果你每次用完直接把车砸了那这个平台就废了。1.2 IoC容器带来的装配变化数据源也是一个单例BeanSpring引入IoC之后DataSource就变成了一个普普通通的Bean由容器负责创建、注入和销毁。在XML时代一个典型的数据源配置长这样bean iddataSource classorg.apache.commons.dbcp2.BasicDataSource destroy-methodclose property namedriverClassName valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/demo?useSSLfalseamp;serverTimezoneAsia/Shanghai/ property nameusername valueroot/ property namepassword value123456/ /bean这里最容易被忽略的是destroy-methodclose。Spring容器默认不会去调用Bean的什么特殊方法如果连接池实现了java.io.Closeable或AutoCloseableSpring在初始化时能推断出销毁方法但老Spring版本和XML配置里还是靠这个属性告诉容器销毁这个Bean的时候调用close()。没配的话应用重启时连接池的close方法不会被调用数据库那边会积累一堆残留连接直到超出wait_timeout才被服务端杀掉。既然数据源是单例Bean你当然可以通过ApplicationContext.getBean(dataSource, DataSource.class)把它拿出来这个能力在生产排错时很有用比如临时写一个测试接口看看拿到的Bean到底是哪个连接池实现是一种最直接的getBean验证手段。这也是为什么我说理解IoC对排查数据源问题至关重要——你代码里Autowired DataSource拿到的那个对象来源可能在自动配置里也可能被你自己的某个Configuration覆盖了。1.3 Spring Boot的自动配置一个ConditionalOnMissingBean的游戏Spring Boot时代数据源配置的入口是spring.datasource.*这一组配置项。自动配置的核心类是DataSourceAutoConfiguration它的工作逻辑可以概括成三个判断classpath里有没有可用的连接池实现有没有已经存在的数据源Bean有没有配置spring.datasource.url。如果以上条件满足Spring Boot会使用DataSourceProperties读取配置再通过DataSourceBuilder创建一个DataSource。这个过程中最关键的注解是ConditionalOnMissingBean只要容器里没有任何DataSource类型的Bean自动配置才会生效一旦你自己定义了一个Bean DataSourceSpring Boot立刻退居二线。很多同学在这里容易产生一个误解以为Spring Boot自动配置是叠加在自定义配置之上的。实际恰恰相反它是缺省逻辑——你定义了它就闭嘴。所以如果你自定义了一个DataSource但没配好连接池参数自动配置不会帮你兜底。另外要注意Spring Boot 2.x默认连接池是HikariCPclasspath里即使有Druid如果没有显式用spring.datasource.typecom.alibaba.druid.pool.DruidDataSource指定或者没有引入Druid的StarterSpring Boot依然会用Hikari。这个点引出了后面章节要展开的问题你配了一堆Druid参数但容器里的实际Bean可能根本不是Druid配置自然就全部无效了。2. 四种装配方式从老项目的XML到Spring Boot自动配置2.1 XML时代的经典写法2010年左右的项目数据源配置基本都在applicationContext.xml里。除了前面那一段BasicDataSource的示例更常见的是把数据库连接信息抽到jdbc.properties再用context:property-placeholder引入context:property-placeholder locationclasspath:jdbc.properties/ bean iddataSource classcom.alibaba.druid.pool.DruidDataSource destroy-methodclose init-methodinit property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ /bean这个写法里有几个经验可以提一下driverClassName在MySQL 5.x时代是com.mysql.jdbc.DriverMySQL 8.x开始必须改成com.mysql.cj.jdbc.Driver并且URL后面要带serverTimezoneAsia/Shanghai否则驱动报错。useSSLfalse建议加上MySQL 5.7以后默认开启SSL会有告警日志本地开发和测试环境没必要。URL里的在XML中要写成amp;漏了这个XML解析直接报错。当时还有一个经典坑jdbc.properties里写了jdbc.username结果和系统环境变量USERNAME冲突Spring按SystemProperties优先解析把Windows用户名当成数据库用户名启动一片诡异报错。后来团队约定所有配置项都必须加前缀比如db.username就是被这个坑逼出来的。2.2 JavaConfig方式为什么类型安全的Bean更推荐Spring 3.0之后JavaConfig逐渐成为主流数据源配置变成Configuration public class DataSourceConfig { Bean public DataSource dataSource() { DruidDataSource dataSource new DruidDataSource(); dataSource.setUrl(jdbc:mysql://localhost:3306/demo?useSSLfalseserverTimezoneAsia/Shanghai); dataSource.setUsername(root); dataSource.setPassword(123456); dataSource.setInitialSize(5); dataSource.setMaxActive(20); return dataSource; } }JavaConfig的好处是类型安全方法参数和返回值都有编译器帮你把关不像XML那样写错了property名到运行期才报错。但纯粹的new DruidDataSource()加setter有一个明显问题配置被写死在代码里换环境得改代码重新编译。更推荐的方案是结合ConfigurationProperties把配置外置Configuration public class DataSourceConfig { Bean ConfigurationProperties(prefix spring.datasource.druid) public DataSource dataSource() { return new DruidDataSource(); } }然后在application.yml里写spring: datasource: druid: url: jdbc:mysql://localhost:3306/demo?useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 initial-size: 5 max-active: 20注意一点ConfigurationProperties绑定是在Spring创建Bean之后、初始化完成之前执行的所以数据源对象会先new出来再把prefix对应的外部配置set进去。如果你在dataSource()方法里已经手动调用了setUrl又配了spring.datasource.druid.url最终生效的是外部配置——因为绑定发生得晚。这个顺序理解到位排查我代码里明明写了怎么还是用配置文件的时就有数了。2.3 Spring Boot自动配置spring.datasource前缀下的约定Spring Boot项目里最常见的写法就是只写application.ymlspring: datasource: url: jdbc:mysql://localhost:3306/demo?useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver这一套配置会被DataSourceProperties读取然后交给自动配置创建连接池。如果你classpath里同时存在多个连接池Spring Boot有一个内部优先级顺序大致是HikariCP优先于Tomcat JDBCTomcat JDBC优先于DBCP2。除非你用spring.datasource.type显式指定否则默认就是按这个顺序挑。想要用Druid在Spring Boot 2.x里有两条路。第一条是引入druid-spring-boot-starter它会提供DruidDataSourceAutoConfigure让Druid参与到自动配置里第二条是自己定义DataSourceBean然后在yml或代码里设置参数。两条路的差别在于Starter方式会接管自动配置连Druid的监控面板StatViewServlet、Web关联监控都一并配好自己定义Bean则只解决连接池本身监控要自己再注册。2.4 多DataSource时的Primary/Qualifier定位当容器里有两个DataSource时事情就开始变复杂了。最简单的情况是Bean Primary ConfigurationProperties(prefix spring.datasource.primary) public DataSource primaryDataSource() { return DataSourceBuilder.create().build(); } Bean ConfigurationProperties(prefix spring.datasource.secondary) public DataSource secondaryDataSource() { return DataSourceBuilder.create().build(); }如果没有PrimarySpring在Autowired DataSource时会报expected single matching bean but found 2。Primary的含义是多候选者中优先选择我相当于给自动配置和依赖注入指定了一个默认数据源。Qualifier则用于精确指定注入哪个具体BeanAutowired Qualifier(secondaryDataSource) private DataSource secondaryDataSource;这里还有一个连带关系DataSourceTransactionManager、JdbcTemplate这些自动配置Bean默认都会去找容器里唯一的DataSource或标了Primary的那个DataSource。如果你把Primary加在了从库上主库的事务管理器就全乱了。所以多数据源下主库的那个Bean必须标Primary这是硬性约定不然后面所有基于DataSource的自动配置都会选错对象。3. 连接池参数逐项拆解Druid那些参数真正在干什么3.1 连接池选型的现实对比讨论具体参数之前先花半分钟回答一个绕不开的问题到底选Druid还是HikariCP我在项目里两种都用过说下直观感受。维度DruidHikariCPDBCP2维护方阿里巴巴开源国内社区活跃日本开发者开源Spring Boot默认Apache Commons性能不错的水平功能多有一定开销微基准测试通常最快一般监控能力内置SQL统计、慢SQL、连接泄漏检测依赖系统指标和第三方采集较弱扩展功能SQL防火墙、SQL解析合并、Web关联监控相对轻量极少典型适用需要SQL监控、审计、防护的中大型项目极高吞吐、极简依赖的微服务老项目维护我的经验是如果你项目里有我要查某个SQL执行了多少次、平均耗时多少这类诉求Druid的监控面板几乎是现成答案如果团队追求极致的简单和低开销HikariCP更合适。连接池没有绝对好坏只有适不适合业务场景。下面所有参数讲解以Druid为例因为它的可调项最丰富Hikari的相对少一些。3.2 一套完整的Druid配置长什么样网上能搜到的Druid配置大多零散我直接给一份完整可用的YAML配合后面的参数说明一起看spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/demo?useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 druid: initial-size: 5 min-idle: 5 max-active: 20 max-wait: 60000 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: true max-pool-prepared-statement-per-connection-size: 20 filters: stat,wall,slf4j connection-properties: druid.stat.mergeSqltrue;druid.stat.slowSqlMillis5000 remove-abandoned: true remove-abandoned-timeout: 180 log-abandoned: true注意一个容易搞混的点type、driver-class-name、url、username、password这几个是spring.datasource下的公共属性只要DataSource统一它们在spring.datasource.druid下也可以配置而initial-size、max-active这些连接池特有参数在Druid Starter里要放到spring.datasource.druid下面。前缀写错了参数就失效后面第五章会专门聊怎么排查这种配置没生效的问题。3.3 每个参数在设计上想解决什么问题把常用参数按用途分几组逐一说清楚。容量类参数initialSize、minIdle、maxActiveinitial-size应用启动时就建立的连接数。设大一点的好处是第一个请求进来不用等慢启动坏处是占用数据库连接资源尤其是几十个服务实例同时启动时。我一般设5小服务甚至可以不设。min-idle池里最少保持的空闲连接数。高于initial-size时连接池会慢慢把空闲连接补到这个数值。这个参数的意义在于应对突发流量——如果空闲连接都被占完新请求必须临时新建连接会有一个短暂的等待。max-active池里最大的活跃连接数这是最重要的容量上限。给个粗略估算思路假设一个SQL平均耗时10ms那么一个连接每秒可以执行约100条SQL如果你的接口每请求平均跑3条SQLQPS是500那每秒需要执行的SQL就是1500条理论连接数约等于1500除以100也就是15个再考虑峰值翻倍和慢SQL冲刷取30到50比较稳。但我必须强调这个公式只是估算起点真正合理的值要看线上监控的active曲线我后面会讲怎么做验证。获取连接超时参数maxWaitmax-wait从池里获取连接的最大等待毫秒数。默认-1表示无限等待生产环境我强烈不建议用默认值。设想一下maxActive已满新的请求线程全都阻塞在getConnection()上数据库端连接数没有新增但应用线程全卡死这个状态非常难排查。配置max-wait: 60000的意思是等60秒还没连接就抛GetConnectionTimeoutException至少让异常暴露出来而不是让整个应用无限挂起。空闲连接管理参数time-between-eviction-runs-millis连接池后台线程每隔多长时间检测一次空闲连接默认60秒。这个线程的主要任务是把超过min-evictable-idle-time-millis的空闲连接淘汰掉同时执行validation-query验证空闲连接是否有效。min-evictable-idle-time-millis空闲连接存活的最小时间。比如默认5分钟意思是连接空闲超过5分钟后才会被后台线程回收。这两个参数配合起来控制池子的新陈代谢。连接有效性检测三兄弟testWhileIdle、testOnBorrow、testOnReturntest-on-borrow每次从池里借连接时先拿validation-query验证这个连接是否还能用。最严谨但每借一次就多一次探测SQL高并发下有额外开销。test-while-idle后台线程在空闲检测时顺手验证空闲连接。代价小而且能发现被数据库服务端关闭的死连接。test-on-return连接归还时验证。用的最少一般不需要。Druid的默认组合是test-while-idle: true加test-on-borrow: false我认为这是比较均衡的选择。HikariCP则是默认在借出时做一次isValid判断策略不同但效果接近。PreparedStatement缓存参数pool-prepared-statements: true配合max-pool-prepared-statement-per-connection-size: 20会为每个连接缓存20条PreparedStatement避免重复解析SQL。这个参数在Oracle这类SQL解析代价高的数据库上收益明显MySQL场景收益有限还有可能增加内存占用。具体要不要开建议压测后决定。监控与防护参数filters和connectionPropertiesfilters: stat,wall,slf4j是Druid特有的过滤器链。stat负责SQL统计不开它Druid监控页面就没有数据wall是SQL防火墙拦截明显有注入风险的SQLslf4j把慢SQL等信息打到日志。配置过滤器前要确认对应的jar包在classpath中一般druid本身的jar就自带了这些filter实现。connection-properties里的druid.stat.mergeSqltrue会把结构相同的SQL合并统计druid.stat.slowSqlMillis5000把超过5秒的查询算作慢SQL。慢SQL阈值要结合业务看不是所有场景都适合5秒这个数。连接泄漏兜底参数removeAbandoned系列这里单独展开说因为很多团队配置错了这个参数导致生产事故。remove-abandoned: true的含义是如果一个连接被借出后超过remove-abandoned-timeout秒还未归还连接池直接强制回收这条连接。log-abandoned: true则要求在回收时打印当时的异常栈方便你定位是谁借走不还。这个机制的设计动机很明确对付代码里漏写close()的连接泄漏。但我见过太多次为防泄漏开这个开关、结果把正常的长事务干掉的案例——比如一个定时任务批量导入数据一个事务跑了5分钟超过180秒还没结束Druid直接强杀连接事务中断数据一致性出问题。所以我的建议是仅当代码审查发现确实存在大量连接泄漏问题时才开启开启时必须先打开log-abandoned抓泄漏源remove-abandoned-timeout值必须大于任何正常业务的最长事务时间短交易系统可以设180有批处理任务的要设到600以上甚至不开这个开关应该当作排雷工具而不是长期运行的常规配置——修完泄漏代码后尽量关掉。3.4 配置提交前先用日志验证一遍很多同学配好参数就上生产直到出问题才回头看日志。我分享一个最简单有效的验证方式启动应用时打开数据源的debug级日志Druid会打印类似这样的初始化信息INFO DruidDataSource - {dataSource-1} inited这段日志说明Druid成功初始化了。如果启动日志里出现的是HikariPool的日志而你又以为自己在用Druid那这个项目拧巴的程度就可想而知了。另外Druid启动后访问配置好的/druid/index.html监控页先看数据源页签里的实际连接的InitialSize、MaxActive等值再跑几个SQL看SQL监控页签是否出来数据。这两步过了说明参数至少是生效的。4. 多数据源与事务边界线上事故频发的重灾区4.1 什么时候需要多数据源多数据源不是设计目标是被业务逼出来的。常见场景有三种读写分离主库负责写从库负责读把读流量从主库剥离开多业务库用户库和订单库物理分离一个服务同时访问异构数据存储业务库是MySQL报表库是ClickHouse或PostgreSQL服务层要同时读写两边。不管哪种场景思路都是让一个Bean对应一个DataSource各自管理各的连接池。最大的困难不在配置本身而在事务边界——Spring事务默认绑定在单个数据源上跨库就不是它管得了的。4.2 最简单的多数据源配置两个Bean 两个JdbcTemplate如果业务比较简单两个数据源固定使用不需要动态切换那么这种配置就够用了Configuration public class MultiDataSourceConfig { Bean Primary ConfigurationProperties(prefix spring.datasource.primary) public DataSource primaryDataSource() { return DataSourceBuilder.create().build(); } Bean ConfigurationProperties(prefix spring.datasource.secondary) public DataSource secondaryDataSource() { return DataSourceBuilder.create().build(); } Bean public JdbcTemplate primaryJdbcTemplate(Qualifier(primaryDataSource) DataSource ds) { return new JdbcTemplate(ds); } Bean public JdbcTemplate secondaryJdbcTemplate(Qualifier(secondaryDataSource) DataSource ds) { return new JdbcTemplate(ds); } }对应的yml配置spring: datasource: primary: url: jdbc:mysql://localhost:3306/primary_db?useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver secondary: url: jdbc:mysql://localhost:3306/secondary_db?useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver使用时就通过Qualifier注入对应的JdbcTemplate。注意给两个数据源分别建了JdbcTemplate之后MyBatis-Plus、Spring Data JPA这些框架要配各自的SqlSessionFactory或EntityManagerFactory工作量会上去很多项目走到这一步就开始考虑动态数据源方案了。4.3 动态数据源的实现与经典误区所谓动态数据源核心是Spring提供的AbstractRoutingDataSource。它本身是一个DataSource内部维护一个targetDataSources映射在真正拿连接时通过determineCurrentLookupKey()返回的key决定走哪个数据源public class DynamicDataSource extends AbstractRoutingDataSource { Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.get(); } } public class DataSourceContextHolder { private static final ThreadLocalString CONTEXT new ThreadLocal(); public static void set(String db) { CONTEXT.set(db); } public static String get() { return CONTEXT.get(); } public static void clear() { CONTEXT.remove(); } }使用方式是在需要切换库的地方调用DataSourceContextHolder.set(secondary)业务结束后在finally里clear()。这个方案有两个经典坑都是我实际踩过的。第一个坑是线程复用导致上下文串库。应用线程池里的线程是复用的如果前一个请求set(secondary)后没清理下一个请求在同一个线程上执行时ThreadLocal里还残留着secondary读操作就跑到从库去了。解决办法没有捷径凡是set过的代码路径必须在finally里调clear()一个都不能漏。第二个坑更隐蔽也是线上事故最多见的事务先于数据源切换启动。Transactional开启时Spring事务管理器会从当前数据源获取一个Connection并把它绑定到当前线程的事务资源里。之后你在同一个方法里再切换DataSourceContextHolder的值拿到连接的SQL仍走的是事务开启时那个连接——因为事务已经把手伸进池子里把连接拽住了AbstractRoutingDataSource不会再参与第二次决策。我见过一个团队做了读写分离改造方法上加了Transactional然后在方法内部根据读写标记切换数据源结果写入生效了、查询实际全走了主库他们查了一天最后用日志逐个打印了连接的URL才定位到事务时序问题。正确的做法是把数据源切换放到事务方法之外完成或者更彻底一点——把读写分离的库放到不同的服务里用服务边界隔离开不要在同一个事务里纠结数据源路由。4.4 跨库事务怎么选多数据源意味着跨库事务早晚会遇到。先说结论不要一上来就引入分布式事务框架大多数业务根本用不上。Transactional默认只能管单个数据源的本地事务一个方法里同时操作两个库最多只保证第一个库的事务第二个库出错时第一个库已经提交了——数据不一致由此而来。真正需要强一致性的方案有三个方向JTA通过Atomikos等实现两阶段提交标准但性能开销大适合小规模跨库场景Spring Boot里有starter可以集成Seata阿里的分布式事务方案AT模式对代码侵入小但需要部署TC服务器运维成本不低最终一致性本地消息表、MQ事务消息、定时对账补偿适合大部分非金融核心业务。我的建议很简单能拆服务就拆服务别让一个事务跨越两个库实在拆不了的评估业务能否接受短时间不一致能用最终一致性方案就别上分布式事务。分布式事务不是银弹它只是把一致性模型暴露得更明显该有的回滚和补偿逻辑一点不会少。5. 数据源配置排错实录四类常见问题的完整排查链路5.1 启动失败Failed to configure a DataSource这个报错信息出现频率极高核心内容一般是Failed to configure a DataSource: url attribute is not specified and no embedded datasource could be configured. Reason: Failed to determine a suitable driver class它想告诉你的是两件事要么你没配数据源的url要么classpath里根本没有可用的数据库驱动。排查链路按顺序走先检查pom.xml或build.gradle有没有mysql-connector-j依赖。没有驱动一切白搭检查是否有spring-boot-starter-jdbc、MyBatis Starter这类依赖它们会触发数据源自动配置。如果项目里引入了这些依赖但没配数据源自动配置就会报错检查application.yml缩进和字段名。spring.datasource.url少一个空格或变成了spring.data.url属性就读不进去。这种低级错误占了排查时间的三分之一检查有没有环境变量干扰。比如服务器上设置了SPRING_DATASOURCE_URL但指向一个不存在的库会覆盖yml里的配置。5.2 配置被忽略自定义参数没生效症状很典型你按Druid的文档配了max-active: 20但监控页里显示最大活跃连接数还是8甚至是HikariPool的日志。直接原因容器里的实际Bean根本不是Druid而是HikariCP或者别的连接池。为什么因为classpath里没有Druid的Starter你也没有自定义DataSourceBean那么Spring Boot就按照默认优先级挑了HikariCP。你写的spring.datasource.druid.*前缀对Hikari来说是完全陌生的配置Hikari根本不会读它。验证方法很简单启动日志里搜HikariPool或DruidDataSource关键字一眼就知道当前用的是什么连接池。想改正要么在spring.datasource.type里显式指定为Druid要么引入druid-spring-boot-starter并保证数据源前缀正确spring: datasource: type: com.alibaba.druid.pool.DruidDataSource druid: max-active: 20也提醒一句如果项目同时引入了druid-spring-boot-starter和Hikari依赖自己又没有定义DataSource Bean时Starter的自动配置优先级可能让Druid接管。这时候要有一个明确统一的连接池决定避免两个连接池在classpath里互相打架。5.3 连接耗尽GetConnectionTimeoutException这是生产环境最危险的症状通常出现在流量高峰。Druid的报错长这样com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis 60000, active 20, maxActive 20, creating 0这条异常信息已经把原因暴露得很清楚了你设置了max-wait为60000毫秒等了整整一分钟还没拿到连接池里20个连接全部处于active状态。完整的排查链路应该是先看Druid监控页的数据源页面active数量是不是长时间顶在maxActive。如果是说明连接不够用或连接被占着不还切到SQL监控页面按执行次数和耗时排序看有没有超大查询、慢查询把连接长期占住。尤其注意SELECT ... FOR UPDATE这类带锁的SQL锁等待期间连接一直被持有用jstack抓应用线程栈搜索getConnection关键字的堆栈看阻塞在等待连接的都是哪些业务代码。如果大量线程卡在同一段代码上基本可以锁定连接泄漏或死锁点如果定位到代码里有没有关闭的Connection先修复再考虑是否开remove-abandoned兜底。需要特别提醒的是连接耗尽很多时候不是连接池参数太小而是慢SQL或长事务把连接占满了。直接把maxActive调大治标不治本连接的总数受数据库端max_connections限制池子再大数据库也撑不住。5.4 自定义DataSource被悄悄覆盖还有一种比较隐蔽的情况你明明在代码里自定义了DataSource但运行起来好像用的不是你定义的那个——比如你配的是Druid实际日志里出现的是Hikari。要理解为什么得回到Spring Boot自动配置的ConditionalOnMissingBean上。这个条件注解只对自动配置生效也就是它只在你没有任何DataSource Bean时才创建数据源。但如果你定义了DataSource自动配置确实会退出。那么覆盖发生在哪里可能有两个原因你的项目里不止一个模块配置了DataSource Bean。两个Configuration都定义了同名Bean后扫描的会覆盖前面的如果开了spring.main.allow-bean-definition-overridingtrue连报错都不会有静默覆盖。自定义DataSource Bean的定义放在了某些被组件扫描重复加载的位置比如同一个类被两个不同的扫描路径命中。排查手段是我前面提到过的写一个简单的接口返回dataSource.getClass()或者通过ApplicationContext.getBean(DataSource.class)打印Bean的类名看清容器里装的到底是什么。如果发现不是预期的类型就去检查所有Configuration类里对DataSource的重新定义。6. 生产环境数据源配置的兜底清单6.1 一份长期运行验证过的配置模板结合前面章节的拆解给出我在生产环境长期使用的一套MySQL 8 Druid配置模板可以结合自己的业务压测数据调整spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://yourhost:3306/yourdb?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowMultiQueriestrue username: youruser password: yourpassword druid: # 容量配置 initial-size: 5 min-idle: 5 max-active: 30 max-wait: 60000 # 空闲连接管理 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 # 慢SQL监控 filters: stat,wall connection-properties: druid.stat.mergeSqltrue;druid.stat.slowSqlMillis3000 # 连接泄漏兜底自己根据长事务情况决定是否开 remove-abandoned: false remove-abandoned-timeout: 180 log-abandoned: true这套配置的思路是容量上不会一上来就超大池子max-active给30先观察监控再调max-wait给60秒给慢查询一定缓冲但不会无限挂起慢SQL阈值设3秒业务上线后根据实际SQL耗时再调整。6.2 上线前必须验证的四个小实验配置不能到线上再验证我在项目交付前通常会做四个实验第一个是重启验证。应用重启后打开日志确认数据源完成了初始化连接池按initial-size建好了连接而不是等到第一个请求才慢启动。第二个是断连恢复。把数据库临时停掉30秒或直接重启观察应用日志里连接池后台线程是否探测到连接失效数据库恢复后应用能否自动重新建立连接。这个实验能直接验证你配的test-while-idle和time-between-eviction-runs-millis参数是否靠谱。第三个是连接等待验证。用一个长锁或者SELECT ... FOR UPDATE把连接占住再发起一批并发请求确认max-wait超时后异常能正常抛出业务能收到明确失败而不是无限卡死。第四个是压测峰值验证。用大于预估峰值的并发跑一轮压测记录监控页里的active峰值。如果峰值距离max-active很远说明池子开大了如果直接顶到max-active就要评估是扩容还是优化SQL。6.3 别忘了连接之外的几个细节数据源配置不等于连接池参数有几个外围细节经常被忽略但它们对最终稳定性影响很大。一个是URL里的字符集和时区。MySQL 8驱动的URL里不带characterEncodingutf8和characterEncodingutf8时中文数据可能变成问号不带serverTimezoneAsia/Shanghai则驱动直接报错。这两个配置值看起来小出问题的时候最难查。一个是驱动版本与连接池的兼容性。不同版本的mysql-connector-j在某些参数行为上有差异比如老版本驱动对useSSL的默认值就和新版本不一样。升级驱动版本时哪怕只升一个小版本也要把数据源相关功能回归一遍。一个是账号权限。我见过生产事故的直接原因是给数据源用的账号权限被回收且连接池里还残留着旧连接表面上能拿到连接但一执行SQL就报无权限。权限变更后最好让连接池把现有连接淘汰掉或者干脆重启应用不要留着可疑的池子继续跑。踩了这么多年数据源的坑我个人的体会就一句话数据源配置永远不要照抄网上的模板先搞清楚你的连接池是谁、你的SQL有多慢、你的业务能容忍连接等待多久再去动参数。把remove-abandoned这类看起来很有用的开关当成最后手段而不是默认选项你会少很多半夜被叫起来的经历。
返回列表