springboot 多数据源切换

markdown 复制代码
# Spring Boot 多数据源手动切换实现指南

在实际项目开发中,经常会遇到需要连接多个数据库的场景,例如:
- 读写分离(主库写,从库读)
- 不同业务模块使用不同数据库
- 数据迁移、报表系统独立数据库

本文将详细介绍如何在 Spring Boot 项目中实现 **多数据源的手动切换**,并通过 `DynamicDataSource` + `ThreadLocal` 的方式灵活控制数据源路由。

---

## 🎯 一、核心目标

- 支持配置多个数据源(如 `master`、`slave`)
- 在运行时 **手动指定** 使用哪个数据源
- 切换过程对业务透明,不影响原有 JDBC/MyBatis 调用方式
- 线程安全,避免数据源错乱

---

## 🔧 二、技术选型

| 技术 | 说明 |
|------|------|
| `AbstractRoutingDataSource` | Spring 提供的抽象类,支持动态路由数据源 |
| `ThreadLocal` | 保证每个线程持有独立的数据源标识 |
| `DataSourceContextHolder` | 自定义上下文工具类 |
| `DynamicDataSource` | 继承 `AbstractRoutingDataSource`,实现动态路由逻辑 |

---

## 📦 三、项目结构

src/ ├── main/ │ ├── java/ │ │ └── com/example/datasource/ │ │ ├── config/ # 配置类 │ │ │ ├── DataSourceConfig.java │ │ │ └── DynamicDataSource.java │ │ ├── context/ # 上下文工具 │ │ │ └── DataSourceContextHolder.java │ │ ├── service/ │ │ │ └── UserService.java # 业务示例 │ │ └── Application.java │ └── resources/ │ └── application.yml

yaml 复制代码
---

## 🛠️ 四、详细实现步骤

### 1. 配置文件 `application.yml`

```yaml
spring:
  datasource:
    master:
      url: jdbc:mysql://localhost:3306/db_master?useSSL=false&serverTimezone=UTC
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver
    slave:
      url: jdbc:mysql://localhost:3306/db_slave?useSSL=false&serverTimezone=UTC
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver

2. 数据源上下文持有者(DataSourceContextHolder

使用 ThreadLocal 存储当前线程的数据源标识。

java 复制代码
public class DataSourceContextHolder {
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    /**
     * 设置当前线程使用的数据源
     */
    public static void setDataSource(String dataSourceKey) {
        contextHolder.set(dataSourceKey);
    }

    /**
     * 获取当前线程的数据源
     */
    public static String getDataSource() {
        return contextHolder.get();
    }

    /**
     * 清除当前数据源(防止线程复用导致污染)
     */
    public static void clear() {
        contextHolder.remove();
    }
}

⚠️ 必须在 finally 块中调用 clear(),避免内存泄漏和线程污染。


3. 动态数据源类(DynamicDataSource

继承 AbstractRoutingDataSource,重写 determineCurrentLookupKey() 方法。

java 复制代码
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

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

determineCurrentLookupKey() 是 Spring 在每次获取连接时自动调用的方法,返回值用于查找目标数据源。


4. 数据源配置类(DataSourceConfig

注册多个数据源,并将 DynamicDataSource 作为主数据源。

java 复制代码
@Configuration
public class DataSourceConfig {

    @Bean
    @ConfigurationProperties("spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @Primary
    public DataSource dynamicDataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();

        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("master", masterDataSource());
        targetDataSources.put("slave", slaveDataSource());

        dynamicDataSource.setTargetDataSources(targetDataSources);
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource()); // 默认数据源
        return dynamicDataSource;
    }

    @Bean
    public JdbcTemplate jdbcTemplate(@Qualifier("dynamicDataSource") DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}

5. 业务层手动切换示例

java 复制代码
@Service
public class UserService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public List<Map<String, Object>> getUsersFromMaster() {
        DataSourceContextHolder.setDataSource("master");
        try {
            return jdbcTemplate.queryForList("SELECT * FROM users");
        } finally {
            DataSourceContextHolder.clear(); // 必须清理
        }
    }

    public List<Map<String, Object>> getUsersFromSlave() {
        DataSourceContextHolder.setDataSource("slave");
        try {
            return jdbcTemplate.queryForList("SELECT * FROM users");
        } finally {
            DataSourceContextHolder.clear(); // 必须清理
        }
    }
}

🔍 五、核心原理剖析

1. 联动机制流程

scss 复制代码
[业务代码]
   ↓
DataSourceContextHolder.setDataSource("slave")
   ↓
JdbcTemplate.query(...) → 获取连接
   ↓
AbstractRoutingDataSource.getConnection()
   ↓
determineTargetDataSource()
   ↓
lookupKey = determineCurrentLookupKey() → 返回 "slave"
   ↓
dataSource = resolvedDataSources.get(lookupKey) → 从 Map 中查找
   ↓
return dataSource.getConnection()

2. 关键源码解析(AbstractRoutingDataSource

java 复制代码
protected DataSource determineTargetDataSource() {
    Object lookupKey = determineCurrentLookupKey(); // 调用我们重写的方法
    DataSource dataSource = this.resolvedDataSources.get(lookupKey); // 根据 key 查找
    if (dataSource == null && this.lenientFallback) {
        dataSource = this.resolvedDefaultDataSource;
    }
    return dataSource;
}

"根据 key 查找数据源"的逻辑由 Spring 框架自动实现,开发者无需手动编码。


⚠️ 六、注意事项

  1. 必须清理 ThreadLocal

    每次使用完必须调用 DataSourceContextHolder.clear(),防止线程池中线程复用导致数据源错乱。

  2. 事务管理器需指向 DynamicDataSource

    如果使用 @Transactional,确保 PlatformTransactionManager 使用的是动态数据源。

  3. 不建议用于分布式事务

    多数据源涉及跨库事务时,应使用 Seata、XA 等分布式事务方案。

  4. 避免在异步线程中切换失效
    ThreadLocal 不会自动传递到子线程,异步场景需手动传递或使用 InheritableThreadLocal


🧩 七、适用场景

场景 说明
读写分离 主库写,从库读,提升性能
多租户系统 不同租户使用不同数据库
报表系统 独立数据库避免影响主业务
数据迁移 新旧系统并行访问

📚 八、扩展建议

  • 结合 AOP 实现基于注解的自动切换(如 @DataSource("slave")
  • 使用 MyBatis 时,只需将 SqlSessionFactory 指向 dynamicDataSource
  • 支持更多数据源:只需在 targetDataSources 中添加即可

🏁 九、总结

通过 DynamicDataSource + ThreadLocal 的组合,我们实现了 Spring Boot 项目中多数据源的 手动、灵活、线程安全的切换机制

核心要点:

  • DataSourceContextHolder 提供上下文
  • determineCurrentLookupKey() 返回当前数据源 key
  • AbstractRoutingDataSource 自动完成路由查找
  • 业务代码中手动设置并清理上下文

这种方式简单、可控,非常适合需要精确控制数据源的场景。

相关推荐
序安InToo16 分钟前
第6课|注释与代码风格
后端·操作系统·嵌入式
xyy12316 分钟前
C#: Newtonsoft.Json 到 System.Text.Json 迁移避坑指南
后端
洋洋技术笔记18 分钟前
Spring Boot Web MVC配置详解
spring boot·后端
JxWang0519 分钟前
VS Code 配置 Markdown 环境
后端
navms22 分钟前
搞懂线程池,先把 Worker 机制啃明白
后端
JxWang0522 分钟前
离线数仓的优化及重构
后端
Nyarlathotep011323 分钟前
gin01:初探gin的启动
后端·go
JxWang0524 分钟前
安卓手机配置通用多屏协同及自动化脚本
后端
JxWang0525 分钟前
Windows Terminal 配置 oh-my-posh
后端
SimonKing41 分钟前
OpenCode AI编程助手如何添加Skills,优化项目!
java·后端·程序员