ARTICLE DETAIL

资讯详情

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

Spring Boot集成Druid:配置、监控与排错实战

Spring Boot集成Druid:配置、监控与排错实战 最近帮一个朋友排查线上连接超时问题发现他们的Spring Boot项目虽然已经引入了Druid但监控页面是空的慢SQL一条都看不到连接池里的活跃连接却在疯狂增长。后来定位下来是用了druid-spring-boot-starter之后配置写错了层级参数全部落在默认值上。这个经历让我想写一篇完整的druid-spring-boot-starter使用笔记把从依赖引入、参数落地到监控开启、密码加密和排错的完整链路梳理清楚给准备在Spring Boot里用好Druid的读者一个能直接照着操作的参考。Druid在国内Java项目里的知名度没什么可怀疑的但很多人对它的用法还停留在手动new DruidDataSource、手动塞StatViewServlet的老路上。druid-spring-boot-starter出现之后集成方式简化了一大截但同时也带来了新的问题自动配置帮你做的事情太多有时候你根本不知道它到底做了什么、配置为什么没生效。本文就按这个思路展开涉及Spring Boot 2.x和3.x两个大版本的差异也会讲清楚。1. 换掉默认连接池的理由Druid不止是连接池1.1 连接池本身HikariCP和Druid的真实差距先大方承认一个事实论单次取连接的性能HikariCP在大多数场景下和Druid的差距微乎其微Spring Boot把它设为默认连接池是有道理的。HikariCP的优点就是快、稳、零配置但你拿它做生产环境时很快会遇到几个痛点数据库连接串、账号密码想加密HikariCP本身不提供现成方案。慢SQL没有统一的统计入口要在代码里手动埋点。SQL注入拦截、连接泄漏检测这些能力HikariCP一概没有。Druid在这些方面是补齐了一整圈外围能力的SQL监控、慢SQL统计、WallFilter防火墙、ConfigTools密码加密、连接泄漏检测、Web请求和SQL的关联统计。所以问题的关键不是哪个连接池快而是你的项目在连接池之外还需要什么。数据库连接池在JavaEE里发展了这么多年最后大家拼的都是外围治理能力Druid在这条路上走得最完整。1.2 为什么直接用druid-spring-boot-starter而不是手动配置很多老项目里看到过这种写法写一个Configuration类手动new DruidDataSource()再写一个ServletRegistrationBean把StatViewServlet注册进去还要再声明一个FilterRegistrationBean去挂WebStatFilter。这套代码本身不难但每个项目复制来复制去配置分散、版本容易漂移换个人维护就是灾难。用druid-spring-boot-starter之后这些组件全部交给Spring Boot的自动配置去装载DataSource、监控Servlet、WebStatFilter、StatFilter、WallFilter都是在classpath扫描时自动创建的。你要做的只是往application.yml里写配置。这就是starter的价值——不是连接池本身变了而是集成方式从自己组装零件变成了写参数让框架组装。提示这套自动配置是Spring Boot的AutoConfiguration机制不是你引入依赖后随便写写就行的理解后面第2节的自动配置类列表对你排查问题帮助很大。2. 依赖引入与starter的自动装配机制2.1 不同Spring Boot版本对应的依赖坐标写依赖之前先确认你的Spring Boot大版本。这里有个非常关键的区分Spring Boot 3.x使用的是jakarta.servlet命名空间和Spring Boot 2.x的javax.servlet不兼容所以Druid官方从1.2.18开始拆出了单独的druid-spring-boot-3-starter。Spring Boot 2.x项目用这个坐标dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.23/version /dependencySpring Boot 3.x项目必须用dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-3-starter/artifactId version1.2.23/version /dependency如果你在Spring Boot 3项目里误用了druid-spring-boot-starter最常见的报错就是ClassNotFoundException: javax.servlet.Servlet因为Spring Boot 3的容器里已经没有javax.servlet这条包路径了。与此同时建议在引入JDBC或JPA依赖时排除掉HikariCP。虽然druid-spring-boot-starter的自动配置在大多数情况下会先创建DruidDataSource但classpath里同时存在两套连接池实现时某些版本的Boot会自动回退创建HikariDataSource日志里会出现两套连接池的初始化信息。排除方式dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId exclusions exclusion groupIdcom.zaxxer/groupId artifactIdHikariCP/artifactId /exclusion /exclusions /dependency排除之后项目里只剩Druid一套连接池自动配置的路就走得很干净了。2.2 Starter启动时替你做了什么主要自动配置类一览druid-spring-boot-starter在Spring Boot 2.x中通过META-INF/spring.factories注册自动配置在Spring Boot 3.x中则通过META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports注册。这里面有几个核心类建议每个用Druid的开发者都眼熟它们自动配置类作用DruidDataSourceAutoConfigure创建DruidDataSource读取spring.datasource.druid前缀的配置DruidSpringAopConfiguration开启aop-patterns后对指定方法做调用监控DruidStatViewServletConfiguration注册Druid监控页面StatViewServletDruidWebStatFilterConfiguration注册Web请求统计过滤器WebStatFilterDruidFilterConfiguration组装stat、wall、slf4j等JDBC过滤器链这些类共同的工作方式就是Spring Boot经典的条件装配classpath里有对应类配置项允许才创建对应Bean。所以你的配置必须写到正确前缀下自动装配才会把你写的参数绑定进DruidDataSource。2.3 DataSource类型由谁决定type字段与自动配置优先级有一个常见的疑问既然引入了Druid starter为什么还要写spring.datasource.type其实在DruidDataSourceAutoConfigure里它会通过ConditionalOnMissingBean(DataSource.class)之类的条件判断接管DataSource创建同时spring.datasource.type可以显式指定最终创建的连接池类型。如果你的项目里已经手动声明了DataSourceBean自动配置则不会重复创建。所以我的建议是要么完全信任starter的自动配置要么完全自己声明DataSource不要两套混着来。混着来的时候你经常会在监控页面看到一个Druid实例、日志里却在打印另一个连接池的参数最后排查半天发现是条件装配的优先级在作怪。3. 生产级参数配置这些数字怎么给才合理3.1 连接池核心参数参考表下面是一份可以直接抄到application.yml里的核心配置我按MySQL场景写的spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/demo?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你的密码 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每个参数的含义和给值思路我整理成一张表参数建议值说明initial-size5启动时初始创建的连接数不应大于max-activemin-idle5连接池最小空闲连接数保证低峰期也有可用连接max-active20最大活跃连接数需要根据并发估算max-wait60000取连接的最大等待时间超时抛异常单位毫秒time-between-eviction-runs-millis60000空闲连接检测的执行间隔min-evictable-idle-time-millis300000连接空闲超过5分钟后可能被回收validation-querySELECT 1连接可用性检测SQLMySQL写SELECT 1即可test-while-idletrue空闲时检测连接推荐开启test-on-borrowfalse每次取连接都检测开销较大一般不推荐开test-on-returnfalse归还连接时检测极其耗时默认关掉就好pool-prepared-statementstrue开启预编译语句缓存max-pool-prepared-statement-per-connection-size20单个连接的预编译语句缓存上限max-active怎么评估一个简单估算方式假设你的服务在峰值时有200个并发请求每个请求占用数据库连接的平均时间是50毫秒那么同一时刻大约会有200 x 0.05 10个连接在活跃状态预留一些余量max-active给到20是合理的。这个数字不要拍脑袋最好压测后回填。3.2 连接有效性检测三个test开关的取舍初学者最容易把testOnBorrow、testWhileIdle、testOnReturn三个开关全部开成true觉得越是检测越安全。实际上testOnBorrowtrue会在每次getConnection()时执行一次validationQuery在连接池高频使用时会产生大量额外SQL查询。我见过一个项目开了testOnBorrow之后QPS只涨了10%数据库的SELECT 1就翻了一倍。比较稳妥的组合是testWhileIdletruetimeBetweenEvictionRunsMillis60000。这样每隔60秒Druid会扫描一遍空闲连接对空闲时间超过minEvictableIdleTimeMillis的连接做一次检测既保证了连接基本可用又不会对每次取连接造成额外开销。3.3 监控入口stat-view-servlet与web-stat-filter的配合Druid的监控能力要生效需要同时配好StatViewServlet和WebStatFilterspring: datasource: druid: stat-view-servlet: enabled: true url-pattern: /druid/* login-username: admin login-password: admin123 reset-enable: false web-stat-filter: enabled: true url-pattern: /* exclusions: *.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*这里要特别注意一个常见误解stat-view-servlet负责提供/druid/index.html监控页面web-stat-filter负责拦截Web请求进行URI维度的统计两者是配合关系不是替代关系。只配前者你能看到连接池和SQL列表但看不到Web请求维度的统计只配后者你连页面都访问不了。提示监控页面登录账号密码直接写在配置里虽然方便但生产环境建议用环境变量覆盖避免密码出现在代码仓库中。reset-enable默认是false如果想在页面手动重置统计就显式改成true。4. 慢SQL统计、SQL防火墙与日志审计4.1 StatFilter让每条SQL都留下痕迹在filters: stat,wall,slf4j这一行里stat对应的是StatFilter它负责收集SQL执行次数、耗时、并发等数据。两个非常有用的附加参数是通过connection-properties传进去的druid.stat.mergeSqltrue druid.stat.slowSqlMillis5000mergeSqltrue把结构相同、参数不同的SQL合并成一条统计比如SELECT * FROM user WHERE id ?无论传入多少不同id都归为一条。这样监控页面里不会被几百条同结构的SQL刷屏。slowSqlMillis5000执行时间超过5秒的SQL会被标记为慢SQL配合filter.stat.log-slow-sqltrue可以直接输出到日志。注意这两个属性走的是connection-properties不是普通的spring.datasource.druid下配置。踩过坑的人都知道写错位置之后慢SQL一直统计不出来。4.2 WallFilter把危险的SQL挡在数据库之前WallFilter是Druid里很独特的一块能力它本质上是一个SQL防火墙。开启方式就是在filters里加上wall也可以精细化配置spring: datasource: druid: filter: wall: enabled: true config: multi-statement-allow: true comment-allow: falsemulti-statement-allow控制是否允许一次执行多条SQL默认是false。很多项目因为用了allowMultiQueriestrue的JDBC连接串发现多条SQL执行被WallFilter拦截因此在config里放开它。这里建议想清楚再开允许multi-statement相当于把SQL拼接的大风险敞口打开了如果业务确实需要请务必配合参数化查询使用。WallFilter对常见SQL注入手法的拦截很有意思它不靠正则黑名单而是直接解析SQL语法树。我做过一次测试把经典的 or 11 --拼到条件里MySQL裸库能查出数据Druid的WallFilter直接拒绝执行并抛异常。这种语法树级别的防护比应用层简单拼接检查要可靠得多。4.3 日志输出slf4j与logback的配合filters: stat,wall,slf4j中的slf4j会把SQL执行日志交给SLF4J处理。启动后你会看到类似这样的日志INFO com.alibaba.druid.filter.logging.Slf4jLogFilter - {conn-10001, stmt-20001} executed. 5.0 milliseconds. SELECT * FROM user WHERE id 1如果觉得SQL日志太吵可以单独控制该Logger的级别logging: level: com.alibaba.druid.filter.logging.Slf4jLogFilter: WARN需要保留审计类日志的场景比如金融项目可以把这一项调到DEBUG或INFO它记录的是真实的SQL执行明细对审计和排障很有价值。5. 配置了不生效一套排查链路帮你定位这一部分我把自己实际遇到过的坑总结成了排查链路按顺序做基本能定位问题。5.1 从启动日志和JVM连接数反向判断Druid启动时会在日志里留下一行关键信息com.alibaba.druid.pool.DruidDataSource - {dataSource-1} inited看到这行日志说明DruidDataSource已经被Spring容器创建并初始化完成了。但请注意inited不代表一切正常它只代表连接池对象建立完毕后面真正建立物理连接时如果有异常会继续打印连接失败的堆栈。如果你想进一步确认连接池中的连接数是否按配置增长可以借助JVisualVM或Arthas查看DruidDataSource的activeCount和poolingCount字段。这两个字段分别表示正在使用的连接数和连接池内空闲连接数拿它们和max-active对照就能判断参数是否真正生效。5.2 前缀和排除项的常见错误我在朋友项目里发现的第一类问题就是配置写错前缀。druid-spring-boot-starter读取的是spring.datasource.druid前缀而不是spring.datasource.hikari也不是druid。下面这几种写法都是错的# 错误1漏掉spring.datasource前缀 druid: max-active: 20 # 错误2沿用默认连接池前缀 spring: datasource: hikari: maximum-pool-size: 20当你把HikariCP的maximum-pool-size还留在配置里时虽然排查时看到spring.datasource下是有配置的但Druid一条也不会读。检查要点永远是max-active、initial-size这些字段必须挂在spring.datasource.druid下面。另一个高发原因是classpath里同时存在HikariCP和Druid。此时Spring Boot默认的DataSourceAutoConfiguration可能优先创建了HikariDataSource你用jconsole看到的连接池不是你想的那一个。处理方式就是第2节说的把HikariCP从spring-boot-starter-jdbc里排除掉。5.3 监控页面缺数据的排查清单监控页面开了但SQL列表没有数据这个问题排行榜上见得太多了。我整理了一张排查清单现象排查方向页面404stat-view-servlet.enabled没开或url-pattern写错能进页面但无数据源信息mybatis或jpa可能绕过了starter创建的DataSourceSQL列表为空web-stat-filter.enabled未设置或exclusions误杀导致请求没被过滤连接池参数不对检查配置前缀用Arthas查看DruidDataSource实际字段值报statement is not allowedWallFilter拦截需检查multi-statement-allow等配置5.4 一次真实的生产事故复盘那次朋友的生产事故表面症状是某个服务实例的连接数持续打满应用到数据库的连接按小时增长直到触顶。我上去先是看了启动日志确认确实走的是Druid但仔细看配置后发现spring.datasource.type没写HikariCP也没排除Spring Boot自动配置最终创建的是HikariDataSource服务里真正跑着的是Hikari而Druid只是被加载但没被使用。把type显式指向Druid并排除Hikari之后重启实例连接数回落正常。这个案例的教训是自动配置越方便越要确认最终生效的组件到底是什么。如果没有监控页面或Arthas做验证你可能永远不会发现项目里真正干活的是另一个连接池。6. 数据库密码加密ConfigTools使用与常见坑6.1 生成密钥对与密文Druid的ConfigTools提供了一套非对称加密方案专门解决数据库密码在配置文件里明文存放的问题。使用方式很简单在命令行执行java -cp druid-1.2.23.jar com.alibaba.druid.filter.config.ConfigTools 你的数据库密码执行后输出三样东西privateKey: MIIEvQIBADANBgkqhkiG9w0BAQEFAASC... publicKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE... password: m4gZ6Gm6hYd5QyN7yS9F4zW3lRJkzDmQ...私钥是给ConfigTools自己解密用的公钥是配置到应用里的。实际部署时私钥不要出现在项目里保留在运维手里用于定期更换密码。6.2 yaml配置与filter.config的正确写法拿到公钥和密文后按下面方式改配置spring: datasource: druid: username: root password: m4gZ6Gm6hYd5QyN7yS9F4zW3lRJkzDmQ connection-properties: config.decrypttrue;config.decrypt.keyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE... filter: config: enabled: true这里有两种开启config解密的方式一种是直接把config写进filtersfilters: stat,wall,slf4j,config另一种就是用上面的filter.config.enabledtrue。我个人推荐第二种可读性更好而且不会因为filters里写错顺序导致filter链初始化失败。两种方式不要混用混用偶尔会触发重复解密或属性覆盖的问题。6.3 解密失败的三个典型原因踩过ConfigTools的坑典型原因集中在下面三个第一公钥填错成私钥。复制过程中把privateKey当成publicKey填进config.decrypt.key启动时直接报RSAException: decrypt error。文本很像务必核对标签。第二filter.config.enabled没开或没有把config加进filters。这种情况下应用启动不会报错但密码是加密串最终连接数据库时认证失败。很多人在SQL日志里找半天最后发现是解密逻辑压根没被激活。第三密文里含有特殊字符在YAML中被转义。比如、/偶尔会被某些YAML解析器转成其他含义一个稳妥做法是用单引号把整段密文包起来password: m4gZ6Gm6hYd5QyN7yS9F4zW3lRJkzDmQ改完之后重启应用看到DruidDataSource正常创建连接再配合监控页面里的数据源状态基本可以确认解密流程跑通了。7. 上线后的监控页利用与两个容易被忽视的坑7.1 监控页面怎么用好从SQL列表到Session跟踪Druid监控页面的地址通常是http://localhost:8080/druid/index.html端口和上下文根按你的项目调整。登录后第一眼看到的是数据源Tab那里有activeCount、poolingCount、maxActive这些实时指标。生产环境遇到连接数异常波动时我基本都先盯着这个页面通常几分钟就能看出是某个接口把连接借走没还还是整体并发真的上来了。SQL Tab更实用。开了mergeSqltrue之后SQL列表会把同结构SQL聚合展示每一条都有执行次数、总耗时、平均耗时、最大耗时。慢SQL直接按Max列降序排序一下就能找出拖垮数据库的罪魁祸首。再配合druid.stat.slowSqlMillis5000超过5秒的SQL会被单独圈出来这个功能在性能优化阶段帮助极大。7.2 连接池泄漏检测removeAbandoned的正确打开方式HikariCP没有的经典功能之一就是连接泄漏检测。Druid里相关参数有三个spring: datasource: druid: remove-abandoned: true remove-abandoned-timeout: 1800 log-abandoned: true意思是连接被借出后如果1800秒还没归还连接池会强制回收并打印一条日志记录该连接当时的调用栈。这个功能很有用但我的实际经验是不要在刚发现问题时就把remove-abandoned直接开成true。因为强制回收的本质是宁可牺牲一个正在执行的长事务也不能让连接池被耗尽如果业务里有合法的长事务会被这个参数误杀。推荐的顺序是先开log-abandonedtrue观察一段时间通过日志确认泄漏的代码位置修掉根本原因。如果短期无法根治且线上连接即将耗尽再临时开remove-abandoned兜底。日志里如果出现get/close call inconsistent那几乎可以断定是连接泄漏点被标记出来了顺着日志里的堆栈找对应方法即可。7.3 Spring Boot 3项目的新坑Spring Boot 3除了要换druid-spring-boot-3-starter还有几个细节值得注意。首先是包名问题旧版本的Druid在Web环境里对javax.servlet的直接依赖在Spring Boot 3下会变成jakarta.servlet所以如果你在代码里直接引用了DruidFilterConfiguration等类需要升级到对应支持Jakarta的版本。其次是Spring Boot 3内置Tomcat的默认行为变化较大监控页面如果访问不到先别急着怀疑Druid用curl http://localhost:8080/druid/index.html看看HTTP状态再确认stat-view-servlet是否被某些权限配置拦截。最后提一个容易被忽略的点Spring Boot 3的自动配置加载方式改成了AutoConfiguration.imports如果你在旧教程里看到修改spring.factories的操作步骤那套方案已经过时了。排查自动配置是否生效时直接看META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件里有没有DruidDataSourceAutoConfigure即可。我在实际运维中还有一个习惯就是定期去Druid的监控页面看一眼SQL执行Top列表。很多时候你以为数据库层没问题的系统慢SQL只是被应用层日志淹没了而连接池层面的统计才能真正客观地反映数据库的负载情况。第一次能清楚看到每条SQL的耗时分布时你会觉得之前几年排查数据库性能的方式都太原始了。如果你正准备把项目里的连接池换成Druid我建议按照本文的顺序一步步来先引入starter并确认启动日志里的inited标记再配好stat-view-servlet和web-stat-filter把监控页面跑起来之后再去折腾max-active、min-idle这些参数。监控页面有了后面每一次参数调整你都能看到实际效果而不是靠猜。等你把密码加密、WallFilter、慢SQL日志这些都配上之后数据库这一层的可观测性就基本到位了。
返回列表