1. 为什么需要多数据源与Druid监控
在企业级应用开发中,数据源管理是个绕不开的话题。最近在重构一个老项目时,我遇到了这样的场景:主业务库需要处理高频交易,报表系统要连接分析型数据库,还有第三方合作方的数据需要单独隔离。这种多数据源需求在微服务架构中非常普遍,而Spring Boot 3.x对多数据源的支持比早期版本更加成熟。
Druid作为阿里巴巴开源的数据库连接池,其监控能力是区别于HikariCP等竞品的核心优势。通过内置的监控页面,我们可以实时看到:
- 活跃连接数峰值波动
- SQL执行耗时分布
- 慢查询统计
- 连接泄露检测
这些指标对于线上问题排查和性能调优至关重要。上周我们生产环境就出现过一次连接泄漏,正是Druid的监控页面帮我们快速定位到未关闭的ResultSet。
2. 基础环境搭建与依赖配置
2.1 初始化Spring Boot 3项目
使用start.spring.io生成项目时,需要特别注意:
- Java版本选择17+(Spring Boot 3最低要求)
- 添加Spring Web、MyBatis/Liquibase等数据访问层依赖
- 排除默认的HikariCP(Spring Boot 3默认连接池)
xml复制<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.18</version>
</dependency>
2.2 多数据源配置原则
不同于单数据源的spring.datasource配置,多数据源需要:
- 为每个数据源创建独立的配置类
- 使用
@Primary标注主数据源 - 明确指定Mapper接口的绑定关系
典型的生产配置示例:
yaml复制# 主库配置
spring.datasource.master.url: jdbc:mysql://master-db:3306/core?useSSL=false
spring.datasource.master.username: admin
spring.datasource.master.password: encrypted_password
spring.datasource.master.driver-class-name: com.mysql.cj.jdbc.Driver
# 从库配置
spring.datasource.slave.url: jdbc:mysql://slave-db:3306/report?useSSL=false
spring.datasource.slave.username: reporter
spring.datasource.slave.password: encrypted_password
spring.datasource.slave.driver-class-name: com.mysql.cj.jdbc.Driver
3. Druid监控页面深度配置
3.1 安全访问控制
默认的/druid路径存在安全隐患,必须配置:
java复制@Configuration
public class DruidConfig {
@Bean
public ServletRegistrationBean<StatViewServlet> druidServlet() {
ServletRegistrationBean<StatViewServlet> reg = new ServletRegistrationBean<>();
reg.setServlet(new StatViewServlet());
reg.addUrlMappings("/druid/*");
// 白名单IP(生产环境务必配置)
reg.addInitParameter("allow", "192.168.1.100,127.0.0.1");
// 黑名单IP
reg.addInitParameter("deny", "10.10.10.1");
// 登录凭证
reg.addInitParameter("loginUsername", "admin");
reg.addInitParameter("loginPassword", "complex_password");
return reg;
}
}
3.2 监控数据采集配置
在application.yml中开启完整监控:
yaml复制spring:
datasource:
druid:
stat-view-servlet:
enabled: true
url-pattern: /druid/*
web-stat-filter:
enabled: true
url-pattern: /*
exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"
filter:
stat:
enabled: true
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: true
wall:
enabled: true
config:
drop-table-allow: false
4. SQL日志打印的进阶方案
4.1 控制台SQL输出配置
标准日志配置(logback-spring.xml):
xml复制<logger name="druid.sql.Statement" level="DEBUG"/>
<logger name="com.ibatis" level="DEBUG"/>
<logger name="com.ibatis.common.jdbc.SimpleDataSource" level="DEBUG"/>
<logger name="java.sql.Connection" level="DEBUG"/>
<logger name="java.sql.Statement" level="DEBUG"/>
<logger name="java.sql.PreparedStatement" level="DEBUG"/>
4.2 带参数值的完整SQL打印
通过自定义拦截器实现:
java复制@Intercepts({
@Signature(type= StatementHandler.class,
method="query",
args={Statement.class, ResultHandler.class}),
@Signature(type= StatementHandler.class,
method="update",
args={Statement.class}),
@Signature(type= StatementHandler.class,
method="batch",
args={Statement.class})
})
public class SqlPrintInterceptor implements Interceptor {
private static final Logger logger = LoggerFactory.getLogger("SQL_LOGGER");
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler handler = (StatementHandler)invocation.getTarget();
BoundSql boundSql = handler.getBoundSql();
String sql = boundSql.getSql();
Object[] params = boundSql.getParameterObject();
logger.info("Executing SQL: {} \nParameters: {}",
sql.replaceAll("[\\s]+", " "),
Arrays.toString(params));
return invocation.proceed();
}
}
5. 生产环境最佳实践
5.1 多数据源事务管理
使用AbstractRoutingDataSource时需注意:
java复制@Bean
@Primary
public DataSource dataSource() {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", masterDataSource());
targetDataSources.put("slave", slaveDataSource());
AbstractRoutingDataSource routingDataSource = new AbstractRoutingDataSource() {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceType();
}
};
routingDataSource.setDefaultTargetDataSource(masterDataSource());
routingDataSource.setTargetDataSources(targetDataSources);
return routingDataSource;
}
// 配合AOP实现动态切换
@Around("@annotation(dataSource)")
public Object around(ProceedingJoinPoint point, DataSource dataSource) throws Throwable {
DataSourceContextHolder.setDataSourceType(dataSource.value());
try {
return point.proceed();
} finally {
DataSourceContextHolder.clear();
}
}
5.2 监控数据持久化方案
Druid的监控数据默认存储在内存,重启会丢失。可通过以下配置存入数据库:
yaml复制spring:
datasource:
druid:
stat:
db-type: mysql
filter:
stat:
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: true
# 监控数据存储配置
monitor:
storage:
enable: true
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://monitor-db:3306/druid_stats
username: monitor
password: encrypted_pwd
initial-size: 3
max-active: 10
6. 常见问题排查指南
6.1 监控页面无法访问
排查步骤:
- 检查
stat-view-servlet.enabled是否为true - 确认没有安全框架(如Spring Security)拦截/druid路径
- 查看启动日志是否有Druid初始化报错
- 尝试直接访问
http://localhost:8080/druid/login.html
6.2 SQL日志不显示
可能原因:
- 日志级别设置过高(应设为DEBUG)
- 使用了MyBatis-Plus但未配置日志实现
- 连接池实际使用的是HikariCP(检查依赖冲突)
解决方案:
java复制@Configuration
public class MyBatisConfig {
@Bean
public ConfigurationCustomizer configurationCustomizer() {
return configuration -> {
configuration.setLogImpl(StdOutImpl.class); // 或Slf4jImpl
};
}
}
6.3 多数据源切换失效
典型症状:
- @Transactional注解内切换不生效
- 异步方法中数据源切换失败
根本原因:
Spring事务管理会缓存Connection,解决方法:
java复制// 在切换数据源后添加
TransactionSynchronizationManager.unbindResource(
dataSource.getConnection().getMetaData().getConnection());
7. 性能优化建议
-
连接池参数调优:
yaml复制spring: datasource: 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 -
监控采样优化:
yaml复制spring: datasource: druid: filter: stat: enabled: true log-slow-sql: true slow-sql-millis: 1000 merge-sql: true slf4j: enabled: true statement-executable-sql-log-enable: true -
WallFilter防护:
java复制@Bean public Filter wallFilter() { WallFilter wallFilter = new WallFilter(); wallFilter.setConfig(wallConfig()); return wallFilter; } @Bean public WallConfig wallConfig() { WallConfig config = new WallConfig(); config.setDropTableAllow(false); config.setAlterTableAllow(false); config.setNoneBaseStatementAllow(false); return config; }
在实际项目中,Druid的监控数据帮我们发现了多个潜在性能问题,比如某个批量插入操作没有使用rewriteBatchedStatements参数,导致实际还是单条提交。通过监控页面的SQL统计功能,我们很快定位到这个问题并优化了30%的写入性能。
