SpringBoot實現動態數據源配置

2024年2月6日 22点热度 0人点赞

場景描述:

前一陣子接手的新項目中需要使用2個數據源。

一個叫行雲數據庫,一個叫OceanBase數據庫。

就是說,我有時候查詢要查行雲的數據,有時候查詢要查 OceanBase 的數據,咋辦?

廢話不多說, 下面以mysql為例,開整。

一、環境依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.3.0</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.28</version>
</dependency>

二、實現思路

在進行下一步之前,我們必須要知道 SpringBoot 自動配置的相關原理,因為之前,我們大多是單數據源。

# 配置mysql數據庫
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/blog?serverTimezone=Asia/Shanghai&allowMultiQueries=true
    username: root
    password: root

從前隻是會用,會配置,甚至都是別人給配置的自己直接拿來用。

但是要搞懂動態數據源就必須要先搞懂自動配置

0.o?讓我看看怎麼個事兒之SpringBoot自動配置

現在我們要實現多數據源,並且可以自動切換

也就是我 A 查詢連接的是行雲數據庫。

而我 B 查詢卻連接的是 OceanBase 數據庫。

怎麼辦?

那第一個肯定就不能再使用
DataSourceAutoConfigurtation了。

我直接反手一個 exclude 。

@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})

然後呢?

Spring boot想得很周到,它提供了AbstractRoutingDataSource 抽象類。

這個類能根據用戶定義的規則選擇當前的數據源


有同學要問了:

AbstractRoutingDataSource 是什麼東西?

AbstractRoutingDataSource 是一個抽象類。

它繼承了 AbstractDataSource 抽象類。

而 AbstractDataSource 實現了 DataSource 接口。

也就是說:AbstractRoutingDataSource 他實際上就是一個DataSource 。

AbstractRoutingDataSource 中有兩個關鍵方法。

setTargetDataSources(Map<Object, Object> targetDataSources)

第一個方法見名知意,設置目標數據源(復數也就是多個)。

protected abstract Object determineCurrentLookupKey();

第二個方法是僅有的一個抽象方法,需要開發者具體實現,也可以見名知意,決定當前使用(目標數據源中的)哪個

我們要做的是什麼?

我們準備 2 個數據源,全部配置好放進 Map<Object, Object> targetDataSources 裡備用。

我們繼承 AbstractRoutingDataSource 並實現抽象方法 determineCurrentLookupKey() 。

當我們繼承AbstractRoutingDataSource時我們自身也是一個數據源。

對於數據源必然有連接數據庫的動作。

隻是AbstractRoutingDataSource的getConnection()方法裡實際是調用determineTargetDataSource()返回的數據源的getConnection()方法。

這樣我們可以在執行查詢之前,設置使用的數據源。

實現可動態路由的數據源,在每次數據庫查詢操作前執行。

它的抽象方法 determineCurrentLookupKey() 決定使用哪個數據源。

我知道肯定有人看不懂要上嘴臉了:

Talk is cheap, show me the FUCKING code !!!

2.1 配置文件

spring:
  datasource:
    dynamic:
      db1:
        driverClassName: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/db1?serverTimezone=Asia/Shanghai&allowMultiQueries=true
        username: root
        password: root
      db2:
        driverClassName: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/db2?serverTimezone=Asia/Shanghai&allowMultiQueries=true
        username: root
        password: root

2.2 自定義動態數據源

DynamicDataSource繼承AbstractRoutingDataSource。

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSource();
    }
}

這裡的determineCurrentLookupKey方法,需要返回一個數據源。

又有同學問了:


DynamicDataSourceContextHolder 又是什麼東西?

public class DynamicDataSourceContextHolder {
    public static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
    public static String getDataSource() {
        return CONTEXT_HOLDER.get();
    }
    public static void setDataSource(String dataSource) {
        CONTEXT_HOLDER.set(dataSource);
    }
    public static void clearDataSource() {
        CONTEXT_HOLDER.remove();
    }
}

看到 Context 應該很熟悉了,跟程序上下文有關。

它的作用就是你查詢數據庫的時候用哪個數據源,就 setDataSource 哪個。

還有點懵?沒事,繼續往下看。

@Configuration
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
public class DynamicDataSourceConfig {
    @Bean("db1")
    @ConfigurationProperties(prefix = "spring.datasource.dynamic.db1")
    public DataSource db1() {
        return DataSourceBuilder.create().build();
    }
    @Bean("db2")
    @ConfigurationProperties(prefix = "spring.datasource.dynamic.db2")
    public DataSource db2() {
        return DataSourceBuilder.create().build();
    }
    @Bean
    @Primary
    public DataSource dataSource() {
        Map<Object, Object> dataSourceMap = new HashMap<>(2);
        dataSourceMap.put("db1", db1());
        dataSourceMap.put("db2", db2());
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        // 需要設置的多數據源
        dynamicDataSource.setTargetDataSources(dataSourceMap);
        // 主數據源/默認數據源
        dynamicDataSource.setDefaultTargetDataSource(db1());
        return dynamicDataSource;
    }
}

這是比較常見的自定義數據源配置了。

可以看到一共註冊了3個數據源。

但是最後一個DynamicDataSource有 @Primary 註解,它表明這個數據源優先級更高。

DynamicDataSource中設置了dataSourceMap,也就是保存了 db1 和 db2。

以上我們動態數據源配置的工作就做完了。

我們以實際查詢中的操作完整捋一遍這當中到底發生了什麼!

@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;
    /**
     *使用db2數據源
     */
    public void saveUser(UserDto userDto) {
        DynamicDatasourceHolder.setDataSource("db2");
        User user = new User();
        user.setUName(userDto.getName());
        userMapper.insert(user);
        DynamicDatasourceHolder.removeDataSource("db2");
    }
}

首先,DynamicDatasourceHolder 設置了數據源 db2 。

CONTEXT_HOLDER 中就保存了一個 “db2” 字符串。

userMapper 進行數據庫操作之前,MyBatis 框架替我們做了一些事。

其中一件事是獲取數據庫連接

MyBatis 就在想:我得找個 DataSource ,因為DataSource 有getConnection() 方法。

誰是 DataSource ?

繼承了 AbstractRoutingDataSource 的 DynamicDataSource 大聲喊到:我是 !

開始連接數據庫!

@Override
public Connection getConnection() throws SQLException {
	return determineTargetDataSource().getConnection();
}

連接哪個?

protected DataSource determineTargetDataSource() {
    // 哥,看這一行!
   Object lookupKey = determineCurrentLookupKey();
   DataSource dataSource = this.resolvedDataSources.get(lookupKey);
   return dataSource;
}

連接這個!

@Override
protected Object determineCurrentLookupKey() {
    return DynamicDataSourceContextHolder.getDataSource();
}

連接完成!

insert 完成!

removeDataSource("db2") !

每次這樣用嫌麻煩?

辦他!

三、動態數據源註解@DS

看這部分之前需要一些前置知識點:

Java註解,看完就會用

師爺,翻譯翻譯什麼叫AOP

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface DS {
    String value() default "db1";
}
@Component
@Aspect
public class DynamicDataSourceAspect {
    @Pointcut("@annotation(com.example.demo.annotation.DS)")
    public void dynamicDataSourcePointCut() {
    }
    @Around("dynamicDataSourcePointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        String dataSourceKey = "db1";
        // 類上的註解
        Class<?> aClass = joinPoint.getTarget().getClass();
        DS annotation = aClass.getAnnotation(DS.class);
        // 方法上的註解
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        DS annotationMethod = signature.getMethod().getAnnotation(DS.class);
        if (Objects.nonNull(annotationMethod)) {
            dataSourceKey = annotationMethod.value();
        } else {
            dataSourceKey = annotation.value();
        }
        // 設置數據源
        DynamicDataSourceContextHolder.setDataSource(dataSourceKey);
        try {
             return joinPoint.proceed();
        }finally {
            DynamicDataSourceContextHolder.clearDataSource();
        }
    }
}