本文介绍场景:
- Spring Boot多数据源配置
- 结合Druid实现多数据源配置
准备工作
- 两个一模一样的数据库:db1和db2,创建好表,比如Student
实现步骤
- application.ymls中配置两个数据库信息
- 自定义数据库配置类,排除SpringBoot提供的自动配置
- 关键在于SpringBoot的AbstractRoutingDataSource类,里面有个实现方法determineCurrentLookupKey,在这里动态的指定需要的数据源
- 结合ThreadLocal和AOP切面来实现数据源的动态切换
具体操作
application.yml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
db1: # 数据源1
url: jdbc:mysql://mylocalhost:3306/db1?createDatabaseIfNotExist=true&character_set_server=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
db2: # 数据源2
url: jdbc:mysql://mylocalhost:3306/db2?createDatabaseIfNotExist=true&character_set_server=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
自定义数据源配置类,自定义后我们需要将SpringBoot内的数据源自动配置被排除掉:
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
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;
@Configuration
public class DataSourceConfig {
@Bean(name = "db1")
@ConfigurationProperties("spring.datasource.db1")
public DataSource db1DataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "db2")
@ConfigurationProperties("spring.datasource.db2")
public DataSource db2DataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "dynamicDataSource")
@Primary
public DataSource dynamicDataSource() {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
// 配置默认数据源
dynamicDataSource.setDefaultTargetDataSource(db1DataSource());
// 配置多数据源,后面才可以进行动态切换
HashMap<Object, Object> datasourceMap = new HashMap<>();
datasourceMap.put(DataSourceEnum.DATASOURCE1.name(), db1DataSource());
datasourceMap.put(DataSourceEnum.DATASOURCE2.name(), db2DataSource());
dynamicDataSource.setTargetDataSources(datasourceMap);
return dynamicDataSource;
}
}
排除自动配置,不然会有循环引用:
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
创建一个枚举便于数据源切换,不用写魔法值:
public enum DataSourceEnum {
/**
* 数据源1
*/
DATASOURCE1,
/**
* 数据源2
*/
DATASOURCE2,
}
利用ThreadLocal创建线程安全的变量副本,记录当前线程用的是哪个数据源:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DataSourceContextHolder {
private static final Logger log = LoggerFactory.getLogger(DataSourceContextHolder.class);
/**
* 使用ThreadLocal线程安全的使用变量副本
*/
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
/**
* 设置/指定数据源
*/
public static void setDataSource(String dataSource) {
log.info("切换到数据源:{}", dataSource);
CONTEXT_HOLDER.set(dataSource);
}
/**
* 获取数据源
*/
public static String getDataSource() {
return CONTEXT_HOLDER.get();
}
/**
* 清空信息
*/
public static void clearDataSource() {
CONTEXT_HOLDER.remove();
}
}
创建注解来声明数据源:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
DataSourceEnum value() default DataSourceEnum.DATASOURCE1;
}
创建该注解的切面,读取要用到的数据源:
@Aspect
@Component
@Order(-1)
public class DataSourceAspect {
@Pointcut("@annotation(com.cc.config.DataSource)")
public void dataSourcePointCut() {}
@Around("dataSourcePointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
DataSource dataSource = method.getAnnotation(DataSource.class);
if (dataSource != null) {
DataSourceContextHolder.setDataSource(dataSource.value().name());
}
try {
return joinPoint.proceed();
} finally {
DataSourceContextHolder.clearDataSource();
}
}
}
切面顺序务必比@Transactional注解的切面高,不然拿不到该数据源,也就没办法实现切换了
@Transactional注解的切面是最大值,即执行顺序最低,所以@Order(-1)就可以
最后是给需要的接口方法添加注解,声明使用的数据源,不加则是默认数据源:
@DataSource(value = DataSourceEnum.DATASOURCE2)
@RequestMapping(value = "/student/listAll", method = RequestMethod.GET)
public List<Student> listAll() {
return studentService.listAll();
}
结合Druid
添加依赖:
<!--druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.21</version>
</dependency>
配置文件要修改一下:
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
druid:
db1:
url: jdbc:mysql://mylocalhost:3306/db1?createDatabaseIfNotExist=true&character_set_server=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
db2:
url: jdbc:mysql://mylocalhost:3306/db2?createDatabaseIfNotExist=true&character_set_server=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
# 初始化时建立物理连接的个数
initial-size: 5
# 最大连接池数量
max-active: 20
# 最小连接池数量
min-idle: 10
# 获取连接时最大等待时间,单位毫秒
max-wait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
time-between-eviction-runs-millis: 60000
# 连接保持空闲而不被驱逐的最小时间
min-evictable-idle-time-millis: 300000
# 用来检测连接是否有效的sql,要求是一个查询语句
validation-query: SELECT 1 FROM DUAL
# 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效
test-while-idle: true
# 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能
test-on-borrow: false
# 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能
test-on-return: false
# 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭
pool-prepared-statements: false
# 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true
max-pool-prepared-statement-per-connection-size: 50
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计
filter:
stat:
enabled: true
# 慢sql记录
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: true
wall:
config:
multi-statement-allow: true
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
connect-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
# 合并多个DruidDataSource的监控数据
use-global-data-source-stat: true
web-stat-filter:
enabled: true
stat-view-servlet:
enabled: true
# 设置白名单,不填则允许所有访问
allow:
url-pattern: /druid/*
# 用户名密码
# login-username: admin
# login-password: 123456
自定义配置类也要调整,给DataSourceBuilder创建的时候添加上type类型:
@Configuration
public class DataSourceConfig {
@Bean(name = "db1")
@ConfigurationProperties("spring.datasource.druid.db1")
public DataSource db1DataSource() {
return DataSourceBuilder.create().type(DruidDataSource.class).build();
}
@Bean(name = "db2")
@ConfigurationProperties("spring.datasource.durid.db2")
public DataSource db2DataSource() {
return DataSourceBuilder.create().type(DruidDataSource.class).build();
}
@Bean(name = "dynamicDataSource")
@Primary
public DataSource dynamicDataSource() {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
// 配置默认数据源
dynamicDataSource.setDefaultTargetDataSource(db1DataSource());
// 配置多数据源
HashMap<Object, Object> datasourceMap = new HashMap<>();
datasourceMap.put(DataSourceEnum.DATASOURCE1.name(), db1DataSource());
datasourceMap.put(DataSourceEnum.DATASOURCE2.name(), db2DataSource());
dynamicDataSource.setTargetDataSources(datasourceMap);
return dynamicDataSource;
}
}
其他的保持不变即可,启动后访问:http://localhost:8080/druid/
思考
多数据源的应用场景一般用于单体应用需要访问多个数据库的情况,
或者为了减轻主数据库压力,将一定的访问量分配给从数据库。
如果想要做到类似Ribbon那样的负载均衡,让从数据库动态的承担访问压力,而不是指定死某些接口,那其实做成微服务的形式就好,没有必要自己撸一个出来,但也可以看看有没有现成的轮子可以用。