使用 Spring Boot + AbstractRoutingDataSource 实现动态切换数据源

1. 动态切换数据源的原理

AbstractRoutingDataSource 是 Spring 提供的一个抽象类,它通过实现 determineCurrentLookupKey 方法,根据上下文信息决定当前使用的数据源。核心流程如下:

  • 定义多数据源配置:注册多个数据源。
  • 实现动态数据源路由:继承 AbstractRoutingDataSource,根据上下文返回数据源标识。
  • 使用拦截器设置上下文:在请求中设置当前使用的数据源。

 2. 实现步骤

2.1 确保你的 pom.xml 中已经包含如下依赖:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

 

2.2 继承自 Spring 提供的抽象类 AbstractRoutingDataSource
package com.imooc.cloud.springboot;

import com.imooc.cloud.dynamic.raw.DataSourceContext;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import java.util.Map;

public class SpringDynamicDataSource extends AbstractRoutingDataSource {
    public SpringDynamicDataSource(Map<Object, Object> targetDataSources) {
        super.setTargetDataSources(targetDataSources);
    }
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContext.getCurrentDb();
    }
}

类定义

public class SpringDynamicDataSource extends AbstractRoutingDataSource {
}
  • 继承自 Spring 提供的抽象类 AbstractRoutingDataSource
  • 是实现多数据源切换的核心类。

构造函数

public SpringDynamicDataSource(Map<Object, Object> targetDataSources) {
    super.setTargetDataSources(targetDataSources);
}
  • 通过构造器传入多个目标数据源(通常是 Map<标识符, DataSource> 形式)。
  • 调用父类方法设置这些数据源。

核心方法:determineCurrentLookupKey()

@Override
protected Object determineCurrentLookupKey() {
    return DataSourceContext.getCurrentDb();
}
  • Spring 框架会在每次数据库操作时调用这个方法。
  • 返回当前线程使用的数据源标识(如 "master""slave1")。
  • 实际上是从 ThreadLocal 中获取当前线程绑定的数据源名称。

2.3 数据源上下文工具类 
public class DataSourceContext {
    private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();

    public static void setCurrentDb(String db) {
        CONTEXT.set(db);
    }

    public static String getCurrentDb() {
        return CONTEXT.get();
    }

    public static void removeCurrentDb() {
        CONTEXT.remove();
    }
}

用于保存和清除当前线程使用的数据源标识。


2.4 将多数据源注入并创建 SpringDynamicDataSource
package com.imooc.cloud.springboot;

import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;

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

public class SpringDataSourceConfiguration {
    @Bean
    public DataSource mybatisPlusDataSource() {
        return DataSourceBuilder.create().
                driverClassName("com.mysql.jdbc.Driver").
                url("jdbc:mysql://192.168.3.150:3306/mybatisplus?characterEncoding=utf8").
                username("root").
                password("123456").build();
    }

    @Bean
    public DataSource mybatisExampleDataSource() {
        return DataSourceBuilder.create().
                driverClassName("com.mysql.jdbc.Driver").
                url("jdbc:mysql://192.168.3.150:3306/mybatis-example?characterEncoding=utf8").
                username("root").
                password("123456").build();
    }

    @Primary
    @Bean
    public SpringDynamicDataSource springDynamicDataSource() {
        Map<Object, Object> targetDataSources = new HashMap<>();
        DataSource mybatisPlusDataSource = mybatisPlusDataSource();
        DataSource mybatisExampleDataSource = mybatisExampleDataSource();
        targetDataSources.put("mybatisPlus", mybatisPlusDataSource);
        targetDataSources.put("mybatisExample", mybatisExampleDataSource);
        return new SpringDynamicDataSource(targetDataSources);
    }
}
2.5 安全地保存和切换当前线程使用的数据源

在多线程环境下,安全地保存和切换当前线程使用的数据源标识(如 "master""slave1" 等),支持嵌套调用(例如在事务中嵌套切换数据源),并且使用 双端队列(Deque)模拟栈结构 来管理数据源切换的上下文。 

  • 使用 ThreadLocal 保存每个线程独立的 数据源栈(Deque)
  • 使用 NamedThreadLocal 有助于在调试或日志中识别该线程局部变量的用途。
  • ArrayDeque 是一个双端队列,这里用作栈(LIFO),实现嵌套切换数据源的功能。
package com.imooc.cloud.util;

import org.springframework.core.NamedThreadLocal;
import org.springframework.util.StringUtils;

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Objects;

public final class DynamicDataSourceContextHolder {
    /**
     * 双端队列其实本质就是一个栈
     */
    private static final ThreadLocal<Deque<String>> DATASOURCE_CONTEXT = NamedThreadLocal.
            withInitial(() -> new ArrayDeque<>());

    private DynamicDataSourceContextHolder() {
        if (DATASOURCE_CONTEXT != null) {
            throw new RuntimeException("禁止反射创建");
        }
    }

    public static String getCurrentDataSource() {
        //todo 2023-07-31 修复补丁。因为可能返回null,而ConcurrentHashMap的get方法不能传入null,否则报空指针
        String peek = DATASOURCE_CONTEXT.get().peek();
        return Objects.isNull(peek) ? "" : peek;
    }

    public static String addDataSource(String dds) {
        String datasource = StringUtils.isEmpty(dds) ? "" : dds;
        DATASOURCE_CONTEXT.get().push(datasource);
        return datasource;
    }

    public static void removeCurrentDataSource() {
        Deque<String> deque = DATASOURCE_CONTEXT.get();
        deque.poll();
        if (deque.isEmpty()) {
            DATASOURCE_CONTEXT.remove();
        }
    }
}
单例构造限制
private DynamicDataSourceContextHolder() {
    if (DATASOURCE_CONTEXT != null) {
        throw new RuntimeException("禁止反射创建");
    }
}
  • 私有构造方法,防止外部实例化。
  • 添加了反射创建检测,防止通过反射破坏单例。
获取当前数据源
public static String getCurrentDataSource() {
    String peek = DATASOURCE_CONTEXT.get().peek();
    return Objects.isNull(peek) ? "" : peek;
}
  • 从当前线程的数据源栈中获取当前使用的数据源标识。
  • 如果栈为空,返回空字符串 "",避免后续操作(如 Map.get(null))导致空指针异常。
设置新数据源(入栈)
public static String addDataSource(String dds) {
    String datasource = StringUtils.isEmpty(dds) ? "" : dds;
    DATASOURCE_CONTEXT.get().push(datasource);
    return datasource;
}
  • 将指定的数据源标识压入栈顶。
  • 支持嵌套切换数据源(例如 AOP + 事务中嵌套注解切换)。
  • 如果传入 null 或空字符串,则使用默认空字符串。
 移除当前数据源(出栈)
public static void removeCurrentDataSource() {
    Deque<String> deque = DATASOURCE_CONTEXT.get();
    deque.poll();
    if (deque.isEmpty()) {
        DATASOURCE_CONTEXT.remove();
    }
}
  • 从栈中弹出一个数据源标识(LIFO)。
  • 如果栈为空,则清除整个线程局部变量,防止内存泄漏。

这个工具类通常用于配合 动态数据源路由类(如 AbstractRoutingDataSource)一起使用,实现多数据源切换。例如:

1. 动态数据源路由类(简化版) 

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

2. AOP 切面控制数据源切换

@Aspect
@Component
public class DataSourceAspect {

    @Before("@annotation(ds))")
    public void beforeSwitchDS(JoinPoint point, DynamicDataSource ds) {
        DynamicDataSourceContextHolder.addDataSource(ds.db());
    }

    @After("@annotation(ds))")
    public void afterSwitchDS(JoinPoint point, DynamicDataSource ds) {
        DynamicDataSourceContextHolder.removeCurrentDataSource();
    }
}

3. 注解定义

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DynamicDataSource {
    String db() default "master";
}

4. Service 使用示例

@Service
public class UserService {

    @DynamicDataSource("slave1")
    public List<User> queryFromSlave() {
        return userMapper.selectAll();
    }

    public void insertUser(User user) {
        userMapper.insert(user);
    }
}


3. 测试

package com.imooc.cloud;

import com.imooc.cloud.util.DynamicDataSourceContextHolder;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;

import java.util.List;

@SpringBootTest
public class SpringDynamicTest {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Test
    public void testQueryUser() {
        DynamicDataSourceContextHolder.addDataSource("mybatisPlus");
        List list = jdbcTemplate.queryForList("select * from user");
        System.out.println("list: "+list);
    }

    @Test
    public void testQueryOrder() {
        DynamicDataSourceContextHolder.addDataSource("mybatisExample");
        List list = jdbcTemplate.queryForList("select * from `user`");
        System.out.println("list: "+list);
    }
}

4. 完整使用流程图

+-----------------+
| @DynamicDataSource("slave1") |
+-----------------+
        ↓
+----------------------+
| AOP Before Advice    |
| DynamicDataSourceContextHolder.addDataSource("slave1") |
+----------------------+
        ↓
+----------------------+
| AbstractRoutingDataSource.determineCurrentLookupKey() |
| return DynamicDataSourceContextHolder.getCurrentDataSource() |
+----------------------+
        ↓
+----------------------+
| JDBC / MyBatis 使用对应数据源执行 SQL |
+----------------------+
        ↓
+----------------------+
| AOP After Advice     |
| DynamicDataSourceContextHolder.removeCurrentDataSource() |
+----------------------+

5. 推荐使用 dynamic-datasource-spring-boot-starter

新项目,强烈建议使用开源组件来简化多数据源配置:

1. 引入依赖
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
    <version>4.2.0</version>
</dependency>
2. 配置文件(application.yml)
spring:
  datasource:
    dynamic:
      primary: master
      datasource:
        master:
          url: jdbc:mysql://localhost:3306/master
          username: root
          password: root
        slave1:
          url: jdbc:mysql://localhost:3306/slave1
          username: root
          password: root
3. 使用注解
@DS("slave1")
public List<User> queryFromSlave() {
    return userMapper.selectList(null);
}

🎯 总结

功能说明
DynamicDataSourceContextHolder数据源上下文管理工具
Deque<String>支持嵌套切换
ThreadLocal线程隔离
AOP + 注解实现优雅的数据源切换
dynamic-datasource-spring-boot-starter推荐使用的封装库
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Brilliant Nemo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值