动态数据源切换
- 一、动态数据源切换的实现
-
- [1. 使用AbstractRoutingDataSource](#1. 使用AbstractRoutingDataSource)
- [2. ThreadLocal管理上下文](#2. ThreadLocal管理上下文)
- 二、数据源初始化与配置
-
- 2.1.配置多数据源
- [2.2. 实现动态数据源切换](#2.2. 实现动态数据源切换)
-
- [2.2.1 创建DynamicDataSource类](#2.2.1 创建DynamicDataSource类)
- [2.2.2 创建DataSourceContextHolder类](#2.2.2 创建DataSourceContextHolder类)
- [2.2.3 配置多数据源](#2.2.3 配置多数据源)
- [2.2.4 配置Druid连接池](#2.2.4 配置Druid连接池)
- 三、实现数据源切换-手动切换
-
- [3.1 创建通用的Service](#3.1 创建通用的Service)
- [3.2 创建Controller](#3.2 创建Controller)
- [3.3 测试动态数据源切换](#3.3 测试动态数据源切换)
- 四、实现数据源切换-AOP切换
-
- [4.1 创建AOP切面](#4.1 创建AOP切面)
- [4.2 创建自定义注解](#4.2 创建自定义注解)
- [4.3 使用注解切换数据源](#4.3 使用注解切换数据源)
-
- [4.3.1 创建通用的Service](#4.3.1 创建通用的Service)
- [4.3.2 在Controller中调用](#4.3.2 在Controller中调用)
- [4.3.3 测试动态数据源切换](#4.3.3 测试动态数据源切换)
- [4.3.4 关于@Order(-1)](#4.3.4 关于@Order(-1))
-
- [4.3.4.1. 作用](#4.3.4.1. 作用)
- [4.3.4.2. 为什么在这里使用 @Order(-1)](#4.3.4.2. 为什么在这里使用 @Order(-1))
- [4.3.4.3. 示例场景](#4.3.4.3. 示例场景)
- [4.3.4.4. 注意事项](#4.3.4.4. 注意事项)
- [五、 注意事项](#五、 注意事项)
- 六、总结
1.动态数据源切换:
使用AbstractRoutingDataSource实现动态数据源切换。
通过ThreadLocal保存当前线程的数据源标识。
2.共用Service:
Service中的方法不关心具体的数据源,只负责通用的业务逻辑。
3.自动切换-手动切换:
在调用Service方法前,根据用户传入的数据源名称动态设置数据源。
在调用Service方法后,清除数据源上下文。
4.自动切换-AOP切面:
在方法执行前,根据注解的值设置数据源。
在方法执行后,清除数据源上下文。
一、动态数据源切换的实现
1. 使用AbstractRoutingDataSource
Spring提供了AbstractRoutingDataSource,可以通过它实现动态数据源切换。你需要自定义一个DataSource路由器,根据当前选择的业务系统动态返回对应的数据源。
2. ThreadLocal管理上下文
使用ThreadLocal来保存当前线程的数据源标识(如业务系统的ID或名称),在切换时更新ThreadLocal中的值。
二、数据源初始化与配置
2.1.配置多数据源
你的配置已经定义了多个数据源(master、iss、eos、mabs),并且使用了Druid连接池。我们需要将这些数据源加载到Spring容器中,并通过AbstractRoutingDataSource实现动态切换。数据库相关.yaml配置文件内容如下
yaml
spring:
mvc:
date-format: yyyy-MM-dd HH:mm:ss
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
servlet:
multipart:
#开启文件上传
enabled: true
# 设置传输大小
max-file-size: 100MB
max-request-size: 100MB
datasource:
druid:
stat-view-servlet:
enabled: false
loginUsername: admin
loginPassword: 1a2b3c4d5e6!@#
allow:
web-stat-filter:
enabled: false
dynamic:
druid: # 全局druid参数,绝大部分值和默认保持一致。(现已支持的参数如下,不清楚含义不要乱设置)
# 连接池的配置信息
# 初始化大小,最小,最大
initial-size: 5
min-idle: 5
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
# validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
# 打开PSCache,并且指定每个连接上PSCache的大小
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
filters: stat,wall,slf4j
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000
primary: master
datasource:
master:
url: jdbc:mysql://10.168.31.48:3306/nanjing_sjys_auth?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
username: root
password: postgres6666!
driver-class-name: com.mysql.cj.jdbc.Driver
iss:
url: jdbc:postgresql://10.168.31.48:5432/yunect_nanjing_iss?currentSchema=public&zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8
username: postgres
password: postgres6666!
driver-class-name: org.postgresql.Driver
eos:
url: jdbc:postgresql://10.168.31.48:5432/yunect_nanjing_iss?currentSchema=eos&zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8
username: postgres
password: postgres6666!
driver-class-name: org.postgresql.Driver
mabs:
url: jdbc:postgresql://10.168.31.48:5432/yunect_nanjing_iss?currentSchema=mabs&zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8
username: postgres
password: postgres6666!
driver-class-name: org.postgresql.Driver
2.2. 实现动态数据源切换
2.2.1 创建DynamicDataSource类
继承AbstractRoutingDataSource,实现动态数据源切换。
java
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceKey();
}
}
2.2.2 创建DataSourceContextHolder类
使用ThreadLocal保存当前线程的数据源标识。
java
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setDataSourceKey(String key) {
contextHolder.set(key);
}
public static String getDataSourceKey() {
return contextHolder.get();
}
public static void clearDataSourceKey() {
contextHolder.remove();
}
}
2.2.3 配置多数据源
将配置文件中的数据源加载到Spring容器中,并配置DynamicDataSource。
java
import com.alibaba.druid.pool.DruidDataSource;
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(name = "masterDataSource")
@ConfigurationProperties(prefix = "spring.datasource.dynamic.datasource.master")
public DataSource masterDataSource() {
return new DruidDataSource();
}
// ISS 数据源
@Bean(name = "issDataSource")
@ConfigurationProperties(prefix = "spring.datasource.dynamic.datasource.iss")
public DataSource issDataSource() {
return new DruidDataSource();
}
// ISS_EOS 数据源
@Bean(name = "eosDataSource")
@ConfigurationProperties(prefix = "spring.datasource.dynamic.datasource.eos")
public DataSource issEosDataSource() {
return new DruidDataSource();
}
// ISS_MABS 数据源
@Bean(name = "mabsDataSource")
@ConfigurationProperties(prefix = "spring.datasource.dynamic.datasource.mabs")
public DataSource issMabsDataSource() {
return new DruidDataSource();
}
// 动态数据源
@Primary
@Bean(name = "dynamicDataSource")
public DataSource dynamicDataSource(
@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("issDataSource") DataSource issDataSource,
@Qualifier("issEosDataSource") DataSource issEosDataSource,
@Qualifier("issMabsDataSource") DataSource issMabsDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", masterDataSource);
targetDataSources.put("iss", issDataSource);
targetDataSources.put("eos", issEosDataSource);
targetDataSources.put("mabs", issMabsDataSource);
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setDefaultTargetDataSource(masterDataSource); // 默认数据源
dynamicDataSource.setTargetDataSources(targetDataSources);
dynamicDataSource.afterPropertiesSet();
return dynamicDataSource;
}
}
2.2.4 配置Druid连接池
根据.yaml配置,Druid连接池已经启用。确保filters和connectionProperties等参数正确配置。
三、实现数据源切换-手动切换
3.1 创建通用的Service
Service中的方法对每个数据源都通用,不关心具体的数据源。
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class DataService {
@Autowired
private DataRepository dataRepository;
/**
* 通用的查询方法
*/
public List<Data> getData() {
// 直接调用Repository方法,数据源由上层决定
return dataRepository.findAll();
}
/**
* 通用的插入方法
*/
public void saveData(Data data) {
dataRepository.save(data);
}
}
3.2 创建Controller
在Controller中根据用户传入的数据源名称动态设置数据源,并调用Service方法。
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/data")
public class DataController {
@Autowired
private DataService dataService;
/**
* 根据数据源名称查询数据
*/
@GetMapping("/query")
public List<Data> queryData(@RequestParam String dataSourceKey) {
// 设置数据源
DataSourceContextHolder.setDataSourceKey(dataSourceKey);
try {
// 调用Service方法
return dataService.getData();
} finally {
// 清除数据源上下文
DataSourceContextHolder.clearDataSourceKey();
}
}
/**
* 根据数据源名称保存数据
*/
@PostMapping("/save")
public void saveData(@RequestParam String dataSourceKey, @RequestBody Data data) {
// 设置数据源
DataSourceContextHolder.setDataSourceKey(dataSourceKey);
try {
// 调用Service方法
dataService.saveData(data);
} finally {
// 清除数据源上下文
DataSourceContextHolder.clearDataSourceKey();
}
}
}
3.3 测试动态数据源切换
启动应用后,访问以下URL测试:
- 查询数据:
/data/query?dataSourceKey=master:查询主数据源的数据。
/data/query?dataSourceKey=iss:查询ISS数据源的数据。
/data/query?dataSourceKey=iss_eos:查询ISS_EOS数据源的数据。
/data/query?dataSourceKey=iss_mabs:查询ISS_MABS数据源的数据。 - 保存数据:
/data/save?dataSourceKey=master:保存数据到主数据源。
/data/save?dataSourceKey=iss:保存数据到ISS数据源。
四、实现数据源切换-AOP切换
4.1 创建AOP切面
通过AOP在方法执行前切换数据源,方法执行后清除数据源上下文。@Order(-1)非常重要,会在后面详细介绍。
java
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.jeecg.modules.dataSource.DataSourceContextHolder;
import org.jeecg.modules.dataSource.aspect.annotation.SwitchDataSource;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Aspect
@Component
@Order(-1)
public class DataSourceAspect {
@Pointcut("@within(org.jeecg.modules.dataSource.aspect.annotation.SwitchDataSource) || @annotation(org.jeecg.modules.dataSource.aspect.annotation.SwitchDataSource)")
public void excudeService() {
}
@Before("excudeService()")
public void beforeSwitchDataSource(JoinPoint joinPoint) {
// 获取方法上的注解
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
SwitchDataSource switchDataSource = method.getAnnotation(SwitchDataSource.class);
String dataSourceKey = null;
// 如果注解值不为空,使用注解值
if (switchDataSource != null && !switchDataSource.value().isEmpty()) {
dataSourceKey = switchDataSource.value();
}
// 如果注解值为空,从方法参数中获取数据源名称
else if (joinPoint.getArgs().length > 0 && joinPoint.getArgs()[0] instanceof String) {
dataSourceKey = (String) joinPoint.getArgs()[0];
}
// 设置数据源
if (dataSourceKey != null && !dataSourceKey.isEmpty()) {
DataSourceContextHolder.setDataSourceKey(dataSourceKey);
}
}
@After("excudeService()")
public void afterClearDataSource(JoinPoint joinPoint) {
// 清除数据源上下文
DataSourceContextHolder.clearDataSourceKey();
}
}
4.2 创建自定义注解
java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TargetDataSource {
String value() default "master"; // 默认使用主数据源
}
4.3 使用注解切换数据源
4.3.1 创建通用的Service
将 @SwitchDataSource 注解加在 DataService 类上,并通过方法参数传入数据源名称。
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@SwitchDataSource // 加在类上,表示该类的方法支持动态数据源切换
public class DataService {
@Autowired
private DataRepository dataRepository;
/**
* 通用的查询方法
*/
public List<Data> getData(String dataSourceKey) {
return dataRepository.findAll();
}
/**
* 通用的插入方法
*/
public void saveData(String dataSourceKey, Data data) {
dataRepository.save(data);
}
}
4.3.2 在Controller中调用
在Controller中调用Service方法,传入数据源名称。
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/data")
public class DataController {
@Autowired
private DataService dataService;
/**
* 查询数据
*/
@GetMapping("/query")
public List<Data> queryData(@RequestParam String dataSourceKey) {
// 调用Service方法,传入数据源名称
return dataService.getData(dataSourceKey);
}
/**
* 保存数据
*/
@PostMapping("/save")
public void saveData(@RequestParam String dataSourceKey, @RequestBody Data data) {
// 调用Service方法,传入数据源名称
dataService.saveData(dataSourceKey, data);
}
}
4.3.3 测试动态数据源切换
启动应用后,访问以下URL测试:
- 查询数据:
/data/query?dataSourceKey=master:查询主数据源的数据。
/data/query?dataSourceKey=iss:查询ISS数据源的数据。 - 保存数据:
/data/save?dataSourceKey=master:保存数据到主数据源。
/data/save?dataSourceKey=iss:保存数据到ISS数据源。
4.3.4 关于@Order(-1)
@Order(-1)是 Spring AOP 中的一个注解,用于指定切面的执行顺序。
4.3.4.1. 作用
- 控制切面的执行顺序:
当多个切面(Aspect)同时作用于同一个连接点(JoinPoint)时,@Order 注解可以指定这些切面的执行顺序。 - 值越小,优先级越高:
@Order 的值越小,切面的优先级越高,越先执行。例如,@Order(-1) 的切面会比 @Order(0) 或 @Order(1) 的切面先执行。
4.3.4.2. 为什么在这里使用 @Order(-1)
代码中,DataSourceAspect 切面用于动态切换数据源。为了确保数据源切换的逻辑在其他切面之前执行,使用了 @Order(-1)。这样可以:
- 优先执行数据源切换:
在方法执行前,先切换到正确的数据源,确保后续操作(如事务管理、日志记录等)在正确的数据源上下文中执行。 - 避免数据源切换被其他切面干扰:
如果其他切面(如事务切面)先执行,可能会导致数据源切换失效或数据源上下文错乱。
4.3.4.3. 示例场景
- 假设有以下两个切面:
DataSourceAspect:用于切换数据源,@Order(-1)。
TransactionAspect:用于管理事务,@Order(0)。 - 执行顺序如下:
DataSourceAspect 的 @Before 方法:切换数据源。
TransactionAspect 的 @Before 方法:开启事务。 - 执行业务方法。
TransactionAspect 的 @After 方法:提交或回滚事务。
DataSourceAspect 的 @After 方法:清除数据源上下文。
如果 DataSourceAspect 的优先级低于 TransactionAspect,可能会导致事务在错误的数据源上执行。
4.3.4.4. 注意事项
- 默认顺序:
如果没有指定 @Order,Spring 会按照切面的注册顺序执行,但这种方式不可靠。 - 负数的使用:
使用负数(如 -1)可以确保切面的优先级高于大多数默认切面(如事务切面)。 - 合理设置顺序:
根据业务需求,合理设置切面的执行顺序,避免逻辑冲突。
五、 注意事项
线程安全
确保每次操作后清除ThreadLocal中的数据源标识,避免线程复用导致数据源错乱。
事务管理
如果涉及跨数据源的事务,需要使用分布式事务(如Seata)。
性能优化
确保Druid连接池参数合理配置,避免频繁创建和销毁连接。
六、总结
通过动态数据源切换可以实现多业务系统的数据隔离。关键点在于:
- 使用AbstractRoutingDataSource实现动态数据源切换。
- 通过AOP切面在方法执行前切换数据源。
- 优化性能,避免频繁切换带来的开销。
- 处理好事务和异常情况。
如果实现得当,这种设计可以很好地支持多业务系统的数据需求。