AbstractRoutingDataSource 介绍

AbstractRoutingDataSource 是 Spring 提供的一个抽象类,它可以让我们动态切换数据源。在使用 AbstractRoutingDataSource 时,我们需要实现它的 determineCurrentLookupKey() 方法,该方法返回当前线程使用的数据源的名称或标识符。

AbstractRoutingDataSource 两个重要的成员变量:

  1. defaultTargetDataSource:默认数据源,即当无法确定使用哪个数据源时,将使用该默认数据源。在AbstractRoutingDataSource中,我们可以通过调用setDefaultTargetDataSource()方法来设置默认数据源。
  2. targetDataSources:一个Map类型的成员变量,其中保存了多个数据源实例。在AbstractRoutingDataSource中,我们可以通过调用setTargetDataSources()方法来设置多个数据源实例,其中Map的key为数据源标识符或名称,value为对应的数据源实例。

使用AbstractRoutingDataSource实现数据源切换的原理如下:

  1. 在应用启动时,我们需要配置多个数据源,并将它们注册到AbstractRoutingDataSource中。
  2. 当应用发起数据库操作时,AbstractRoutingDataSource会根据determineCurrentLookupKey()方法返回的值,查找对应的数据源。
  3. 在determineCurrentLookupKey()方法中,我们可以根据不同的策略来决定使用哪个数据源。例如,可以根据当前请求的用户信息、请求的URL、当前时间等来选择不同的数据源。

简单示例

有两个数据库datasource01、datasource02,分别有一张表user,

在 yml 中配置两个数据源 datasource01、datasource02

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    datasource1:
      url: jdbc:mysql://localhost:3306/datasource01?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
      driver-class-name: com.mysql.cj.jdbc.Driver
      username: root
      password: 123456
    datasource2:
      url: jdbc:mysql://localhost:3306/datasource02?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
      driver-class-name: com.mysql.cj.jdbc.Driver
      username: root
      password: 123456

将这两个 DataSource 加载到 Spring IOC容器中

package cc.oolo.imgcdn.config;


import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@Configuration
public class DataSourceConfig {
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.datasource1")
    public DataSource dataSource1() {
        // 底层会自动拿到spring.datasource.datasource1中的配置,创建一个DruidDataSource
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.datasource2")
    public DataSource dataSource2() {
        return DruidDataSourceBuilder.create().build();
    }
}

定义 DynamicDataSource 并且添加 @Component和@Primary注解,并且继承 AbstractRoutingDataSource,afterPropertiesSet为初始化方法,在 Spring Bean 生命周期初始化阶段就会执行该方法,在这个方法里面,将数据源加载到 targetDataSources 和 defaultTargetDataSource

package cc.oolo.imgcdn.config;

import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@Component
@Primary
public class DynamicDataSource extends AbstractRoutingDataSource {

    @Resource
    private DataSource dataSource1;

    @Resource
    private DataSource dataSource2;


    @Override
    protected Object determineCurrentLookupKey() {
        return "d1";
    }

    @Override
    public void afterPropertiesSet() {
        // 为targetDataSource初始化所有数据源
        Map<Object,Object> targetDataSources = new HashMap<>();
        targetDataSources.put("d1",dataSource1);
        targetDataSources.put("d2",dataSource2);
        super.setTargetDataSources(targetDataSources);
        // 为defaultTargetDatasource设置默认数据源
        super.setDefaultTargetDataSource(dataSource1);
        super.afterPropertiesSet();
    }
}

然后当需要获取连接的时候,就会调用 getConnection() 方法,在这个方法里面会调用 determineTargetDataSourcea().getConnection(),获取连接然后返回,determineTargetDataSource() 方法根据我们返回的值,从 resolvedDataSources 获取对应的数据源返回。

至于以上为什么不直接从 targetDataSources 获取,因为在初始化的时候,AbstractRoutingDataSource 会将 targetDataSources 进行处理和转换后,赋值给 resolvedDataSources

以上我们添加了"d1"和"d2"数据源,在选择数据源的时候,只需要在 determineCurrentLookupKey() 方法返回该数据源所对应的key即可

使用ThreadLocal动态控制

上面这个例子需要我们手动去修改 determineCurrentLookupKey 方法的返回值,没有办法根据我们的业务,动态切换数据源。

在这里可以引入ThreadLocal,使用ThreadLocal存储需要使用的数据源的key,需要修改的时候直接修改ThreadLocal即可,只需要在 DynamicDataSource 中创建一个 ThreadLocal,并且将determineCurrentLookupKey 方法的返回值修改为 threadLocal.get() 即可

package cc.oolo.imgcdn.config;

import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;

@Component
@Primary
public class DynamicDataSource extends AbstractRoutingDataSource {

    public static ThreadLocal<String> name = new ThreadLocal<>();

    @Resource
    private DataSource dataSource1;

    @Resource
    private DataSource dataSource2;


    @Override
    protected Object determineCurrentLookupKey() {
        return name.get();
    }

    @Override
    public void afterPropertiesSet() {
        // 为targetDataSource初始化所有数据源
        Map<Object,Object> targetDataSources = new HashMap<>();
        targetDataSources.put("d1",dataSource1);
        targetDataSources.put("d2",dataSource2);
        super.setTargetDataSources(targetDataSources);
        // 为defaultTargetDatasource设置默认数据源
        super.setDefaultTargetDataSource(dataSource1);
        super.afterPropertiesSet();
    }
}

测试:

    @Test
    public void test1(){
        DynamicDataSource.name.set("d1");
        System.out.println(userService.getUser());
        DynamicDataSource.name.set("d2");
        System.out.println(userService.getUser());
    }

结果:

2023-07-20 17:56:04.353  INFO 21600 --- [           main] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} inited
UserEntity(id=1, name=数据源1, age=18, gender=男, email=1@qq.com)
2023-07-20 17:56:04.529  INFO 21600 --- [           main] com.alibaba.druid.pool.DruidDataSource   : {dataSource-2} inited
UserEntity(id=1, name=数据源2, age=120, gender=女, email=2@qq.com)

使用 自定义注解 + AOP + ThreadLocal 切换数据源

自定义注解 DataSourceSwitch,注解可以标识在类或者方法上面

package cc.oolo.imgcdn.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSourceSwitch {
    String value() default "d1";
}

定义 Aspect,拦截 DataSourceSwitch 注解和 cc.oolo.imgcdn.controller、cc.oolo.imgcdn.service.impl

包下的所有类和方法

package cc.oolo.imgcdn.aspect;

import cc.oolo.imgcdn.annotation.DataSourceSwitch;
import cc.oolo.imgcdn.config.DynamicDataSource;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;

@Component
@Aspect
public class DynamicDataSourceAspect {
    @Pointcut("within(cc.oolo.imgcdn.controller..*) || within(cc.oolo.imgcdn.service.impl..*) || (@annotation(cc.oolo.imgcdn.annotation.DataSourceSwitch))")
    public void pointCut(){
    }

    @Around("pointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取目标类
        Class<?> targetClass = joinPoint.getTarget().getClass();
        // 获取目标方法
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        // 获取类上的注解
        DataSourceSwitch classAnnotation = targetClass.getAnnotation(DataSourceSwitch.class);
        // 获取方法上的注解
        DataSourceSwitch methodAnnotation = method.getAnnotation(DataSourceSwitch.class);
        // 如果类上有注解,以类上的注解为准
        if (classAnnotation != null){
            // 根据注解的value设置ThreadLocal的值
            DynamicDataSource.name.set(classAnnotation.value());
            System.out.println("选择的数据源:"+ classAnnotation.value());
        }
        // 如果方法上有注解,以方法上的注解为准
        if (methodAnnotation != null){
            // 根据注解的value设置ThreadLocal的值
            DynamicDataSource.name.set(methodAnnotation.value());
            System.out.println("选择的数据源:"+ methodAnnotation.value());
        }
        return joinPoint.proceed();
    }
}

测试:

package cc.oolo.imgcdn.controller;


import cc.oolo.imgcdn.annotation.DataSourceSwitch;
import cc.oolo.imgcdn.service.UserService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
public class UserController {
    @Resource
    private UserService userService;

    @GetMapping("getUser")
    @DataSourceSwitch("d2")
    public Object getUser(){
        return userService.getUser();
    }
}

结果:

{
    "id": "1",
    "name": "数据源2",
    "age": 120,
    "gender": "女",
    "email": "2@qq.com"
}

注意:由于我们是通过AOP拦截,如果标注在类上,该类必须被Spring托管,如果标注在方法上,如果实现了接口,那么只能标注在实现了接口的抽象方法的方法上。

其他应用

  1. 在实现读写分离的时候,可以使用mybatis拦截器,拦截query和update方法,然后再根据自己的规则选择不同的数据源
  2. 如果是mysql集群,数据是分散到不同的数据库中,则可以使用AOP,根据数据的唯一值对数据库的数量取模,然后选择不同的数据源
最后修改:2023 年 07 月 20 日
如果觉得我的文章对你有用,请随意赞赏