ARTICLE DETAIL

资讯详情

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

Spring数据源配置全解析:连接池选型与多数据源实战

Spring数据源配置全解析:连接池选型与多数据源实战 Spring 里 DataSource 配置这件事说难不难说简单也容易翻车。很多项目跑着跑着突然连不上数据库重启一下又好了十有八九就是数据源配置埋了雷。这篇就把 Spring 体系下的数据源配置从头到尾捋一遍从连接池选型、单数据源配置、多数据源动态切换一直到线上常见的坑和排查手段全部基于实际踩过的经验来写希望能帮你少走点弯路。这篇内容适合谁看刚接触 Spring 的初级开发者、写 Spring Boot 项目但没认真研究过数据源的中间层同学、以及被数据库连接池问题折磨过的运维和全栈开发。看完至少能搞清楚你项目里的数据源到底是怎么装配起来的、每个参数到底在干什么以及出了问题该往哪个方向查。1. 内容整体设计与思路拆解1.1 数据源在 Spring 体系中的定位先明确一个概念DataSource 在 Spring 里是数据库连接的工厂它本身不是连接池但它往往扮演着连接池的角色。JDBC 原生方式是DriverManager.getConnection()每次手动创建连接用完再手动关闭这种做法在并发场景下就是灾难。DataSource 接口的出现就是为了把连接的管理行为规范化获取连接、归还连接、连接池扩容缩容全都交给实现类来处理。Spring 对 DataSource 的介入分三层做。第一层是基础设施层Spring 提供DataSourceUtils来管理事务性连接保证同一个事务里多次 DAO 调用用的是同一个数据库连接第二层是配置层通过 Bean 定义把 DataSource 注入到 JdbcTemplate、MyBatis、JPA 等上层组件第三层是事务抽象层DataSourceTransactionManager依赖 DataSource 来开启、提交、回滚事务。所以你会发现数据源配置不只是配一个连接池的问题它直接决定了你项目的事务边界、并发上限、SQL 执行性能上限。配置合理后面所有上层组件都顺配置混乱调试起来你会怀疑人生。1.2 常见配置方式的演进与选型Spring 框架本身经历了 XML 配置时代、Java Config 时代和 Boot 自动配置时代。这三种方式至今都在存量项目里共存处理方式完全不同。XML 时代长这样bean iddataSource classcom.alibaba.druid.pool.DruidDataSource init-methodinit destroy-methodclose property namedriverClassName valuecom.mysql.cj.jdbc.Driver / property nameurl valuejdbc:mysql://localhost:3306/test / property nameusername valueroot / property namepassword value123456 / /beanJava Config 时代则是这样Bean(destroyMethod close) public DataSource dataSource() { DruidDataSource ds new DruidDataSource(); ds.setDriverClassName(com.mysql.cj.jdbc.Driver); ds.setUrl(jdbc:mysql://localhost:3306/test); ds.setUsername(root); ds.setPassword(123456); return ds; }Spring Boot 时代直接在application.yml里写spring: datasource: url: jdbc:mysql://localhost:3306/test username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver这三种方式背后其实是同一个思想先建连接池对象再填参数最后交给 Spring 容器管理生命周期。理解了这个你就不会被五花八门的写法搞晕。我的建议很直接新项目一律用 Spring Boot 的自动配置如果要定制连接池参数用spring.datasource.druid.*或spring.datasource.hikari.*这样有针对性的前缀去设置尽量避免直接 new 一个 DataSource。直接 new 的问题在于你得手动管理连接池的初始化和销毁稍不注意连接池就泄漏了。1.3 连接池选型的分歧与取舍市面上 Java 连接池主流就这三家DBCP2、C3P0、HikariCP、Druid。DBCP2 现在很少用C3P0 在 Hibernate 旧项目里还能见到真正常用的是 HikariCP 和 Druid。HikariCP 是 Spring Boot 2.x 以后的默认连接池性能极好flyweight 模式极简实现字节码优化做得很极致。它的核心思路是尽量减少连接池内部的锁竞争把连接数组化、按线程 id 分片降低并发碰撞。如果项目里没有特别强烈的监控诉求直接用它就够了。Druid 是阿里开源的连接池性能上略逊 HikariCP但胜在功能全内置 SQL 防火墙、慢 SQL 日志、Web 监控页面、Active/Session 统计、Spring 监控、防 SQL 注入。很多公司选 Druid 不是因为它快而是因为团队需要可视化地看数据库压力。Druid 在阿里巴巴内部大量验证过稳定性没问题配置得当的话性能差距在某些场景下可以忽略。有一点要注意随着 Spring Boot 3 的推出Druid 官方适配 Boot 3 的 starter 通过druid-spring-boot-3-starter提供版本注意选新的。如果你的场景是“要性能、要简洁”选 HikariCP如果你的场景是“要监控、要防护”选 Druid。两种池子的参数概念相近迁移成本不高。2. 核心细节解析与实操要点2.1 连接池参数背后的原理配置数据源最难的不是配好 url 和账号密码而是理解池子里的那些参数。这里用 HikariCP 举例它比较克制的参数背后是严谨的设计。maximumPoolSize是池中最大连接数。这个值不是越大越好。连接数超过数据库并发处理能力以后多出来的连接只会堆积排队。通常经验值是core_count * 2 disk_count附近的水平结合应用的实际 QPS 来调整。假设你的应用是 4 核机器数据库本身性能也一般maxPoolSize 初始设个 10 到 15 足够了。minimumIdle是池中最小空闲连接数。HikariCP 作者建议如果池子不大直接让 minimumIdle 等于 maximumPoolSize。这样连接创建后常驻省去频繁创建的波动。对于小规模内部系统这确实能降低响应时间抖动。connectionTimeout是获取连接的超时时间默认 30 秒。如果是高并发系统这个时间建议调到 3 到 5 秒否则请求会长时间挂在获取连接上拖垮线程池。实际线上如果你看到大量Connection is not available, request timed out报错往往就是连接池被打满此时调 connectionTimeout 只能缓解症状根治要增加 maximumPoolSize 或优化 SQL。maxLifetime是连接最大存活时间。这个参数极其关键必须小于数据库 wait_timeout。MySQL 的 wait_timeout 默认 8 小时HikariCP 的 maxLifetime 默认 30 分钟。但很多二次开发的框架会把 maxLifetime 改得很长一旦连接被 MySQL 服务端主动断开而客户端还没感知就会出现连接复用时报错的情况。我实测里 maxLifetime 在 20 到 30 分钟之间就很稳妥。validationTimeout和connectionTestQuery是连接健康检查相关参数。HikariCP 框架里 JDBC4 的isValid()已经能完成检测无需额外配置 test query。Druid 则需要通过testWhileIdle和validationQuery配合常用的是SELECT 1。用 MySQL 的话注意顺便用SELECT 1能验证连接是否还能返回。2.2 驱动与 URL 的兼容性陷阱配数据源时最容易踩的坑就是驱动类和数据库版本的匹配问题。MySQL 8.x 以上推荐用com.mysql.cj.jdbc.Driver而 MySQL 5.x 用com.mysql.jdbc.Driver。问题在于老版本驱动类名在新版本包里还存在但已经被标记废弃某些新特性不再支持。如果你把 MySQL 8 的驱动包用到 5 的数据库上大概率能连上但会出现乱码、时区等问题。驱动类只是表象URL 参数才是真正的隐藏坑。serverTimezone不配就会出现时区异常useSSLfalse不配在某些版本会强制走 SSL 加密导致连不上或性能下降allowPublicKeyRetrievaltrue在 MySQL 8 的某些认证方式下必须配置否则报Public Key Retrieval is not allowed。我常用的 MySQL URL 长这样jdbc:mysql://localhost:3306/test?useUnicodetruecharacterEncodingutf8zeroDateTimeBehaviorconvertToNulluseSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltruerewriteBatchedStatementstrueuseCursorFetchtrue里面rewriteBatchedStatementstrue对批量插入性能的提升非常明显不加这个addBatch走的是一条条执行的老路加上这个MySQL 驱动会重写 SQL 为真正的多值插入。useCursorFetchtrue配合setFetchSize可以避免大查询一次性把结果集全载入内存适合报表类场景。注意useCursorFetch开启后游标模式对 MySQL 服务端内存有消耗不是所有查询都适合。2.3 Spring Boot 自动配置的装配原理Spring Boot 对数据源的自动配置核心在DataSourceAutoConfiguration它会在 classpath 下找到连接池实现类后按照一定优先级创建 DataSource。如果你同时引入了 HikariCP 和 Druid 的 starterBoot 会因为DataSource的自动配置条件不满足而跳过或者出现无法判断该用哪个连接池的诡异报错。这个优先级逻辑是固定的先看 classpath 是否有 HikariCP再看 Tomcat 连接池再看 DBCP2最后才是 Druid。官方文档说得很清楚HikariCP 在 classpath 中存在时默认使用它。很多项目一加 Druid starter 就把 HikariCP 的 jar 一起带着了这是一个很常见的配置误区。正确做法是如果要用 Druid就排除 HikariCP。Maven 里的排除方式dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-3-starter/artifactId version1.2.20/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId exclusions exclusion groupIdcom.zaxxer/groupId artifactIdHikariCP/artifactId /exclusion /exclusions /dependency如果你不想要 Boot 的自动装配也可以手动设置spring.datasource.typecom.alibaba.druid.pool.DruidDataSource这种方式适合那种需要完全掌控连接池对象的情况。不过一般情况下没必要排除依赖是最干净的方案。3. 实操过程与核心环节实现3.1 单数据源的标准配置流程直接给一份基于 Spring Boot 3 Druid 的完整配置过程这是目前企业级项目最常用的组合。第一步引入依赖。Spring Boot 3 需要引入druid-spring-boot-3-starter版本选 1.2.18 以上目前常用 1.2.20。如果坚持用老版druid-spring-boot-starter在 Spring Boot 3 下会因为 jakarta 命名空间问题直接启动失败。第二步配置文件。这里给了固化了丰富参数的一份示例spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/blog?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltruerewriteBatchedStatementstrue username: root password: 你的密码 druid: # 初始连接数 initial-size: 5 # 最小空闲连接数 min-idle: 5 # 最大活跃连接数 max-active: 20 # 获取连接的超时时间单位毫秒 max-wait: 30000 # 泄漏检测 remove-abandoned: true remove-abandoned-timeout: 180 # 打开慢 SQL 记录 log-slow-sql: true slow-sql-millis: 3000 # 从连接池获取连接时验证 test-on-borrow: false test-while-idle: true validation-query: SELECT 1 # 空闲连接检测间隔 time-between-eviction-runs-millis: 60000 # 连接最大存活时间 min-evictable-idle-time-millis: 300000 # 配置监控统计拦截的 filters filters: stat,wall,slf4j # WebStatFilter 配置用于监控 Web 应用 web-stat-filter: enabled: true url-pattern: /* exclusions: *.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/* # StatViewServlet 配置用于展示监控信息 stat-view-servlet: enabled: true url-pattern: /druid/* reset-enable: false login-username: admin login-password: admin123密码中包含特殊字符时记得用单引号或双引号包裹YAML 的解析规则决定了123456abc这类纯字符串还好1234abcd这种带或#的就容易出问题。第三步验证依赖引入。启动项目日志里会看到 Druid 连接池的初始化信息包括初始连接数、最大连接数等。访问http://localhost:8080/druid可以看到 Druid 的监控页面输入配置的用户名密码就能看到 SQL 执行统计、连接池使用情况、慢 SQL 清单。这一步能立刻看出连接池是否真正生效不要跳过。很多人以为配置写了就生效了其实 Boot 如果没正确排除 HikariCP你写的spring.datasource.druid.*配置就只会静默失效。3.2 多数据源的配置与动态切换原理多数据源是 DataSource 配置里最容易被问爆的一个场景。项目要同时连业务库和报表库或者做了读写分离主库写、从库读。Spring Boot 自身不直接支持多数据源需要自己实现动态切换逻辑。核心思路是利用AbstractRoutingDataSource它内部维护一个 targetDataSources 映射表。每次getConnection()时会调用determineCurrentLookupKey()方法根据返回的 key 选择对应的数据源。我们只需要继承这个类实现一个基于 ThreadLocal 的 key 切换机制即可。先看实现代码public class DynamicDataSource extends AbstractRoutingDataSource { private static final ThreadLocalString CONTEXT_HOLDER new ThreadLocal(); public static void setDataSource(String dataSourceKey) { CONTEXT_HOLDER.set(dataSourceKey); } public static void clear() { CONTEXT_HOLDER.remove(); } Override protected Object determineCurrentLookupKey() { return CONTEXT_HOLDER.get(); } }然后在配置类里把多个数据源注册进去Configuration public class DataSourceConfig { Bean ConfigurationProperties(spring.datasource.master) public DataSource masterDataSource() { return DruidDataSourceBuilder.create().build(); } Bean ConfigurationProperties(spring.datasource.slave) public DataSource slaveDataSource() { return DruidDataSourceBuilder.create().build(); } Bean public DynamicDataSource dynamicDataSource( Qualifier(masterDataSource) DataSource master, Qualifier(slaveDataSource) DataSource slave) { MapObject, Object targetDataSources new HashMap(); targetDataSources.put(master, master); targetDataSources.put(slave, slave); DynamicDataSource dynamicDataSource new DynamicDataSource(); dynamicDataSource.setTargetDataSources(targetDataSources); dynamicDataSource.setDefaultTargetDataSource(master); return dynamicDataSource; } }这样配置后业务代码里只需要在查询之前调用DynamicDataSource.setDataSource(slave)执行完再调用clear()。为了减少手动代码通常配合 AOP 切面使用定义一个DataSource(slave)注解切面在方法执行前设置 key方法执行后清理。这个方案的核心优势是扩展性强以后加第三数据源只需往 map 里塞一个 key。多数据源最坑的地方是事务。如果你在 service 方法上加TransactionalSpring 事务管理器在事务开始时就把数据源确定下来了方法是中间再切换数据源是无效的。更麻烦的是多个数据源参与同一个事务时默认是独立提交的一个失败一个成功需要引入 JTA 分布式事务或者 Seata 框架。我个人的建议是尽量保持一个事务只操作一个数据源跨库事务用最终一致性方案解决不要硬上分布式事务。3.3 基于注解的 AOP 动态切换完整示例下面给一个可以直接抄作业的注解切面实现。注解定义Target({ElementType.METHOD, ElementType.TYPE}) Retention(RetentionPolicy.RUNTIME) public interface DS { String value() default master; }切面实现Aspect Component public class DataSourceAspect { Before(annotation(ds)) public void before(JoinPoint point, DS ds) { DynamicDataSource.setDataSource(ds.value()); } After(annotation(ds)) public void after(JoinPoint point, DS ds) { DynamicDataSource.clear(); } }用的时候Service public class ReportService { DS(slave) public ListMapString, Object getReport() { // 查询逻辑 } }这套方案的关键点在于After里一定要执行clear()否则 ThreadLocal 里的值会残留。如果线程被线程池复用下次请求进来可能带着上个请求的数据源 key产生串数据源的严重事故。实测中出现过这样的问题某一个接口偶尔查询出来的数据是旧的排查到最后发现就是 ThreadLocal 没清理线程池里的线程把这个 key 带到了下一个请求。所以清理逻辑务必放到 finally 或者After通知里不要依赖业务代码自觉。3.4 读写分离场景的注意点很多同学读完写分离直接把写操作指向主库、读操作指向从库就算完事但实际落地有几个容易被忽视的细节。主从延迟问题是最常见的。MySQL 主从复制默认是异步的主库刚写入的数据不一定立刻出现在从库上。你刚提交订单马上查询订单详情走的从库结果查出来是空的。对这个场景处理方式大致有两种一种是强制路由关键查询强制走主库在注解里额外加一个DS(master)另一种是延迟容忍策略允许短暂的数据不一致。另外事务中的读操作必须走主库。如果一个事务里先写了再读读却走了从库可能读到未同步的旧数据破坏事务的一致性。通常事务和读写分离结合时建议在事务方法上直接指定主库。主从切换的监控也要考虑。当从库挂了如果代码里还是把读请求打到从库会出现大量连接异常。此时需要结合连接池的失败重试机制或者通过注册中心动态摘除从库节点。这些属于高可用架构范畴但在配置多数据源时就把方案预留出来后续改造成本会低很多。4. 常见问题与排查技巧实录4.1 连接池报错案例速查表数据源配置的错误种类在本地开发、测试环境、生产环境的表现完全不同下面这些是我实际遇到过并且反复验证有效的排查路径。现象可能原因排查方向ClassNotFoundException: com.mysql.jdbc.Driver驱动包版本过旧或未引入更换com.mysql.cj.jdbc.Driver确认 mysql-connector-j 依赖存在Public Key Retrieval is not allowedMySQL 8 的 caching_sha2_password 认证URL 加上allowPublicKeyRetrievaltrueCommunications link failure网络不通、防火墙、连接被服务端断开检查网络连通性核对 maxLifetime 与 wait_timeoutConnection is not available, request timed out连接池最大连接数不足或连接泄漏调大 max-active排查未关闭的 ResultSet/StatementTable doesnt exist库名配错或连到了错误实例核对 URL 中的 schema 名称Access denied for user账号密码、IP 白名单问题用命令行直接测试账号权限Connection refused数据库未启动或端口错误检查数据库进程与监听端口slow SQL log 中出现大量查询SQL 未走索引、表数据量大通过 Druid 看执行计划优化 SQL 或加索引druid 监控页打不开没开 stat-view-servlet 或拦截器未放行检查配置放行/druid/*路径表里这行Connection is not available是重灾区。它通常在连接池满的时候出现但连接池满的根源往往是连接泄漏。用 Druid 的话remove-abandoned打开并设置合适的超时时间能帮你兜底但它其实是双刃剑如果remove-abandoned-timeout太短正常的长事务可能被误杀。我建议生产环境先不开 remove-abandoned通过监控观察连接池水位确认泄漏后再针对性设置。4.2 数据库 wait_timeout 与连接池 maxLifetime 的配合数据库服务端连接空闲超时是导致连接异常最大的隐藏杀手。MySQL 默认wait_timeout是 8 小时但对长时间跑的应用来说连接达到 8 小时空闲会被服务端关闭。客户端连接池不知道这件事下次取连接时才发现连接已死于是报错。规避方式就是让连接池里的连接在到达服务端超时之前先主动销毁重建。Druid 里把minEvictableIdleTimeMillis设置小于数据库 wait_timeout 的一半比如 MySQL 8 小时Druid 里建议 3 到 5 小时。HikariCP 则通过maxLifetime控制官方默认 30 分钟一般够用。如果不想改连接池参数也可以修改 MySQL 端的 wait_timeout。但要注意服务端这个参数是全局变量改小会影响所有连接的客户端导致其他依赖长连接的程序出问题。所以倾向于调连接池而不是调数据库。这里分享一个小验证方法手动用命令行连上 MySQL执行show variables like wait_timeout拿到数据库的真实空闲超时再对照连接池配置里的 maxLifetime 或 minEvictableIdleTimeMillis确保连接池数值严格小于数据库数值。这个检查我每次排查连接问题都会做十次有八次能找到突破口。4.3 Druid 监控权限与安全配置Druid 的监控页面能直接看到所有 SQL 执行情况、表结构、甚至能执行 SQL所以必须做权限控制。默认不配账号密码的话所有人都能打开监控页这在生产环境属于严重安全事故。推荐配置我在上面的示例里已经给了就是stat-view-servlet片段下的 login-username 和 login-password。注意这里不是数据库账号是 Druid 监控页自己的登录账号。另外reset-enable必须设为 false否则页面上的 “Reset” 按钮能清空所有统计信息把历史监控数据全部抹掉影响问题溯源。Druid 的 wall filter 是 SQL 防火墙。生产环境建议保留这个 filter它能拦截掉很多明显非法的 SQL比如select * from user where id 1 or 11这种注入尝试。不过 wall filter 也会拦截部分合法但不规范的 SQL得做好白名单配置。我一般不直接用默认配置而是在wall里设置config参数允许特定场景的语句通过。4.4 常见多数据源配置错误多数据源场景下最容易踩的坑是循环依赖和 Bean 装配顺序问题。比如在DataSourceConfig里手动创建多个数据源再用Primary标记默认数据源但Primary如果没有加JPA 或 MyBatis 自动配置时就分不清该用哪个 DataSource直接启动报错No qualifying bean of type DataSource或expected single matching bean but found X。确保默认数据源加Primary这行基本解决大多数启动期故障Bean Primary public DataSource dynamicDataSource(...) { return dynamicDataSource; }另一个问题是配置前缀。多个数据源如果都写在spring.datasource.url下面就无法区分必须拆分到自定义前缀下。用ConfigurationProperties(spring.datasource.master)时YAML 里就必须是spring.datasource.master.url、spring.datasource.master.username这样的结构不再直接复用spring.datasource.url。我在实际项目中还见到过一种错误Spring Boot 自动配置类依然生效它不知道你搞了多数据源于是自动创建一个默认的数据源。然后你的 master 数据源又被标记成Primary两个数据源同时存在行为诡异。解决方法是显式排除DataSourceAutoConfigurationSpringBootApplication(exclude {DataSourceAutoConfiguration.class})排除之后项目里所有数据源都由你手动配置管理。这是多数据源项目推荐的做法。5. 数据源参数调优与线上稳定性经验5.1 初始连接数与最大连接数的设置逻辑连接池参数设置要考虑应用启动阶段和运行高峰两个维度。initialSize设为 5 到 10能让服务启动后短时间内就承受一定流量不用等并发请求来了再慢慢建连。建连接这个过程很昂贵涉及 TCP 三次握手、MySQL 认证、权限检查、创建会话一次完整耗时可能上百毫秒。如果初始连接数太少刚上线或重启完的瞬间遭遇流量高峰大量请求会排队等新连接建立表现为接口响应时间瞬间飙升。最大连接数的设定更讲究。它受两个因素制约应用本身的并发线程数和数据库能承受的连接上限。Tomcat 默认线程池最大 200 个如果 200 个线程同时去连接池拿连接连接池只给 20 个必然有 180 个线程在等待。所以理论上maxActive至少要等于应用最大并发线程数。但另一方面数据库不是无限连接的每个连接都消耗内存和 CPU连接过多反而产生上下文切换开销。经验取值思路是先压测跑一下单连接能支撑的 QPS再用预估的总 QPS 除以单连接 QPS 得到大致连接数。比如单连接能支撑 500 QPS系统预估 5000 QPS那连接数 10 左右够了。但这是理想情况还要考虑慢查询。慢查询会长时间占住连接实际生产里很多系统连接数远远超过理论值就是因为有慢 SQL 存在。所以调参前先看慢 SQL 日志把慢 SQL 解决掉连接数才降得下来。抛开慢 SQL 空谈连接池参数意义不大。5.2 连接池泄漏的检测与定位连接泄漏是最难缠的问题之一。它的表现是系统一开始正常运行几天后连接池被打满频繁报错重启后恢复。你直觉以为是配置问题其实是有代码拿了连接没关闭。JDBC 原生写法里常见泄漏点Connection conn null; try { conn dataSource.getConnection(); // 业务逻辑 } catch (Exception e) { // 处理异常 } finally { // 忘记关闭 conn // conn.close() 被遗漏 }用 MyBatis 或 JPA 时框架会帮你管理连接生命周期但如果你手动注入了SqlSessionTemplate或直接使用JdbcTemplate之外的底层 API就可能自己拿到连接不释放。Druid 的监控页面是定位泄漏最好的工具。打开监控页的 “连接池” 标签能看到当前活跃连接数、空闲连接数、逻辑连接打开数、物理连接打开数。如果活跃连接数长期居高不下且removeAbandoned是关闭状态基本可以确定有泄漏。此时再开启remove-abandoned: true配合removeAbandonedTimeout: 180日志里会输出泄漏发生时的调用栈。这条堆栈信息直接指向泄漏代码位置拿到它就能精准修复。有一个容易被忽略的情况长时间事务也会让连接看起来像泄漏。Transactional方法内部如果执行了外部服务调用比如 HTTP 请求第三方接口事务就一直不提交连接一直被占用。所以事务方法里尽量别做远程调用或耗时的 IO 操作否则一个慢接口就能吃掉几十上百个连接。5.3 慢 SQL 监控与优化联动连接池配置和数据源配置解决的是“连接管理”问题但线上数据库稳定性更多取决于 SQL 本身的执行效率。Druid 的log-slow-sql能把超过阈值的 SQL 打印到应用日志里。把这个阈值设为 1000 毫秒左右比较合理太短会轰炸日志太长会错过有价值的慢语句。拿到慢 SQL 之后常规的动作是EXPLAIN看执行计划检查是否走索引、是否出现全表扫描、是否产生临时表。实际上很多慢 SQL 的根子不在 SQL 写法而在数据量增长后索引失效或统计信息过期。比如一个订单表按order_no查询订单量过千万后命中索引也慢可能是因为order_no字段的字符集排序规则导致无法利用索引前缀。排查时不要只盯着索引有没有还要看字段类型和排序规则。我见过不少项目线上慢 SQL 一大半都是类型隐式转换引起的查询条件里的字符串类型赋给 int 列索引完全失效。配合 Druid 监控页的 SQL 列表按执行次数、耗时时长排序优先优化“累计耗时高”的语句而不是优化单次最快或最慢的。量变引发质变调优优先级是高频执行语句 耗时最长语句 偶发超慢语句。5.4 连接池参数快速调优参考表这里给一份基于我多年项目经验的起始参数参考注意这是起点不是终点最终取值要看压测结果和监控数据。参数建议值说明initialSize5~10启动时建立的连接数避免启动即高峰minIdle等于 maxActive 或略小空闲连接保底数量防止连接频繁重建maxActive根据并发和 QPS 压测保守起步 20后续按监控调整maxWait30000获取连接等待超时过高会导致线程堆积timeBetweenEvictionRunsMillis60000每 60 秒检测一次空闲连接和坏连接minEvictableIdleTimeMillis300000空闲 5 分钟以上才可能被回收validationQuerySELECT 1轻量级探活别用复杂 SQLtestWhileIdletrue空闲时检测每轮探测一次对性能影响可控testOnBorrowfalse获取连接时不额外检测性能优先testOnReturnfalse归还时不检测removeAbandoned视情况怀疑泄漏时打开配合超时时间使用logSlowSqltrue打开阈值 3000ms 起maxPoolPreparedStatementPerConnectionSize20PreparedStatement 缓存数量视场景调整这套参数适用于中低并发、以 MySQL 为主的业务系统。如果你做的是低延迟高并发服务或者用的是 PostgreSQL部分参数要重新评估。PostgreSQL 连接池的行为和 MySQL 差异不小比如空闲连接探测的频度、PreparedStatement 缓存的收益等都不完全一致。5.5 生产环境数据源配置的检查清单最后给一份上线前自查清单我每次发布前都会照着过一遍确认 URL 中是否配置了serverTimezone、useUnicode、characterEncoding时区和字符集乱码问题能不能在源头阻断。生产环境强制useSSLfalse或正确配置 SSL 证书避免性能损耗和潜在握手问题。核对数据库 wait_timeout 与连接池 maxLifetime / minEvictableIdleTimeMillis 的关系确保池内连接生命周期小于服务端空闲超时。开启慢 SQL 日志阈值先设为 1000 或 3000 毫秒保证能发现隐患。用 Druid 就配好监控页的独立账号密码reset-enable必须 false。多数据源场景确认Primary标注了默认数据源确认排除DataSourceAutoConfiguration。打开连接池的初始化日志确认项目启动时读取的配置和预期一致而不是被默认值覆盖。检查连接池在应用关闭时是否能正常销毁避免重启时端口和连接残留。测试环境模拟数据库异常确认应用断线重连能力和连接池自愈能力符合预期。这里面最后一条很多人忽略生产环境是禁止随意杀掉数据库测试的但在测试环境下杀一次数据库观察应用表现比任何时候排查问题都高效。数据源配置这件事实践久了你会发现核心不是会写那几个配置项而是理解连接从创建到复用再到销毁的完整生命周期。我个人的体会是先用 Spring Boot 自动配置把项目跑起来再逐步深入加参数、上监控、做多数据源每一步改动都用监控数据验证效果而不是一上来就堆一堆参数。参数堆得越多后期越难定位问题。先把基础配置做对再追求调优这条路走得最稳。
返回列表