SpringBoot進行多數據源配置的詳細步驟
多數據源核心概念
多數據源是指在一個應用程序中同時連接和使用多個數據庫的能力。在實際開發(fā)中,我們經常會遇到以下場景需要多數據源:
- 同時連接生產數據庫和報表數據庫
- 讀寫分離場景(主庫寫,從庫讀)
- 微服務架構中需要訪問其他服務的數據庫
- 多租戶系統中每個租戶有獨立數據庫
多數據源實現示例
多數據源的配置文件以及配置類
application.yml 配置示例
spring:
datasource:
jdbc-url: jdbc:mysql://localhost:3306/db1 # 主數據源
username: root
password: root123
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
pool-name: PrimaryHikariPool
# 最大連接數
maximum-pool-size: 20
# 最小空閑連接
minimum-idle: 5
# 空閑連接超時時間(ms)
idle-timeout: 30000
# 連接最大生命周期(ms)
max-lifetime: 1800000
# 獲取連接超時時間(ms)
connection-timeout: 30000
connection-test-query: SELECT 1
second-datasource:
jdbc-url: jdbc:mysql://localhost:3306/db2 # 主數據源
username: root
password: root123
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
pool-name: SecondHikariPool
maximum-pool-size: 20
minimum-idle: 5
idle-timeout: 30000
max-lifetime: 1800000
connection-timeout: 30000
connection-test-query: SELECT 1多數據源配置類
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
public class DbConfig {
@Bean("db1DataSourceProperties")
@ConfigurationProperties(prefix = "spring.datasource")
public DataSourceProperties db1DataSourceProperties() {
return new DataSourceProperties();
}
@Bean(name = "db1DataSource")
public DataSource dataSource() {
return db1DataSourceProperties().initializeDataSourceBuilder().build();
}
@Bean("db2DataSourceProperties")
@ConfigurationProperties(prefix = "spring.second-datasource")
public DataSourceProperties db2DataSourceProperties() {
return new DataSourceProperties();
}
@Bean(name = "db2DataSource")
public DataSource db2DataSource() {
return db2DataSourceProperties().initializeDataSourceBuilder().build();
}
}
禁用默認數據源
多數據源時需在主類排除自動配置
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}JPA 多數據源配置
主數據源 JAP 配置
import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.persistence.EntityManager;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Objects;
@Configuration
// 啟用 Spring 的事務管理功能,允許使用 @Transactional 注解來管理事務
@EnableTransactionManagement
// 啟用 JPA 倉庫的自動掃描和注冊功能
@EnableJpaRepositories(
// 指定要掃描的 JPA 倉庫接口所在的包路徑
basePackages = "com.example.db1",
// 指定使用的實體管理器工廠的 Bean 名稱
entityManagerFactoryRef = "db1EntityManagerFactory",
// 指定使用的事務管理器的 Bean 名稱
transactionManagerRef = "db1TransactionManager"
)
public class Db1JpaConfig {
/**
* 創(chuàng)建實體管理器工廠的 Bean,并將其標記為主要的實體管理器工廠 Bean
*/
@Bean(name = "db1EntityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
@Qualifier("db1DataSource")DataSource dataSource,
JpaProperties jpaProperties) {
return new EntityManagerFactoryBuilder(new HibernateJpaVendorAdapter(),
new HashMap<>(), null)
// 設置數據源
.dataSource(dataSource)
// 指定要掃描的實體類所在的包路徑
.packages("com.example.db1")
// 設置持久化單元的名稱
.persistenceUnit("db1")
// 設置 JPA 的屬性
.properties(jpaProperties.getProperties())
.build();
}
/**
* 創(chuàng)建事務管理器的 Bean,并將其標記為主要的事務管理器 Bean
*/
@Bean(name = "db1TransactionManager")
public PlatformTransactionManager transactionManager(
@Qualifier("db1EntityManagerFactory") LocalContainerEntityManagerFactoryBean entityManagerFactory) {
return new JpaTransactionManager(Objects.requireNonNull(entityManagerFactory.getObject()));
}
/**
* QueryDSL的核心組件
*/
@Bean(name = "db1JPAQueryFactory")
public JPAQueryFactory db1JPAQueryFactory(
@Qualifier("db1EntityManagerFactory") EntityManager entityManager) {
return new JPAQueryFactory(entityManager);
}
}從數據源 JAP 集成配置(略)
MyBatis 多數據源配置
主數據源 MyBatis 配置
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
@Configuration
// 此注解用于指定 MyBatis Mapper 接口的掃描范圍和對應的 SqlSessionFactory 引用
@MapperScan(
// 指定要掃描的 Mapper 接口所在的基礎包路徑
basePackages = "com.example.mapper.db1",
// 配置使用的 SqlSessionFactory Bean 的名稱
sqlSessionFactoryRef = "db1SqlSessionFactory"
)
public class Db1MyBatisConfig {
/**
* 創(chuàng)建 SqlSessionFactory Bean
*/
@Bean("db1SqlSessionFactory")
public SqlSessionFactory db1SqlSessionFactory(
@Qualifier("db1DataSource") DataSource dataSource) throws Exception {
// 創(chuàng)建 SqlSessionFactoryBean 實例,用于創(chuàng)建 SqlSessionFactory
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
// 設置 SqlSessionFactory 使用的數據源
sessionFactory.setDataSource(dataSource);
// 設置 Mapper XML 文件的位置,使用 PathMatchingResourcePatternResolver 來查找匹配的資源
sessionFactory.setMapperLocations(
new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/db1/*.xml"));
// 獲取并返回 SqlSessionFactory 實例
return sessionFactory.getObject();
}
/**
* 創(chuàng)建 SqlSessionTemplate Bean
*/
@Bean("db1SqlSessionTemplate")
public SqlSessionTemplate db1SqlSessionTemplate(
@Qualifier("db1SqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
// 創(chuàng)建并返回 SqlSessionTemplate 實例,用于簡化 MyBatis 的操作
return new SqlSessionTemplate(sqlSessionFactory);
}
/**
* 創(chuàng)建事務管理器的 Bean,并將其標記為主要的事務管理器 Bean
*/
@Bean("db1TransactionManager")
public PlatformTransactionManager transactionManager(
@Qualifier("db1DataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
從數據源 MyBatis 配置(略)
事務管理:跨數據源事務處理
單數據源事務
在單數據源場景下,Spring的事務管理非常簡單:
@Service
public class AccountService {
@Transactional // 使用默認事務管理器
public void transfer(Long fromId, Long toId, BigDecimal amount) {
// do some thing ...
}
}
多數據源事務挑戰(zhàn)
多數據源事務面臨的主要問題是分布式事務的挑戰(zhàn)。Spring 的 @Transactional 注解默認只能管理單個事務管理器,無法直接協調多個數據源的事務。
解決方案對比:
| 方案 | 原理 | 優(yōu)點 | 缺點 | 適用場景 |
|---|---|---|---|---|
| JTA (Java Transaction API) | 使用全局事務協調器 | 強一致性 | 性能開銷大,配置復雜 | 需要強一致性的金融系統 |
| 最終一致性 (Saga模式) | 通過補償操作實現 | 高性能,松耦合 | 實現復雜,需要補償邏輯 | 高并發(fā),可接受短暫不一致 |
| 本地消息表 | 通過消息隊列保證 | 可靠性高 | 需要額外表存儲消息 | 需要可靠異步處理的場景 |
事務管理器:DataSourceTransactionManager 和 JpaTransactionManager
DataSourceTransactionManager 和 JpaTransactionManager 是 Spring 框架中針對不同持久層技術的事務管理器。
技術棧適配差異
1.DataSourceTransactionManager
適用場景:純 JDBC、MyBatis、JdbcTemplate 等基于原生 SQL 的數據訪問技術
事務控制對象:直接管理 java.sql.Connection ,通過數據庫連接實現事務
局限性:
- 無法自動綁定 JPA 或 Hibernate 的 EntityManager/Session 到當前事務上下文
- 混合使用 JDBC 和 JPA 時可能導致連接隔離(各自使用獨立連接),破壞事務一致性
2.JpaTransactionManager
適用場景:JPA 規(guī)范實現(如 Hibernate、EclipseLink)
事務控制對象:管理 JPA EntityManager,通過其底層連接協調事務
核心優(yōu)勢:
- 自動將 EntityManager 綁定到線程上下文,確保同一事務中多次操作使用同一連接
- 支持 JPA 的延遲加載(Lazy Loading)、緩存同步等特性
3.混合技術棧的特殊情況
混合技術棧需嚴格隔離事務管理器,并考慮分布式事務需求
JPA操作使用JpaTransactionManager,MyBatis操作使用DataSourceTransactionManager
跨數據源事務需引入分布式事務(如Atomikos),否則不同數據源的事務無法保證原子性
若一個 Service 方法同時使用 JPA和 Mybatis(未驗證):
- 使用 DataSourceTransactionManager 可能導致兩個操作使用不同連接,違反 ACID
- 使用 JpaTransactionManager 能保證兩者共享同一連接(因 JPA 底層復用 DataSource 連接)
事務同步機制對比
| 特性 | DataSourceTransactionManager | JpaTransactionManager |
|---|---|---|
| 連接資源管理 | 直接管理 Connection | 通過 EntityManager 間接管理連接 |
| 跨技術兼容性 | 僅限 JDBC 系技術 | 支持 JPA 及其混合場景(如 JPA+JDBC) |
| 高級 ORM 功能支持 | 不支持(如延遲加載) | 完整支持 JPA 特性 |
| 配置復雜度 | 簡單(僅需 DataSource) | 需額外配置 EntityManagerFactory |
多數據源事務使用
事務配置詳見上文
多數據源事務使用示例
import org.springframework.transaction.annotation.Transactional;
@Service
public class AccountService {
@Transactional(transactionManager = "db1TransactionManager") // 指定事務管理器
public void transfer(Long fromId, Long toId, BigDecimal amount) {
// do some thing ...
}
}
基于 AbstractRoutingDataSource 的動態(tài)數據源
動態(tài)數據源上下文
public class DynamicDataSourceContextHolder {
// 使用ThreadLocal保證線程安全
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
// 數據源列表
public static final String PRIMARY_DS = "primary";
public static final String SECONDARY_DS = "secondary";
public static void setDataSourceType(String dsType) {
CONTEXT_HOLDER.set(dsType);
}
public static String getDataSourceType() {
return CONTEXT_HOLDER.get();
}
public static void clearDataSourceType() {
CONTEXT_HOLDER.remove();
}
}
動態(tài)數據源配置
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class DynamicDataSourceConfig {
/**
* 創(chuàng)建動態(tài)數據源 Bean,并將其設置為主要的數據源 Bean
*/
@Bean
@Primary
public DataSource dynamicDataSource(
@Qualifier("db1DataSource") DataSource db1DataSource,
@Qualifier("db2DataSource") DataSource db2DataSource) {
// 用于存儲目標數據源的映射,鍵為數據源標識,值為數據源實例
Map<Object, Object> targetDataSources = new HashMap<>();
// 將主數據源添加到目標數據源映射中,使用自定義的主數據源標識
targetDataSources.put(DynamicDataSourceContextHolder.PRIMARY_DS, db1DataSource);
// 將從數據源添加到目標數據源映射中,使用自定義的從數據源標識
targetDataSources.put(DynamicDataSourceContextHolder.SECONDARY_DS, db2DataSource);
// 創(chuàng)建自定義的動態(tài)數據源實例
DynamicDataSource dynamicDataSource = new DynamicDataSource();
// 設置動態(tài)數據源的目標數據源映射
dynamicDataSource.setTargetDataSources(targetDataSources);
// 設置動態(tài)數據源的默認目標數據源為主數據源
dynamicDataSource.setDefaultTargetDataSource(db1DataSource);
return dynamicDataSource;
}
/**
* 自定義動態(tài)數據源類,繼承自 AbstractRoutingDataSource
*/
private static class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 確定當前要使用的數據源的標識
* @return 當前數據源的標識
*/
@Override
protected Object determineCurrentLookupKey() {
// 從上下文持有者中獲取當前要使用的數據源類型
return DynamicDataSourceContextHolder.getDataSourceType();
}
}
}
基于AOP的讀寫分離實現
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ReadOnly {
// 標記為讀操作
}
@Aspect
@Component
public class ReadWriteDataSourceAspect {
@Before("@annotation(readOnly)")
public void beforeSwitchDataSource(JoinPoint point, ReadOnly readOnly) {
DynamicDataSourceContextHolder.setDataSourceType(DynamicDataSourceContextHolder.SECONDARY_DS);
}
@After("@annotation(readOnly)")
public void afterSwitchDataSource(JoinPoint point, ReadOnly readOnly) {
DynamicDataSourceContextHolder.clearDataSourceType();
}
}使用示例
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
@Transactional
public void createProduct(Product product) {
// 默認使用主數據源(寫)
productRepository.save(product);
}
@ReadOnly // 執(zhí)行該注解標記的方法時,前后都會執(zhí)行ReadWriteDataSourceAspect切面類方法
@Transactional
public Product getProduct(Long id) {
// 使用從數據源(讀)
return productRepository.findById(id).orElse(null);
}
@ReadOnly
@Transactional
public List<Product> listProducts() {
// 使用從數據源(讀)
return productRepository.findAll();
}
}常見問題與解決方案
典型問題排查表
| 方案 | 原理 | 優(yōu)點 | 缺點 | 適用場景 |
|---|---|---|---|---|
| JTA (Java Transaction API) | 使用全局事務協調器 | 強一致性 | 性能開銷大,配置復雜 | 需要強一致性的金融系統 |
| 最終一致性 (Saga模式) | 通過補償操作實現 | 高性能,松耦合 | 實現復雜,需要補償邏輯 | 高并發(fā),可接受短暫不一致 |
| 本地消息表 | 通過消息隊列保證 | 可靠性高 | 需要額外表存儲消息 | 需要可靠異步處理的場景 |
數據源切換失敗案例分析
問題描述:
在動態(tài)數據源切換場景下,有時切換不生效,仍然使用默認數據源。
原因分析:
- 數據源切換代碼被異常繞過,未執(zhí)行
- 線程池場景下線程復用導致上下文污染
- AOP 順序問題導致切換時機不對
解決方案:
@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE) // 確保最先執(zhí)行
public class DataSourceAspect {
@Around("@annotation(targetDataSource)")
public Object around(ProceedingJoinPoint joinPoint, TargetDataSource targetDataSource) throws Throwable {
String oldKey = DynamicDataSourceContextHolder.getDataSourceType();
try {
DynamicDataSourceContextHolder.setDataSourceType(targetDataSource.value());
return joinPoint.proceed();
} finally {
// 恢復為原來的數據源
if (oldKey != null) {
DynamicDataSourceContextHolder.setDataSourceType(oldKey);
} else {
DynamicDataSourceContextHolder.clearDataSourceType();
}
}
}
}
???????// 線程池配置確保清理上下文
@Configuration
public class ThreadPoolConfig {
@Bean
public ExecutorService asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("Async-");
executor.setTaskDecorator(runnable -> {
String dsKey = DynamicDataSourceContextHolder.getDataSourceType();
return () -> {
try {
if (dsKey != null) {
DynamicDataSourceContextHolder.setDataSourceType(dsKey);
}
runnable.run();
} finally {
DynamicDataSourceContextHolder.clearDataSourceType();
}
};
});
executor.initialize();
return executor.getThreadPoolExecutor();
}
}多數據源與緩存集成
當多數據源與緩存(如 Redis)一起使用時,需要注意緩存鍵的設計:
@Service
public class CachedUserService {
@Autowired
private PrimaryUserRepository primaryUserRepository;
@Autowired
private SecondaryUserRepository secondaryUserRepository;
@Autowired
private RedisTemplate<String, User> redisTemplate;
private String getCacheKey(String source, Long userId) {
return String.format("user:%s:%d", source, userId);
}
@Cacheable(value = "users", key = "#root.target.getCacheKey('primary', #userId)")
public User getPrimaryUser(Long userId) {
return primaryUserRepository.findById(userId).orElse(null);
}
@Cacheable(value = "users", key = "#root.target.getCacheKey('secondary', #userId)")
public User getSecondaryUser(Long userId) {
return secondaryUserRepository.findById(userId).orElse(null);
}
@CacheEvict(value = "users", allEntries = true)
public void clearAllUserCache() {
// 清除所有用戶緩存
}
}
總結與擴展
技術選型建議
| 場景 | 推薦方案 | 理由 |
|---|---|---|
| 簡單多數據源,無交叉訪問 | 獨立配置多個數據源 | 簡單直接,易于維護 |
| 需要動態(tài)切換數據源 | AbstractRoutingDataSource | 靈活,可運行時決定數據源 |
| 需要強一致性事務 | JTA(XA) | 保證ACID,但性能較低 |
| 高并發(fā),最終一致性可接受 | Saga模式 | 高性能,松耦合 |
| 讀寫分離 | AOP+注解方式 | 透明化,對業(yè)務代碼侵入小 |
以上就是SpringBoot進行多數據源配置的詳細步驟的詳細內容,更多關于SpringBoot多數據源配置的資料請關注腳本之家其它相關文章!
相關文章
Java哈希表HashTable(又稱散列表)的實現及練習題
哈希表又稱為散列表,是一種通過哈希函數將數據映射到特定位置的數據結構,這篇文章主要介紹了Java哈希表HashTable(又稱散列表)的實現及練習題的相關資料,文中通過代碼介紹的非常詳細,需要的朋友可以參考下2025-12-12
Java下載文件中文文件名亂碼的解決方案(文件名包含很多%)
Java下載文件時,文件名中文亂碼問題通常是由于編碼不正確導致的,使用`URLEncoder.encode(filepath, "UTF-8")`可以解決在提示下載框中正確顯示漢字文件名的問題,但在選擇直接打開時,文件名會變成亂碼,解決這個問題的方法2025-02-02
springsecurity?登錄認證流程分析一(ajax)
這篇文章主要介紹了springsecurity?登錄認證一(ajax篇),本文通過實例代碼圖文相結合給大家介紹的非常詳細,感興趣的朋友跟隨小編一起看看吧2024-08-08

