【多数据源动态切换数据源】

动态数据源切换

  • 一、动态数据源切换的实现
    • [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切面在方法执行前切换数据源。
  • 优化性能,避免频繁切换带来的开销。
  • 处理好事务和异常情况。
    如果实现得当,这种设计可以很好地支持多业务系统的数据需求。
相关推荐
addaduvyhup18 分钟前
《Java到Go的平滑转型指南》
java·笔记·后端·学习·golang
Java中文社群26 分钟前
面试官:工作中优化MySQL的手段有哪些?
java·后端·面试
半升酒1 小时前
Spring AOP 核心概念与实践指南
java·spring
终将超越过去1 小时前
SpringBoot-3-JWT令牌
java·spring boot·后端
郑州吴彦祖7721 小时前
HTTPS协议—加密算法和中间攻击人的博弈
java·网络·安全·https
齐 飞1 小时前
JVM类文件结构详解
java·jvm·笔记
创码小奇客2 小时前
Java 泛型:从入门到起飞
java·spring boot·spring
执墨2 小时前
啊?我的缓存中怎么出现了 java.util.ArrayList
java·redis·spring
Java技术小馆2 小时前
微服务架构中设计高可用和故障恢复机制
java·后端·面试
乌云暮年2 小时前
算法刷题整理合集(七)·【算法赛】
java·算法·链表·蓝桥杯·二分