在 Spring Boot 中配置多數據源是一個非常常見的需求,主要用于以下場景:
- 讀寫分離:一個主數據庫(Master)負責寫操作,一個或多個從數據庫(Slave)負責讀操作,以提高性能和可用性。
- 業務拆分:不同的業務模塊使用不同的數據庫(例如,用戶庫、訂單庫、商品庫)。
- 連接異構數據庫:同時連接 MySQL、PostgreSQL 等不同類型的數據庫。
下面我將詳細介紹兩種主流的實現方式:
- 靜態方式(推薦用于業務隔離場景):通過包路徑區分不同的數據源,配置簡單,結構清晰。
- 動態方式(推薦用于讀寫分離場景):使用 AOP 和自定義注解,在方法級別動態切換數據源,更靈活。
方案一:靜態方式(按包路徑隔離)
這種方式的核心思想是為每個數據源創建一套獨立的配置(DataSource
, SqlSessionFactory
, TransactionManager
),并使用 @MapperScan
注解掃描不同包路徑下的 Mapper 接口,將它們綁定到對應的數據源上。
1. 添加依賴 (pom.xml)
確保有以下依賴。通常 Spring Boot Starter 會包含大部分。
<dependencies><!-- Spring Boot Web Starter --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- MyBatis-Plus Starter --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.3.1</version> <!-- 請使用較新版本 --></dependency><!-- MySQL Driver --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><!-- Connection Pool (HikariCP is default) --><dependency><groupId>com.zaxxer</groupId><artifactId>HikariCP</artifactId></dependency>
</dependencies>
2. 配置文件 (application.yml)
為不同的數據源定義各自的連接信息,并用不同的前綴區分。
spring:datasource:# 主數據源配置 (master)master:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/db_master?serverTimezone=UTCusername: rootpassword: your_passwordtype: com.zaxxer.hikari.HikariDataSource # 指定連接池類型# 從數據源配置 (slave)slave:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3307/db_slave?serverTimezone=UTCusername: rootpassword: your_passwordtype: com.zaxxer.hikari.HikariDataSource
3. 創建數據源配置類
為每個數據源創建一個 Java 配置類。
主數據源配置 (MasterDataSourceConfig.java)
package com.example.config.datasource;import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import javax.sql.DataSource;@Configuration
// 掃描 Master 庫的 Mapper 接口
@MapperScan(basePackages = "com.example.mapper.master", sqlSessionTemplateRef = "masterSqlSessionTemplate")
public class MasterDataSourceConfig {@Bean(name = "masterDataSource")@ConfigurationProperties(prefix = "spring.datasource.master")@Primary // 標記為主數據源public DataSource masterDataSource() {return DataSourceBuilder.create().build();}@Bean(name = "masterSqlSessionFactory")@Primarypublic SqlSessionFactory masterSqlSessionFactory(@Qualifier("masterDataSource") DataSource dataSource) throws Exception {MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();bean.setDataSource(dataSource);// 如果有 XML 文件,指定位置// bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/master/*.xml"));return bean.getObject();}@Bean(name = "masterTransactionManager")@Primarypublic DataSourceTransactionManager masterTransactionManager(@Qualifier("masterDataSource") DataSource dataSource) {return new DataSourceTransactionManager(dataSource);}@Bean(name = "masterSqlSessionTemplate")@Primarypublic SqlSessionTemplate masterSqlSessionTemplate(@Qualifier("masterSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {return new SqlSessionTemplate(sqlSessionFactory);}
}
從數據源配置 (SlaveDataSourceConfig.java)
package com.example.config.datasource;import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import javax.sql.DataSource;@Configuration
// 掃描 Slave 庫的 Mapper 接口
@MapperScan(basePackages = "com.example.mapper.slave", sqlSessionTemplateRef = "slaveSqlSessionTemplate")
public class SlaveDataSourceConfig {@Bean(name = "slaveDataSource")@ConfigurationProperties(prefix = "spring.datasource.slave")public DataSource slaveDataSource() {return DataSourceBuilder.create().build();}@Bean(name = "slaveSqlSessionFactory")public SqlSessionFactory slaveSqlSessionFactory(@Qualifier("slaveDataSource") DataSource dataSource) throws Exception {MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();bean.setDataSource(dataSource);// bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/slave/*.xml"));return bean.getObject();}@Bean(name = "slaveTransactionManager")public DataSourceTransactionManager slaveTransactionManager(@Qualifier("slaveDataSource") DataSource dataSource) {return new DataSourceTransactionManager(dataSource);}@Bean(name = "slaveSqlSessionTemplate")public SqlSessionTemplate slaveSqlSessionTemplate(@Qualifier("slaveSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {return new SqlSessionTemplate(sqlSessionFactory);}
}
4. 創建 Mapper 接口
將不同數據源的 Mapper 接口放到各自的包下。
com.example.mapper.master
->UserMasterMapper.java
com.example.mapper.slave
->OrderSlaveMapper.java
5. 使用
現在你可以在 Service 中直接注入并使用對應的 Mapper,Spring 會自動為它們關聯正確的數據源。
@Service
public class MyService {@Autowiredprivate UserMasterMapper userMasterMapper; // 操作 master 庫@Autowiredprivate OrderSlaveMapper orderSlaveMapper; // 操作 slave 庫public void doSomething() {// ...userMasterMapper.insert(someUser); // 寫入主庫Order order = orderSlaveMapper.selectById(1); // 從從庫讀取}
}
優點:配置隔離,結構非常清晰,不會混淆。
缺點:如果一個 Service 方法需要同時操作兩個庫,代碼會稍微復雜,且默認的 @Transactional
不能跨數據源生效。
方案二:動態方式(AOP + 自定義注解)
這種方式更靈活,適用于讀寫分離等需要在同一個 Service 中切換數據源的場景。
1. 配置文件 (application.yml)
與方案一相同。
2. 創建自定義注解
創建一個注解,用于標記方法應該使用哪個數據源。
package com.example.config.dynamic;import java.lang.annotation.*;@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {String value() default "master"; // 默認使用 master 數據源
}
3. 創建數據源上下文持有者
使用 ThreadLocal
來存儲當前線程需要使用的數據源 Key。
package com.example.config.dynamic;public class DataSourceContextHolder {private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();public static void setDataSourceKey(String key) {CONTEXT_HOLDER.set(key);}public static String getDataSourceKey() {return CONTEXT_HOLDER.get();}public static void clearDataSourceKey() {CONTEXT_HOLDER.remove();}
}
4. 創建動態數據源類
繼承 AbstractRoutingDataSource
,重寫 determineCurrentLookupKey
方法,從 DataSourceContextHolder
獲取當前數據源 Key。
package com.example.config.dynamic;import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;public class DynamicDataSource extends AbstractRoutingDataSource {@Overrideprotected Object determineCurrentLookupKey() {return DataSourceContextHolder.getDataSourceKey();}
}
5. 創建AOP切面
創建一個切面,攔截 @DataSource
注解,在方法執行前設置數據源 Key,在方法執行后清除它。
package com.example.config.dynamic;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.core.annotation.Order;
import org.springframework.stereotype.Component;import java.lang.reflect.Method;@Aspect
@Component
@Order(1) // 保證該AOP在@Transactional之前執行
public class DataSourceAspect {@Pointcut("@annotation(com.example.config.dynamic.DataSource)")public void dsPointCut() {}@Around("dsPointCut()")public Object around(ProceedingJoinPoint point) throws Throwable {MethodSignature signature = (MethodSignature) point.getSignature();Method method = signature.getMethod();DataSource dataSource = method.getAnnotation(DataSource.class);// 設置數據源if (dataSource != null) {DataSourceContextHolder.setDataSourceKey(dataSource.value());}try {return point.proceed();} finally {// 清除數據源,防止內存泄漏DataSourceContextHolder.clearDataSourceKey();}}
}
6. 整合數據源配置
創建一個統一的配置類來管理所有數據源。
package com.example.config;import com.example.config.dynamic.DynamicDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;@Configuration
public class DynamicDataSourceConfig {@Bean(name = "masterDataSource")@ConfigurationProperties(prefix = "spring.datasource.master")public DataSource masterDataSource() {return DataSourceBuilder.create().build();}@Bean(name = "slaveDataSource")@ConfigurationProperties(prefix = "spring.datasource.slave")public DataSource slaveDataSource() {return DataSourceBuilder.create().build();}@Bean@Primary // 必須!將動態數據源設置為主數據源public DynamicDataSource dataSource(DataSource masterDataSource, DataSource slaveDataSource) {Map<Object, Object> targetDataSources = new HashMap<>();targetDataSources.put("master", masterDataSource);targetDataSources.put("slave", slaveDataSource);DynamicDataSource dynamicDataSource = new DynamicDataSource();dynamicDataSource.setTargetDataSources(targetDataSources);dynamicDataSource.setDefaultTargetDataSource(masterDataSource); // 設置默認數據源return dynamicDataSource;}
}
注意:這種方式下,SqlSessionFactory
和 TransactionManager
只需要配置一個,并讓它們使用這個 @Primary
的 DynamicDataSource
即可。Spring Boot 會自動配置好。
7. 使用
在 Service 方法上添加 @DataSource
注解來切換數據源。
@Service
public class ProductService {@Autowiredprivate ProductMapper productMapper;// 默認不加注解,使用 master 數據源(因為我們配置了默認值)@Transactional // 事務仍然有效public void addProduct(Product product) {productMapper.insert(product);}// 顯式指定使用 slave 數據源@DataSource("slave")public Product getProductById(Integer id) {return productMapper.selectById(id);}
}
重要提醒:關于事務
- 單數據源事務:在以上兩種方案中,只要
DataSourceTransactionManager
與SqlSessionFactory
綁定的是同一個DataSource
,@Transactional
注解就能正常工作。在動態方案中,事務管理器綁定的是DynamicDataSource
,它能確保事務在當前線程選擇的數據源上生效。 - 跨數據源事務(分布式事務):標準的
@Transactional
無法管理跨多個數據源的事務。如果你需要在同一個方法中對master
和slave
都進行寫操作,并保證它們的原子性,你需要引入 JTA(Java Transaction API)事務管理器,例如 Atomikos 或 Narayana。這會增加配置的復雜度。
總結
- 如果你的業務模塊和數據庫綁定關系固定,方案一(靜態方式) 更簡單、更直觀。
- 如果你需要實現讀寫分離,或者在業務邏輯中頻繁切換數據源,方案二(動態方式) 提供了更高的靈活性。