十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Spring Boot 3多数据源配置与Druid监控实战

Spring Boot 3多数据源配置与Druid监控实战 最近群里好多人在问 Spring Boot 3 多数据源配置和 Druid 监控怎么搞折腾了大半天发现网上很多教程还停留在 Boot 2 时代要么依赖不兼容要么配置完监控页面打不开。这次我直接基于 Spring Boot 3 MyBatis Plus Druid 把整套方案重新跑了一遍从依赖版本、多数据源配置类、Druid 监控页面到踩坑记录一次性整理清楚。适合正在做项目需要接多个数据库、读写分离或者想把 Druid 监控页用起来的朋友参考。1. 项目整体设计思路与选型1.1 核心需求拆解多数据源的场景其实很多常见的就是读写分离、业务库隔离、历史数据归档。我这次的项目是两个业务库一个主库负责订单数据另一个日志库负责操作流水。两者之间的数据没有强耦合也不需要跨库 join所以核心诉求就是“代码里能同时操作两个库并且每个数据源都能被 Druid 监控到”。多数据源如果做不好后面维护成本会很高。最典型的坑包括Mapper 扫描到错误的数据源、事务管理器不生效、连接池配置互相覆盖、监控数据混在一起。所以第一步不是写代码而是把方案选型定清楚。我不推荐一上来就复制网上的 AOP 动态切换代码因为大多数场景根本用不到。只要数据源之间的边界是固定的直接为每个数据源单独创建 SqlSessionFactory 和 MapperScan 包反而最直观。后面如果真要动态切换再在 Service 层做路由也不迟。1.2 两种主流实现路线对比多数据源实现路线大体分两类一类是使用 Spring 的 AbstractRoutingDataSource配合切面在方法调用时动态切换另一类是针对不同包、不同 Mapper 固定绑定到不同数据源。第一类适合同一个 Service 里需要频繁切换数据源的场景比如客户维度分库。第二类适合业务边界清晰、库和库之间基本隔离的场景。我的选择是第二类因为它不需要自定义注解、切面、ThreadLocal也没有事务切换失效的隐患。当然如果项目很复杂也可以直接用 baomidou 的 dynamic-datasource-spring-boot3-starter它内部封装了切换逻辑用 DS 注解就能搞定。但在我这个场景里两个库的 Mapper 本来就在不同包下独立绑定反而更干净也更容易排查问题。1.3 项目结构与依赖规划我采用 Maven 多 module 折叠在一个 Spring Boot 应用里包结构按数据源边界拆分controller对外接口service业务逻辑mapper/primary主库 Mappermapper/logs日志库 Mapperentity实体类config数据源和 Druid 监控配置两个数据源分别对应独立的配置类一个标记为 Primary 作为默认数据源另一个作为辅助数据源。MapperScan 分别扫描不同包并指定对应的 SqlSessionTemplateRef这样每个 Mapper 拿到的连接池是确定的不会串库。依赖上我固定使用 Spring Boot 3.2.xMyBatis Plus 用支持 Boot 3 的 mybatis-plus-spring-boot3-starterDruid 用 1.2.20 以上版本。注意 Boot 3 已经切换到 jakarta 命名空间Druid 版本太老会直接启动报错这是第一个要避开的坑。2. 多数据源配置核心步骤2.1 依赖引入Spring Boot 3 的注意事项pom.xml 里的核心依赖如下dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.5/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-3-starter/artifactId version1.2.20/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-aop/artifactId /dependencyMyBatis Plus 从 3.5.3 开始提供专门的 Boot 3 starter包名是 mybatis-plus-spring-boot3-starter不要再写老版本的 mybatis-plus-boot-starter。Druid 同样要写 druid-spring-boot-3-starter它内部已经处理了 jakarta servlet 的兼容问题。如果你只引入旧的 druid-spring-boot-starter启动时大概率会出现 NoClassDefFoundError问题根源就是 javax.servlet 和 jakarta.servlet 不一致。我这边还引入了 spring-boot-starter-aop是为了给 Druid 做 Spring 监控。如果不需要这个功能可以去掉但事务、配置类本身不需要额外依赖。2.2 application.yml 配置前缀规划多数据源的配置最忌讳“把所有参数堆在 spring.datasource 下”。我用了两个独立前缀主库和日志库完全分开这样 Druid 的初始化参数可以各自调整不会互相影响。server: port: 8080 spring: datasource: druid: primary: url: jdbc:mysql://localhost:3306/order_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver initial-size: 5 min-idle: 5 max-active: 20 max-wait: 60000 filters: stat,wall,slf4j connection-properties: druid.stat.slowSqlMillis2000 logs: url: jdbc:mysql://localhost:3306/log_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver initial-size: 3 min-idle: 3 max-active: 10 max-wait: 30000 filters: stat,wall,slf4j connection-properties: druid.stat.slowSqlMillis2000注意两个点。第一filters 里写上 stat 和 wall一个负责 SQL 统计一个负责防火墙slf4j 可选它会输出 SQL 日志方便排查问题。第二connection-properties 中的 druid.stat.slowSqlMillis2000 表示超过 2 秒的 SQL 会被标记为慢 SQL这个在监控页面里能看到。这两个数据源的 spring.datasource.druid.primary 和 spring.datasource.druid.logs 前缀分别对应后面配置类里的 ConfigurationProperties不用再写一堆 setter 代码。2.3 配置类主从数据源、SqlSessionFactory、事务管理器核心配置类如下主库和日志库各写一份。先看主库配置package com.example.dm.config; import com.alibaba.druid.pool.DruidDataSource; import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionTemplate; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import javax.sql.DataSource; Configuration MapperScan( basePackages com.example.dm.mapper.primary, sqlSessionTemplateRef primarySqlSessionTemplate ) public class PrimaryDataSourceConfig { Primary Bean(name primaryDataSource) ConfigurationProperties(prefix spring.datasource.druid.primary) public DataSource primaryDataSource() { return new DruidDataSource(); } Primary Bean(name primarySqlSessionFactory) public SqlSessionFactory primarySqlSessionFactory( Qualifier(primaryDataSource) DataSource dataSource) throws Exception { MybatisSqlSessionFactoryBean factory new MybatisSqlSessionFactoryBean(); factory.setDataSource(dataSource); factory.setTypeAliasesPackage(com.example.dm.entity); factory.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources(classpath*:mapper/primary/*.xml)); return factory.getObject(); } Primary Bean(name primarySqlSessionTemplate) public SqlSessionTemplate primarySqlSessionTemplate( Qualifier(primarySqlSessionFactory) SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); } Primary Bean(name primaryTransactionManager) public DataSourceTransactionManager primaryTransactionManager( Qualifier(primaryDataSource) DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } }日志库配置类基本一样只是包名、前缀、Bean 名不同注意不要加 PrimaryConfiguration MapperScan( basePackages com.example.dm.mapper.logs, sqlSessionTemplateRef logsSqlSessionTemplate ) public class LogsDataSourceConfig { Bean(name logsDataSource) ConfigurationProperties(prefix spring.datasource.druid.logs) public DataSource logsDataSource() { return new DruidDataSource(); } Bean(name logsSqlSessionFactory) public SqlSessionFactory logsSqlSessionFactory( Qualifier(logsDataSource) DataSource dataSource) throws Exception { MybatisSqlSessionFactoryBean factory new MybatisSqlSessionFactoryBean(); factory.setDataSource(dataSource); factory.setTypeAliasesPackage(com.example.dm.entity); factory.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources(classpath*:mapper/logs/*.xml)); return factory.getObject(); } Bean(name logsSqlSessionTemplate) public SqlSessionTemplate logsSqlSessionTemplate( Qualifier(logsSqlSessionFactory) SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); } Bean(name logsTransactionManager) public DataSourceTransactionManager logsTransactionManager( Qualifier(logsDataSource) DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } }这段配置里有几个细节需要说明。DataSource 的 Bean 直接 new DruidDataSource()然后交给 Spring Boot 的 ConfigurationProperties 去绑定参数不需要手动调用 setUrl、setUsername。SqlSessionFactory 用的是 MyBatis Plus 的 MybatisSqlSessionFactoryBean不是原生 MyBatis 的 SqlSessionFactoryBean否则 BaseMapper 的内置方法不会生效。如果有 XML 文件需要放到对应 mapper 包下的 resources 目录并在 setMapperLocations 里配置路径。2.4 多数据源事务和 MyBatis Plus 注意事项多数据源的事务记住一句话一个 SqlSessionFactory 配一个事务管理器事务默认只在当前数据源内生效。比如在 Service 里同时修改主库订单表和日志库日志表不能指望一个 Transactional 把两边都包进去。如果要把某个方法绑定到日志库事务需要这样写Transactional(transactionManager logsTransactionManager) public void writeLog(OperationLog log) { logMapper.insert(log); }不指定 transactionManager 时Spring 会使用 Primary 那个事务管理器也就是主库。如果你连主库的事务管理器都没标记 Primary启动时会出现 NoUniqueBeanDefinitionException。还有一个坑是 MyBatis Plus 的全局配置。手动构建 SqlSessionFactory 后application.yml 里的 mybatis-plus.global-config 和 mybatis-plus.configuration 不会自动注入到所有 factory。如果要用逻辑删除、自动填充需要在每个 MybatisSqlSessionFactoryBean 上单独配置 GlobalConfig或者把公共配置抽成一个方法。我这边项目暂时没有这些复杂需求所以没有额外写但这一点你必须心里有数。3. Druid 监控接入3.1 监控组件怎么组合Druid 监控不是单指一个页面它至少由四部分组成StatFilter 负责统计 SQL 执行时间、执行次数StatViewServlet 提供 Web 监控页面WebStatFilter 负责拦截 Web 请求记录 URL 和 Session 维度信息Spring 监控通过 AOP 拦截 Service 方法的调用。StatFilter 已经在每个数据源的 filters 里配好了这一步很多人会漏掉。没有 StatFilter 的话监控页面能打开但 SQL 列表永远是空的。StatViewServlet 和 WebStatFilter 我选择用代码注册比在 yml 里依赖 starter 的开关更直观也不容易混淆两个数据源。3.2 servlet/filter 注册代码新建 DruidMonitorConfigpackage com.example.dm.config; import com.alibaba.druid.support.http.StatViewServlet; import com.alibaba.druid.support.http.WebStatFilter; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class DruidMonitorConfig { Bean public ServletRegistrationBeanStatViewServlet druidStatViewServlet() { ServletRegistrationBeanStatViewServlet registration new ServletRegistrationBean(new StatViewServlet(), /druid/*); registration.addInitParameter(loginUsername, monitor); registration.addInitParameter(loginPassword, f8x2Kp#9Lw); registration.addInitParameter(allow, 127.0.0.1); registration.addInitParameter(resetEnable, false); return registration; } Bean public FilterRegistrationBeanWebStatFilter druidWebStatFilter() { FilterRegistrationBeanWebStatFilter registration new FilterRegistrationBean(new WebStatFilter()); registration.addUrlPatterns(/*); registration.addInitParameter(exclusions, *.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*); registration.addInitParameter(profileEnable, true); return registration; } }这段代码里有几个参数很关键。loginUsername 和 loginPassword 决定了访问 /druid/index.html 时输入的账号密码千万别再用 admin/admin 这种默认组合。allow 控制白名单我这里只允许本机访问如果你要远程看把允许访问的 IP 加进去。resetEnable 设为 false防止别人在页面上点“重置”把统计数据清掉。WebStatFilter 的 exclusions 要排除静态资源和 /druid 本身否则监控页面的请求也会被统计看起来会很乱。profileEnable 设为 true 后URI 监控里可以看到每个请求的耗时分布。Spring 监控如果想看到 Service 方法级别的调用统计可以再用 Druid 的 DruidStatInterceptor 配合一个方法切点Bean public DruidStatInterceptor druidStatInterceptor() { return new DruidStatInterceptor(); } Bean public JdkRegexpMethodPointcut druidStatPointcut() { JdkRegexpMethodPointcut pointcut new JdkRegexpMethodPointcut(); pointcut.setPattern(com.example.dm.service.*); return pointcut; } Bean public Advisor druidStatAdvisor() { return new DefaultPointcutAdvisor(druidStatPointcut(), druidStatInterceptor()); }需要 import com.alibaba.druid.support.spring.stat.DruidStatInterceptor 和 springframework.aop.support.JdkRegexpMethodPointcut。配好之后Druid 页面的 Spring 监控 tab 里就会出现 Service 方法的执行统计。3.3 监控页面访问与指标解读启动项目后浏览器访问 http://localhost:8080/druid/index.html输入刚才配置的账号密码就能进。页面左侧有几个 tab我重点看这几个数据源展示每个 Druid 连接池的活跃连接数、空闲连接数、最大连接数。如果某个库的连接池一直涨这里能快速看出来。SQL 监控按 SQL 语句聚合执行次数、返回行数、执行时间。点开单条 SQL 可以看到最近一次执行的耗时和慢 SQL 标记。SQL 防火墙显示拦截掉的 SQL 语句和攻击次数。如果 wall filter 生效遇到明显带注入特征的 SQL 会直接在这里记录并拦截。URI 监控按接口路径统计请求次数、并发数、耗时。Spring 监控显示被 AOP 拦截的 Service 方法调用情况。日常排查慢接口的时候我一般先看 URI 监控找到响应时间长的接口然后再去 SQL 监控里看这个接口对应的 SQL 是不是存在慢查询效率很高。3.4 监控参数优化与慢 SQL 设置慢 SQL 的阈值可以在 connection-properties 里控制我配置的是 2000 毫秒。你还可以在 DruidDataSource 上设置 timeBetweenLogStatsMillis 来周期性输出统计日志到应用日志里这样即使不进页面也能看到连接池的运行情况。因为这里两个数据源是独立配置的如果你想调整主库的监控参数就只改 primary 下的 connection-properties想调整日志库就只改 logs 下的。如果两个库都配了 filters监控数据会同时出现在同一个 Druid 页面里数据源 tab 会分别显示两个连接池不会混在一起。4. 从建表到验证一次跑通4.1 准备两个库和测试表我这边在本地 MySQL 建了两个库order_db 和 log_db。主库建一张订单表CREATE DATABASE IF NOT EXISTS order_db DEFAULT CHARACTER SET utf8mb4; USE order_db; CREATE TABLE user_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, user_id BIGINT NOT NULL, amount DECIMAL(10,2) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );日志库建一张操作日志表CREATE DATABASE IF NOT EXISTS log_db DEFAULT CHARACTER SET utf8mb4; USE log_db; CREATE TABLE operation_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, biz_type VARCHAR(32) NOT NULL, content VARCHAR(500), create_time DATETIME DEFAULT CURRENT_TIMESTAMP );建完表后在两个库里各插入几条测试数据方便后面通过 selectCount 验证数据源是否切换正确。4.2 编写Mapper与Service实体类我按两表字段定义分别对应 Order 和 OperationLog。这里就不把完整实体代码贴出来了重点是 Mapper 接口要放在不同的包下。主库 Mapperpackage com.example.dm.mapper.primary; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.dm.entity.Order; public interface OrderMapper extends BaseMapperOrder { }日志库 Mapperpackage com.example.dm.mapper.logs; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.dm.entity.OperationLog; public interface OperationLogMapper extends BaseMapperOperationLog { }Service 里同时注入两个 Mapperpackage com.example.dm.service; import com.example.dm.mapper.primary.OrderMapper; import com.example.dm.mapper.logs.OperationLogMapper; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.Map; Service public class QueryService { private final OrderMapper orderMapper; private final OperationLogMapper operationLogMapper; public QueryService(OrderMapper orderMapper, OperationLogMapper operationLogMapper) { this.orderMapper orderMapper; this.operationLogMapper operationLogMapper; } public MapString, Long countAll() { MapString, Long result new HashMap(); result.put(orderCount, orderMapper.selectCount(null)); result.put(logCount, operationLogMapper.selectCount(null)); return result; } }这段代码能正常工作本质上是因为 OrderMapper 由 primarySqlSessionTemplate 创建OperationLogMapper 由 logsSqlSessionTemplate 创建两者底层连接的是不同数据库Mapper 本身不需要额外注解。4.3 启动项目并确认数据源与监控启动 Spring Boot 应用观察日志。如果配置正确Druid 会打印两个数据源的初始化日志MyBatis Plus 也会分别构建两个 SqlSessionFactory。如果以前只配置过单数据源第一次看到两个 “druid dataSource” 的字样不要慌那正常。然后写一个简单接口验证RestController public class DemoController { private final QueryService queryService; public DemoController(QueryService queryService) { this.queryService queryService; } GetMapping(/demo/count) public MapString, Long count() { return queryService.countAll(); } }启动后访问 /demo/count返回结果应该是类似{orderCount:12,logCount:5}这时候打开 Druid 监控页切到 SQL 监控能看到两条 select count 语句分别来自两个数据源。数据源 tab 里也能看到两个连接池。到这里多数据源和监控都通了。4.4 动态切换数据源的补充思路如果你的项目里同一个包下的 Mapper 需要根据业务路由到不同数据源静态绑定就不合适了。可以改用 dynamic-datasource-spring-boot3-starter配置上会简化很多spring: datasource: dynamic: primary: primary strict: true datasource: primary: url: jdbc:mysql://localhost:3306/order_db username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver logs: url: jdbc:mysql://localhost:3306/log_db username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver然后在目标 Mapper 或 Service 方法上使用 DS(logs) 即可切换。这个方案对日常分库场景更友好但如果你需要精细控制多个 SqlSessionFactory、每个库独立事务静态绑定的方式仍然是最可控的。5.
返回列表