Spring Boot整合Druid实现多数据源与读写分离:动态配置与深度监控实践
随着业务的快速发展,单数据源往往难以支撑日益增长的并发请求和数据吞吐量。数据库的读写分离和多数据源管理成为了高并发、大数据量场景下不可或缺的架构优化手段。然而,如何优雅、灵活地实现这些功能,并确保系统稳定性和可观测性,是许多开发者面临的挑战。
本文将聚焦于如何利用阿里巴巴开源的Druid连接池,在Spring Boot项目中实现一套健壮的多数据源管理和读写分离方案,并提供动态配置与详细监控的能力。
1. 为什么选择Druid连接池?
在众多Java连接池中,Druid以其强大的监控功能、丰富的配置选项、SQL防火墙以及对各种数据库方言的良好支持脱颖而出。它不仅能提供高性能的连接管理,还能在生产环境中提供宝贵的数据洞察,帮助我们快速定位问题。
核心优势:
- 性能卓越: 高效的连接管理策略,减少连接创建和销毁的开销。
- 功能强大: 内置SQL监控、慢SQL记录、SQL防火墙等功能。
- 动态配置: 支持运行时修改连接池参数,无需重启应用。
- 易于集成: 与Spring Boot等主流框架无缝集成。
2. 项目准备
首先,确保你的Spring Boot项目引入了必要的依赖。
在pom.xml中添加:
<dependencies>
<!-- Spring Boot Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Druid Spring Boot Starter -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version> <!-- 根据实际情况选择最新版本 -->
</dependency>
<!-- MySQL Connector (或其他数据库驱动) -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- MyBatis (或JPA等ORM框架) -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<!-- AOP用于实现动态数据源切换 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
3. 配置多数据源
假设我们有两个数据源:一个主库(master)用于写操作,一个从库(slave)用于读操作。
在application.yml中配置:
spring:
datasource:
# 禁用Spring Boot的自动配置,我们手动管理
type: com.alibaba.druid.pool.DruidDataSource
dynamic: # 自定义动态数据源配置
primary: master # 默认数据源
strict: false # 是否严格模式,当找不到数据源时报错
datasource:
master:
url: jdbc:mysql://localhost:3306/db_master?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: your_master_password
driver-class-name: com.mysql.cj.jdbc.Driver
# 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,log4j2 # 开启监控和SQL防火墙
connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
slave:
url: jdbc:mysql://localhost:3306/db_slave?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: your_slave_password
driver-class-name: com.mysql.cj.jdbc.Driver
initial-size: 3
min-idle: 3
max-active: 10
# 其他与master类似或根据从库负载调整的配置
filters: stat,wall,log4j2
connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
4. 实现动态数据源切换
为了在运行时动态选择数据源,我们需要一个自定义的AbstractRoutingDataSource实现类和一套切换机制。
4.1 定义数据源上下文
public class DynamicDataSourceContextHolder {
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
public static void setDataSourceType(String dataSourceType) {
CONTEXT_HOLDER.set(dataSourceType);
}
public static String getDataSourceType() {
return CONTEXT_HOLDER.get();
}
public static void clearDataSourceType() {
CONTEXT_HOLDER.remove();
}
}
4.2 自定义动态数据源
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import java.util.Map;
public class DynamicDataSource extends AbstractRoutingDataSource {
public DynamicDataSource(javax.sql.DataSource defaultDataSource, Map<Object, Object> targetDataSources) {
// 设置默认数据源
super.setDefaultTargetDataSource(defaultDataSource);
// 设置所有目标数据源
super.setTargetDataSources(targetDataSources);
// 调用 afterPropertiesSet 方法初始化数据源
super.afterPropertiesSet();
}
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceType();
}
}
4.3 配置动态数据源Bean
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
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 javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.dynamic.datasource.master")
public DataSource masterDataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.dynamic.datasource.slave")
public DataSource slaveDataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean
@Primary // 标记为主数据源,Spring会优先注入这个
public DataSource dynamicDataSource(DataSource masterDataSource, DataSource slaveDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", masterDataSource);
targetDataSources.put("slave", slaveDataSource);
return new DynamicDataSource(masterDataSource, targetDataSources); // 默认使用master
}
}
4.4 定义数据源切换注解和AOP切面
// DataSourceType.java
import java.lang.annotation.*;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSourceType {
String value() default "master"; // 默认是master
}
// DataSourceAspect.java
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Aspect
@Order(1) // 确保在事务注解之前执行
@Component
public class DataSourceAspect {
@Around("@annotation(com.example.demo.annotation.DataSourceType)") // 替换为你的注解路径
public Object switchDataSource(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
DataSourceType dataSourceType = method.getAnnotation(DataSourceType.class);
if (dataSourceType != null) {
String ds = dataSourceType.value();
DynamicDataSourceContextHolder.setDataSourceType(ds);
System.out.println("Switching to dataSource: " + ds);
}
try {
return point.proceed();
} finally {
DynamicDataSourceContextHolder.clearDataSourceType();
System.out.println("Cleared dataSource context.");
}
}
}
现在,你可以在Service层的方法上使用@DataSourceType("slave")来指定读操作使用从库,@DataSourceType("master")或不加注解(默认为master)用于写操作。
import com.example.demo.annotation.DataSourceType; // 替换为你的注解路径
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserService {
// 注入你的Mapper或Repository
// @Autowired
// private UserMapper userMapper;
@Transactional // 事务默认在master库
public void createUser(String name) {
// userMapper.insert(name);
System.out.println("Creating user in master DB: " + name);
}
@DataSourceType("slave") // 指定从库
public String getUserById(Long id) {
// return userMapper.selectById(id);
System.out.println("Querying user from slave DB with ID: " + id);
return "User from slave DB";
}
@DataSourceType("master") // 明确指定主库
@Transactional
public void updateUser(Long id, String newName) {
// userMapper.update(id, newName);
System.out.println("Updating user in master DB with ID: " + id + ", new name: " + newName);
}
}
5. Druid监控页面集成
Druid提供了一个强大的内置监控页面,可以实时查看连接池状态、SQL执行情况、慢SQL等。
5.1 配置Druid Servlet
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 ServletRegistrationBean<StatViewServlet> druidStatViewServlet() {
ServletRegistrationBean<StatViewServlet> servletRegistrationBean =
new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*"); // 访问路径
// 设置白名单或黑名单
// servletRegistrationBean.addInitParameter("allow", "127.0.0.1");
// servletRegistrationBean.addInitParameter("deny", "192.168.1.100");
// 设置登录名和密码
servletRegistrationBean.addInitParameter("loginUsername", "admin");
servletRegistrationBean.addInitParameter("loginPassword", "123456");
// 是否可以重置数据
servletRegistrationBean.addInitParameter("resetEnable", "true");
return servletRegistrationBean;
}
@Bean
public FilterRegistrationBean<WebStatFilter> druidWebStatFilter() {
FilterRegistrationBean<WebStatFilter> filterRegistrationBean =
new FilterRegistrationBean<>(new WebStatFilter());
// 过滤所有请求
filterRegistrationBean.addUrlPatterns("/*");
// 排除对静态资源和Druid监控页面的过滤
filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
return filterRegistrationBean;
}
}
启动应用后,访问 http://localhost:8080/druid/index.html (根据你的端口和Context Path调整) 即可进入Druid监控页面,输入配置的用户名密码即可查看详细的监控指标。
6. 总结
通过上述步骤,我们成功在Spring Boot应用中集成了Druid连接池,并实现了多数据源管理和基于AOP的读写分离。这套方案不仅具备了良好的扩展性和灵活性,还能通过Druid强大的监控功能,为系统的稳定运行提供有力保障。在实际项目中,你可以根据业务需求和数据库负载情况,进一步优化连接池参数,并考虑引入数据库中间件(如ShardingSphere、MyCAT等)来处理更复杂的分布式事务和分库分表场景。