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 自动完成路由查找
  • 业务代码中手动设置并清理上下文

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

相关推荐
绝无仅有3 小时前
mysql性能优化实战与总结
后端·面试·github
用户8356290780513 小时前
从手动编辑到代码生成:Python 助你高效创建 Word 文档
后端·python
德育处主任3 小时前
玩转 Strands:AI Agent 开发,原来可以这么简单!
后端·aigc
Undoom3 小时前
大模型选型“炼狱”与终结:一份来自普通开发者的AI Ping深度评测报告
后端
用户4099322502123 小时前
FastAPI的CI流水线怎么自动测端点,还能让Allure报告美到犯规?
后端·ai编程·trae
双向333 小时前
Docker 镜像瘦身实战:从 1.2GB 压缩到 200MB 的优化过程
后端
Cyan_RA93 小时前
计算机网络面试题 — TCP连接如何确保可靠性?
前端·后端·面试
BingoGo4 小时前
PHP-FPM 深度调优指南 告别 502 错误,让你的 PHP 应用飞起来
后端·php
CoovallyAIHub4 小时前
微软发布 Visual Studio 2026 Insider:AI深度集成,性能大提升,让开发效率倍增(附下载地址)
后端·编程语言·visual studio