SpringBoot進行多數(shù)據(jù)源配置的詳細步驟
多數(shù)據(jù)源核心概念
多數(shù)據(jù)源是指在一個應用程序中同時連接和使用多個數(shù)據(jù)庫的能力。在實際開發(fā)中,我們經(jīng)常會遇到以下場景需要多數(shù)據(jù)源:
- 同時連接生產(chǎn)數(shù)據(jù)庫和報表數(shù)據(jù)庫
- 讀寫分離場景(主庫寫,從庫讀)
- 微服務架構中需要訪問其他服務的數(shù)據(jù)庫
- 多租戶系統(tǒng)中每個租戶有獨立數(shù)據(jù)庫
多數(shù)據(jù)源實現(xiàn)示例
多數(shù)據(jù)源的配置文件以及配置類
application.yml 配置示例
spring:
datasource:
jdbc-url: jdbc:mysql://localhost:3306/db1 # 主數(shù)據(jù)源
username: root
password: root123
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
pool-name: PrimaryHikariPool
# 最大連接數(shù)
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 # 主數(shù)據(jù)源
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多數(shù)據(jù)源配置類
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();
}
}
禁用默認數(shù)據(jù)源
多數(shù)據(jù)源時需在主類排除自動配置
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}JPA 多數(shù)據(jù)源配置
主數(shù)據(jù)源 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)
// 設置數(shù)據(jù)源
.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);
}
}從數(shù)據(jù)源 JAP 集成配置(略)
MyBatis 多數(shù)據(jù)源配置
主數(shù)據(jù)源 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 使用的數(shù)據(jù)源
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);
}
}
從數(shù)據(jù)源 MyBatis 配置(略)
事務管理:跨數(shù)據(jù)源事務處理
單數(shù)據(jù)源事務
在單數(shù)據(jù)源場景下,Spring的事務管理非常簡單:
@Service
public class AccountService {
@Transactional // 使用默認事務管理器
public void transfer(Long fromId, Long toId, BigDecimal amount) {
// do some thing ...
}
}
多數(shù)據(jù)源事務挑戰(zhàn)
多數(shù)據(jù)源事務面臨的主要問題是分布式事務的挑戰(zhàn)。Spring 的 @Transactional 注解默認只能管理單個事務管理器,無法直接協(xié)調(diào)多個數(shù)據(jù)源的事務。
解決方案對比:
| 方案 | 原理 | 優(yōu)點 | 缺點 | 適用場景 |
|---|---|---|---|---|
| JTA (Java Transaction API) | 使用全局事務協(xié)調(diào)器 | 強一致性 | 性能開銷大,配置復雜 | 需要強一致性的金融系統(tǒng) |
| 最終一致性 (Saga模式) | 通過補償操作實現(xiàn) | 高性能,松耦合 | 實現(xiàn)復雜,需要補償邏輯 | 高并發(fā),可接受短暫不一致 |
| 本地消息表 | 通過消息隊列保證 | 可靠性高 | 需要額外表存儲消息 | 需要可靠異步處理的場景 |
事務管理器:DataSourceTransactionManager 和 JpaTransactionManager
DataSourceTransactionManager 和 JpaTransactionManager 是 Spring 框架中針對不同持久層技術的事務管理器。
技術棧適配差異
1.DataSourceTransactionManager
適用場景:純 JDBC、MyBatis、JdbcTemplate 等基于原生 SQL 的數(shù)據(jù)訪問技術
事務控制對象:直接管理 java.sql.Connection ,通過數(shù)據(jù)庫連接實現(xiàn)事務
局限性:
- 無法自動綁定 JPA 或 Hibernate 的 EntityManager/Session 到當前事務上下文
- 混合使用 JDBC 和 JPA 時可能導致連接隔離(各自使用獨立連接),破壞事務一致性
2.JpaTransactionManager
適用場景:JPA 規(guī)范實現(xiàn)(如 Hibernate、EclipseLink)
事務控制對象:管理 JPA EntityManager,通過其底層連接協(xié)調(diào)事務
核心優(yōu)勢:
- 自動將 EntityManager 綁定到線程上下文,確保同一事務中多次操作使用同一連接
- 支持 JPA 的延遲加載(Lazy Loading)、緩存同步等特性
3.混合技術棧的特殊情況
混合技術棧需嚴格隔離事務管理器,并考慮分布式事務需求
JPA操作使用JpaTransactionManager,MyBatis操作使用DataSourceTransactionManager
跨數(shù)據(jù)源事務需引入分布式事務(如Atomikos),否則不同數(shù)據(jù)源的事務無法保證原子性
若一個 Service 方法同時使用 JPA和 Mybatis(未驗證):
- 使用 DataSourceTransactionManager 可能導致兩個操作使用不同連接,違反 ACID
- 使用 JpaTransactionManager 能保證兩者共享同一連接(因 JPA 底層復用 DataSource 連接)
事務同步機制對比
| 特性 | DataSourceTransactionManager | JpaTransactionManager |
|---|---|---|
| 連接資源管理 | 直接管理 Connection | 通過 EntityManager 間接管理連接 |
| 跨技術兼容性 | 僅限 JDBC 系技術 | 支持 JPA 及其混合場景(如 JPA+JDBC) |
| 高級 ORM 功能支持 | 不支持(如延遲加載) | 完整支持 JPA 特性 |
| 配置復雜度 | 簡單(僅需 DataSource) | 需額外配置 EntityManagerFactory |
多數(shù)據(jù)源事務使用
事務配置詳見上文
多數(shù)據(jù)源事務使用示例
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)數(shù)據(jù)源
動態(tài)數(shù)據(jù)源上下文
public class DynamicDataSourceContextHolder {
// 使用ThreadLocal保證線程安全
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
// 數(shù)據(jù)源列表
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)數(shù)據(jù)源配置
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)數(shù)據(jù)源 Bean,并將其設置為主要的數(shù)據(jù)源 Bean
*/
@Bean
@Primary
public DataSource dynamicDataSource(
@Qualifier("db1DataSource") DataSource db1DataSource,
@Qualifier("db2DataSource") DataSource db2DataSource) {
// 用于存儲目標數(shù)據(jù)源的映射,鍵為數(shù)據(jù)源標識,值為數(shù)據(jù)源實例
Map<Object, Object> targetDataSources = new HashMap<>();
// 將主數(shù)據(jù)源添加到目標數(shù)據(jù)源映射中,使用自定義的主數(shù)據(jù)源標識
targetDataSources.put(DynamicDataSourceContextHolder.PRIMARY_DS, db1DataSource);
// 將從數(shù)據(jù)源添加到目標數(shù)據(jù)源映射中,使用自定義的從數(shù)據(jù)源標識
targetDataSources.put(DynamicDataSourceContextHolder.SECONDARY_DS, db2DataSource);
// 創(chuàng)建自定義的動態(tài)數(shù)據(jù)源實例
DynamicDataSource dynamicDataSource = new DynamicDataSource();
// 設置動態(tài)數(shù)據(jù)源的目標數(shù)據(jù)源映射
dynamicDataSource.setTargetDataSources(targetDataSources);
// 設置動態(tài)數(shù)據(jù)源的默認目標數(shù)據(jù)源為主數(shù)據(jù)源
dynamicDataSource.setDefaultTargetDataSource(db1DataSource);
return dynamicDataSource;
}
/**
* 自定義動態(tài)數(shù)據(jù)源類,繼承自 AbstractRoutingDataSource
*/
private static class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 確定當前要使用的數(shù)據(jù)源的標識
* @return 當前數(shù)據(jù)源的標識
*/
@Override
protected Object determineCurrentLookupKey() {
// 從上下文持有者中獲取當前要使用的數(shù)據(jù)源類型
return DynamicDataSourceContextHolder.getDataSourceType();
}
}
}
基于AOP的讀寫分離實現(xiàn)
@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) {
// 默認使用主數(shù)據(jù)源(寫)
productRepository.save(product);
}
@ReadOnly // 執(zhí)行該注解標記的方法時,前后都會執(zhí)行ReadWriteDataSourceAspect切面類方法
@Transactional
public Product getProduct(Long id) {
// 使用從數(shù)據(jù)源(讀)
return productRepository.findById(id).orElse(null);
}
@ReadOnly
@Transactional
public List<Product> listProducts() {
// 使用從數(shù)據(jù)源(讀)
return productRepository.findAll();
}
}常見問題與解決方案
典型問題排查表
| 方案 | 原理 | 優(yōu)點 | 缺點 | 適用場景 |
|---|---|---|---|---|
| JTA (Java Transaction API) | 使用全局事務協(xié)調(diào)器 | 強一致性 | 性能開銷大,配置復雜 | 需要強一致性的金融系統(tǒng) |
| 最終一致性 (Saga模式) | 通過補償操作實現(xiàn) | 高性能,松耦合 | 實現(xiàn)復雜,需要補償邏輯 | 高并發(fā),可接受短暫不一致 |
| 本地消息表 | 通過消息隊列保證 | 可靠性高 | 需要額外表存儲消息 | 需要可靠異步處理的場景 |
數(shù)據(jù)源切換失敗案例分析
問題描述:
在動態(tài)數(shù)據(jù)源切換場景下,有時切換不生效,仍然使用默認數(shù)據(jù)源。
原因分析:
- 數(shù)據(jù)源切換代碼被異常繞過,未執(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 {
// 恢復為原來的數(shù)據(jù)源
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();
}
}多數(shù)據(jù)源與緩存集成
當多數(shù)據(jù)源與緩存(如 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() {
// 清除所有用戶緩存
}
}
總結與擴展
技術選型建議
| 場景 | 推薦方案 | 理由 |
|---|---|---|
| 簡單多數(shù)據(jù)源,無交叉訪問 | 獨立配置多個數(shù)據(jù)源 | 簡單直接,易于維護 |
| 需要動態(tài)切換數(shù)據(jù)源 | AbstractRoutingDataSource | 靈活,可運行時決定數(shù)據(jù)源 |
| 需要強一致性事務 | JTA(XA) | 保證ACID,但性能較低 |
| 高并發(fā),最終一致性可接受 | Saga模式 | 高性能,松耦合 |
| 讀寫分離 | AOP+注解方式 | 透明化,對業(yè)務代碼侵入小 |
以上就是SpringBoot進行多數(shù)據(jù)源配置的詳細步驟的詳細內(nèi)容,更多關于SpringBoot多數(shù)據(jù)源配置的資料請關注腳本之家其它相關文章!
相關文章
如何讓@EnableConfigurationProperties的值注入到@Value中
這篇文章主要介紹了如何讓@EnableConfigurationProperties的值注入到@Value中的實現(xiàn)方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2025-06-06
Spring?Security權限注解啟動及邏輯處理使用示例
這篇文章主要為大家介紹了Spring?Security權限注解啟動及邏輯處理使用示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-07-07
Feign如何使用protobuf的類作為參數(shù)調(diào)用
這篇文章主要介紹了Feign如何使用protobuf的類作為參數(shù)調(diào)用,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-03-03
Java實現(xiàn)視頻格式轉(zhuǎn)化的操作代碼
在當今數(shù)字化時代,視頻已成為我們?nèi)粘I詈凸ぷ髦胁豢苫蛉钡囊徊糠?不同的設備和平臺可能支持不同的視頻格式,因此,視頻格式轉(zhuǎn)換的需求也日益增長,本文將介紹如何使用Java實現(xiàn)視頻格式轉(zhuǎn)換,需要的朋友可以參考下2025-01-01

