springboot实现方法级别的动态多数据源切换

本文介绍了如何在SpringBoot应用中实现方法级别的动态数据源切换,包括Druid连接池配置、AbstractRoutingDataSource的使用、注解与切面的配合,以及实战步骤和常见问题解决。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

原理概述

主要设计三大部分:druid连接池、spring内置的抽象类AbstractRoutingDataSource、注解及切面的配合使用。其中担任动态数据源切换重要角色的是AbstractRoutingDataSource的实现类,通过他我们可以设置默认的数据源以及多个待选数据源。

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
    @Nullable
    private Map<Object, Object> targetDataSources;    //目标数据源的map,key是数据源的名称,value是配置的DataSource对象
    @Nullable
    private Object defaultTargetDataSource;   //默认数据源
    private boolean lenientFallback = true;
    private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
    @Nullable
    private Map<Object, DataSource> resolvedDataSources;
    @Nullable
    private DataSource resolvedDefaultDataSource;
	...
	...

详细用法请接着往下看

具体实现

环境搭建

首先添加druid、mybatis等pom依赖、保证项目里有这些依赖就可以,其他基础依赖此处省略

        <!--druid数据连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.21</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.11</version>
        </dependency>
        <!--json-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.70</version>
        </dependency>

数据源配置

在配置文件中配置数据库的连接信息

#mysql数据源
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    druid:
      filters: mergeStat
      initial-size: 0
      log-abandoned: true
      max-active: 20
      max-wait: 6000
      min-evictable-idle-time-millis: 25200000
      min-idle: 0
      remove-abandoned-timeout: 1800
      removeAbandoned: true
      test-on-borrow: false
      test-on-return: false
      test-while-idle: true
      time-between-eviction-runs-millis: 60000
      validation-query: SELECT 1
    password: xxxx
    type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:mysql://xxxx:3306/yuhang?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: xxxx
    red12hours:
      password: xxxx
      type: com.alibaba.druid.pool.DruidDataSource
      url: jdbc:mysql://xx.xx.xx.xx:3306/red_12_hours?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
      username: xxxx
      driverClassName: com.mysql.cj.jdbc.Driver
    publicweb:
      password: xxxxx
      type: com.alibaba.druid.pool.DruidDataSource
      url: jdbc:mysql://xx.xx.xx.xx:3306/publicweb?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
      username: xxxx
      driverClassName: com.mysql.cj.jdbc.Driver

此处共配置了三个数据源,以及其他的一些druid连接池的配置,写好yml配置文件后,需要编写springboot的Configuration类来创建对应的Bean存入IOC容器中。

@Configuration
public class DataSourcesConfig {

    /**
     * @Bean:向IOC容器中注入一个Bean
     * @ConfigurationProperties:使得配置文件中以spring.datasource为前缀的属性映射到Bean的属性中
     * @Primary注解很重要,因为系统中存在多个数据源,需要指定一个主要的,否则启动项目会报错,当然只需要加一个就可以,其他数据源的配置不需要加
     * @return
     */
    @ConfigurationProperties(prefix = "spring.datasource")
    @Bean(name = "dataSource")
    @Primary
    public DataSource masterDataSource(){
        //做一些其他的自定义配置,比如密码加密等......
        return new DruidDataSource();
    }

    /**
     * @Bean:向IOC容器中注入一个Bean
     * @ConfigurationProperties:使得配置文件中以spring.datasource为前缀的属性映射到Bean的属性中
     * @return
     */
    @ConfigurationProperties(prefix = "spring.datasource.red12hours")
    @Bean(name = "redDataSource")
    public DataSource redDataSource(){
        //做一些其他的自定义配置,比如密码加密等......
        return new DruidDataSource();
    }
        /**
     * @Bean:向IOC容器中注入一个Bean
     * @ConfigurationProperties:使得配置文件中以spring.datasource为前缀的属性映射到Bean的属性中
     * @return
     */
    @ConfigurationProperties(prefix = "spring.datasource.publicweb")
    @Bean(name = "publicwebDataSource")
    public DataSource publicwebDataSource(){
        //做一些其他的自定义配置,比如密码加密等......
        return new DruidDataSource();
    }
 }

三个数据源则需要创建三个Bean对象,在@ConfigurationProperties注解中写好需要用的配置的前缀即可当做参数注入到新建的对象中,需要保证yml文件配置名和参数名的一致,写到这里还不算完,这个配置类后续还需要加入其他的配置Bean。

实现AbstractRoutingDataSource抽象类

因为druid连接池是公共的资源,不能因为这里的切换而影响了其他线程的执行,所以引入ThreadLocal来保证线程间的隔离

/**
 * 使用ThreadLocal存储切换数据源后的KEY
 */
public class DataSourceHolder {

    //线程  本地环境
    private static final ThreadLocal<String> dataSources = new InheritableThreadLocal();

    //设置数据源
    public static void setDataSource(String datasource) {
        dataSources.set(datasource);
    }

    //获取数据源
    public static String getDataSource() {
        return dataSources.get();
    }

    //清除数据源
    public static void clearDataSource() {
        dataSources.remove();
    }
}

因为ThreadLocal可以实现线程与线程之间的隔离,所以用ThreadLocal存储当前数据源的名称,每个线程都有其自己的当前数据源,所以单线程的切换不会影响到其他的线程,下面就要实现AbstractRoutingDataSource类了

/**
 * 动态数据源,继承AbstractRoutingDataSource
 * 这里不需要通过@Compent注解来注入IOC容器,需要调用构造器手动创建Bean
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

    /**
     * 返回需要使用的数据源的key,将会按照这个KEY从Map获取对应的数据源(切换)
     * @return
     */
    @Override
    protected Object determineCurrentLookupKey() {
        //从ThreadLocal中取出KEY
        return DataSourceHolder.getDataSource();
    }

    /**
     * 构造方法填充Map,构建多数据源
     */
    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
        //默认的数据源,可以作为主数据源
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        //目标数据源
        super.setTargetDataSources(targetDataSources);
        //执行afterPropertiesSet方法,完成属性的设置
        super.afterPropertiesSet();
    }
}

下一步,创建DynamicDataSourceBean,将多数据源注入其中,在上面编写的DataSourcesConfig配置类中加入下面的代码

    @Bean(name = "dynamicDataSource")
    public DynamicDataSource DynamicDataSource(
            @Autowired @Qualifier("dataSource") DataSource primary,
            @Autowired @Qualifier("publicwebDataSource") DataSource publicwebDataSource,
            @Autowired @Qualifier("redDataSource") DataSource redDataSource
    ){
        HashMap<Object, Object> ds = new HashMap<>();
        ds.put("redDataSource",redDataSource);
        ds.put("publicwebDataSource",publicwebDataSource);
        //做一些其他的自定义配置,比如密码加密等......
        DynamicDataSource dynamicDataSource = new DynamicDataSource(primary,ds);
        return dynamicDataSource;
    }

替换mybaits中的sqlSessionFactory,以及事务管理器

注意入参是我们自己定义的实现了AbstractRoutingDataSource 类的多数据源管理对象

    /**
     * 创建动态数据源的SqlSessionFactory,传入的是动态数据源
     * @Primary这个注解很重要,如果项目中存在多个SqlSessionFactory,这个注解一定要加上
     */
    @Primary
    @Bean("sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactoryBean(DynamicDataSource dynamicDataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dynamicDataSource);
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.setDefaultFetchSize(100);
        configuration.setDefaultStatementTimeout(30);
        sqlSessionFactoryBean.setConfiguration(configuration);
        return sqlSessionFactoryBean.getObject();
    }
    /**
     * 重写事务管理器,管理动态数据源
     */
    @Primary
    @Bean(value = "transactionManager")
    public PlatformTransactionManager annotationDrivenTransactionManager(DynamicDataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

OK,截止到目前系统中已经具备了多数据源动态切换的能力,你可以 通过以下方式来实现数据源的切换,同时,以下方式可以实现同一方法内去执行不同的数据源查询,但是事务管理此时应该是不生效了,但是还没有实测。

    public List<JSONObject> getAllData(){
        ArrayList<JSONObject> jsonObjects = new ArrayList<>();
        //不指定数据源则使用默认的
        List<JSONObject> sysUser = dsTestMapper.getSysUserList();
        DataSourceHolder.setDataSource("redDataSource");  //切换数据源
        List<JSONObject> sysUser2 = dsTestMapper.getWxUser();
        DataSourceHolder.clearDataSource();  //使用完记得清除掉数据源,相当于切换回默认数据源
        jsonObjects.add(sysUser.get(0));
        jsonObjects.add(sysUser2.get(0));
        return jsonObjects;
    }

而实际项目中,较多的场景是方法内数据源一致,所以搭配注解来实现方法级别的数据源切换是非常常用的。

注解及切面的实现

编写注解:

/**
 * 切换数据源的注解
 */
@Target(value = ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
@Documented
public @interface SwitchSource {

    /**
     * 默认切换的数据源KEY
     */
    String DEFAULT_NAME = "redDataSource";

    /**
     * 需要切换到数据的KEY
     */
    String value() default DEFAULT_NAME;
}

对加注解了的方法添加环绕切面

@Aspect
//优先级要设置在事务切面执行之前
@Order(1)
@Component
@Slf4j
public class DataSourceAspect {


    @Pointcut("@annotation(SwitchSource)")
    public void pointcut() {
    }

    /**
     * 在方法执行之前切换到指定的数据源
     * @param joinPoint
     */
    @Before(value = "pointcut()")
    public void beforeOpt(JoinPoint joinPoint) {
        /*因为是对注解进行切面,所以这边无需做过多判定,直接获取注解的值,进行环绕,将数据源设置成远方,然后结束后,清楚当前线程数据源*/
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        SwitchSource switchSource = method.getAnnotation(SwitchSource.class);
        log.info("[Switch DataSource]:" + switchSource.value());
        DataSourceHolder.setDataSource(switchSource.value());
    }

    /**
     * 方法执行之后清除掉ThreadLocal中存储的KEY,这样动态数据源会使用默认的数据源
     */
    @After(value = "pointcut()")
    public void afterOpt() {
        DataSourceHolder.clearDataSource();
        log.info("[Switch Default DataSource]");
    }
}

效果展示

    @RequestMapping(value = "getSysUser",method = RequestMethod.GET)
    @Transactional(rollbackFor = Exception.class)
    public List<JSONObject> getSysUser(){
        List<JSONObject> sysUser = dsTestMapper.getSysUserList();
        return sysUser;
    }

    @SwitchSource("redDataSource")
    @RequestMapping(value = "getWxUser",method = RequestMethod.GET)
    @Transactional(rollbackFor = Exception.class)
    public List<JSONObject> getWxUser(){
        List<JSONObject> sysUser = dsTestMapper.getWxUser();
        return sysUser;
    }

    @SwitchSource("publicwebDataSource")
    @RequestMapping(value = "getPublicDoc",method = RequestMethod.GET)
    @Transactional(rollbackFor = Exception.class)
    public List<JSONObject> getPublicDoc(){
        List<JSONObject> sysUser = dsTestMapper.getPublicDoc();
        return sysUser;
    }

在这里插入图片描述在这里插入图片描述

问题总结

需要注意的一点是因为切换数据源是通过注解加切面实现的,所以就会出现以下问题:
A类中有m方法使用m数据源,n方法使用n数据源,同时在A类中有方法k来调用m,n方法,此时发现切换数据源是不生效的,都是使用的k方法的数据源,解决方法就是把k方法放到m和n所在类以外的其他类中,此类问题在使用注解加切面的编程中都会存在,例如SpringCache的使用。

参考来源

微信公众号:程序员DD,干货很多,此篇大多参考 link这边文章,并经过实践后,总结的使用方法,里面有很多原理、源码,可以详读。但是原博文里面缺少了一部分代码,就是创建DynamicDataSourceBean的那部分,没有那部分会导致项目启动报错

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值