SpringBoot整合druid实现多数据源切换并实现sql监控
前言:目前需要项目需要引用不同数据库的数据(用户信息及订单数据等),想整合在一起,但是项目还在开发中,尝试着在项目中引用多数据源进行开发,通过aop实现数据源切换,以下是我代码实现:
一.开发环境:
JDK:1.8
SpringBoot:2.4.4
二.加入依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<!--druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.22</version>
</dependency>
<!--SpringAOP-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
三.代码实现:
1. 在properties里面添加数据源配置
#==============================主数据库相关配置
spring.druid.master.driver-class-name =com.mysql.cj.jdbc.Driver
spring.druid.master.url=jdbc:mysql:XXXXXXXX
spring.druid.master.username=xxxx
spring.druid.master.password=xxxx
#==============================从1数据库相关配置
spring.druid.slave1.driver-class-name =com.mysql.cj.jdbc.Driver
spring.druid.slave1.url=jdbc:mysql:XXXXXXXX
spring.druid.slave1.username=xxxx
spring.druid.slave1.password=xxxx
#最大连接数
spring.druid.maxActive=30
#最小连接数
spring.druid.minIdle=5
#获取连接的最大等待时间
spring.druid.maxWait=10000
#解决mysql8小时的问题
spring.druid.validationQuery=SELECT 'x'
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
spring.druid.timeBetweenEvictionRunsMillis=60000
#空闲连接最小空闲时间
spring.druid.minEvictableIdleTimeMillis=300000
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
spring.druid.filters=stat,wall,log4j
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
spring.druid.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
#开启控制台打印sql
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
# mybatis 下划线转驼峰配置,两者都可以
mybatis.configuration.map-underscore-to-camel-case=true
2. 多数据源配置类
/**
* @Author xw
* @Description 多数据源配置类
* @Date 2021/4/19 13:56
*/
@Configuration
public class DataSourceConfig {
/**
* 数据源1
* spring.druid.master :application.properteis中对应属性的前缀
* @return
*/
@Bean(name = "master")
@ConfigurationProperties(prefix = "spring.druid.master")
public DataSource master() {
System.out.println("master:"+new DruidDataSource());
return new DruidDataSource();
}
/**
* 数据源2
* spring.druid.slave1 :application.properteis中对应属性的前缀
* @return
*/
@Bean(name = "slave1")
@ConfigurationProperties(prefix = "spring.druid.slave1")
public DataSource slave1() {
System.out.println("slave1:"+ new DruidDataSource());
return new DruidDataSource();
}
/**
* 动态数据源: 通过AOP在不同数据源之间动态切换
* @return
*/
@Primary // 注意:这里需要该注解声明是默认数据源
@Bean(name = "dynamicDataSource")
public DataSource dynamicDataSource() {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
// 配置多数据源
Map<Object, Object> targetDataSource = new HashMap();
targetDataSource.put("master", master());
targetDataSource.put("slave1", slave1());
// 默认数据源
dynamicDataSource.setDefaultTargetDataSource(master());
//数据源
dynamicDataSource.setTargetDataSources(targetDataSource);
return dynamicDataSource;
}
//添加事务
@Bean(name = "transactionManager")
public PlatformTransactionManager platformTransactionManager(@Qualifier("dynamicDataSource")DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean
@ConditionalOnMissingBean
public ServletRegistrationBean druidServlet() {
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");
//白名单
servletRegistrationBean.addInitParameter("allow", "ip");
// IP黑名单(存在共同时,deny优先于allow):如果满足deny的话提示:Sorry, you are not permitted to view this page.
servletRegistrationBean.addInitParameter("deny", "ip");
//用于登陆的账号密码
servletRegistrationBean.addInitParameter("loginUsername", "admin");
servletRegistrationBean.addInitParameter("loginPassword", "admin");
//是否能重置数据
servletRegistrationBean.addInitParameter("resetEnable", "true");
return servletRegistrationBean;
}
/**
* @Description: 注册filter信息,用于拦截
*/
@Bean
public FilterRegistrationBean<WebStatFilter> filterRegistrationBean() {
//创建过滤器
FilterRegistrationBean<WebStatFilter> filterRegistrationBean = new FilterRegistrationBean<>(new WebStatFilter());
//设置过滤器过滤路径
filterRegistrationBean.addUrlPatterns("/*");
//忽略过滤得形式
filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
return filterRegistrationBean;
}
}
3. 动态数据源上下文,数据源相关操作类
/**
* @Author xw
* @Description 动态数据源上下文,数据源相关操作类
* @Date 2021/4/19 13:59
*/
public class DataSourceContextHolder {
private static final Logger logger = LoggerFactory.getLogger(DataSourceContextHolder.class);
public static final String Mater = "master";
public static final String Slave1 = "slave1";
private static final ThreadLocal<String> contextHolder = ThreadLocal.withInitial(() -> Mater);
/**
* 设置数据源名
* @param dataSource
*/
public static void setDB(String dataSource ) {
logger.info("切换数据源 : {}",dataSource);
contextHolder.set(dataSource);
}
/**
* 获取数据源名
* @return
*/
public static String getDB() {
return contextHolder.get();
}
/**
* 清除数据源名
*/
public static void clearDB() {
contextHolder.remove();
}
}
4. 动态获取数据源(动态获取数据源需要继承AbstractRoutingDataSource,并重写determineCurrentLookupKey()方法,此方法是在open connection**时触发, 事务是在connection层面管理的,启用事务后,一个事务内部的connection是复用的,所以就算AOP切了数据源字符串,但是数据源并不会被真正修改这是一个注意事项,后续会进行说明
**
* @Author xw
* @Description 动态获取数据源
* @Date 2021/4/19 14:00
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
private static final Logger logger = LoggerFactory.getLogger(DynamicDataSource.class);
public static DynamicDataSource build() {
return new DynamicDataSource();
}
@Override
protected Object determineCurrentLookupKey() {
logger.info("当前使用数据源 : {}",DataSourceContextHolder.getDB());
return DataSourceContextHolder.getDB();
}
}
5. 自定义注解(自定义aop注解,实现自由切换数据源)
**
* 自定义注解
*/
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface DS {
String value() default DataSourceContextHolder.Mater;
}
6.AOP实现数据源切换
**
* @Author xw
* @Description AOP实现数据源切换
* @Date 2021/4/19 14:01
*/
@Aspect
@Component
@Order(-1) //值越小,优先级越高 保证该AOP在@Transactional之前执行
public class DynamicDataSourceAspect {
private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class);
@Pointcut("@annotation(DS)")
public void dataSourcePointCut(){
}
@Around("dataSourcePointCut()")
public Object around(ProceedingJoinPoint point)throws Throwable{
MethodSignature signature = (MethodSignature)point.getSignature();
Method method = signature.getMethod();
DS ds = method.getAnnotation(DS.class);
// 通过判断 DataSource 中的值来判断当前方法应用哪个数据源
if(ds == null){
DataSourceContextHolder.setDB(DataSourceContextHolder.Mater);
logger.debug("当前数据源: " + ds.value());
}else{
DataSourceContextHolder.setDB(ds.value());
logger.debug("当前数据源:",ds.value());
}
try{
return point.proceed();
}finally {
DataSourceContextHolder.clearDB();
logger.info("clear datasource {}",ds.value());
}
}
}
至此,多数据源配置已经完成,我们只需要在service的实现类里面使用自定义的@DS注解就可以实现aop多数据源切换了
继承baseServiceImpl事务处理,需要在aop实现类上加上@Order(-1)进行优先级排序,不然事务在aop 之前执行时,切换数据源的方法在事务开启时触发,get不到数据源,会一直默认为主数据源。
如上代码有几点注意事项:
- 加@Order(-1),保证aop在事务之前执行,不然切换不了数据源
- 有可能做了@Order(-1)的优先级排序处理依然切换不了数据源,可能出现的问题:
项目使用了shiro安全框架,做了DefaultAdvisorAutoProxyCreator,如下的bean
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
这是shiro的事务自动代理,启动项目时,二次代理首先开启了事务支持,所以加入了@Order(-1)也没有起效可能是这个原因引起的,如果不影响,可以直接注释掉这个bean就可以正常使用了。