SpringBoot進(jìn)行多數(shù)據(jù)源配置的詳細(xì)步驟
多數(shù)據(jù)源核心概念
多數(shù)據(jù)源是指在一個應(yīng)用程序中同時連接和使用多個數(shù)據(jù)庫的能力。在實際開發(fā)中,我們經(jīng)常會遇到以下場景需要多數(shù)據(jù)源:
- 同時連接生產(chǎn)數(shù)據(jù)庫和報表數(shù)據(jù)庫
- 讀寫分離場景(主庫寫,從庫讀)
- 微服務(wù)架構(gòu)中需要訪問其他服務(wù)的數(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(); } }
禁用默認(rèn)數(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 的事務(wù)管理功能,允許使用 @Transactional 注解來管理事務(wù) @EnableTransactionManagement // 啟用 JPA 倉庫的自動掃描和注冊功能 @EnableJpaRepositories( // 指定要掃描的 JPA 倉庫接口所在的包路徑 basePackages = "com.example.db1", // 指定使用的實體管理器工廠的 Bean 名稱 entityManagerFactoryRef = "db1EntityManagerFactory", // 指定使用的事務(wù)管理器的 Bean 名稱 transactionManagerRef = "db1TransactionManager" ) public class Db1JpaConfig { /** * 創(chuàng)建實體管理器工廠的 Bean,并將其標(biāo)記為主要的實體管理器工廠 Bean */ @Bean(name = "db1EntityManagerFactory") public LocalContainerEntityManagerFactoryBean entityManagerFactory( @Qualifier("db1DataSource")DataSource dataSource, JpaProperties jpaProperties) { return new EntityManagerFactoryBuilder(new HibernateJpaVendorAdapter(), new HashMap<>(), null) // 設(shè)置數(shù)據(jù)源 .dataSource(dataSource) // 指定要掃描的實體類所在的包路徑 .packages("com.example.db1") // 設(shè)置持久化單元的名稱 .persistenceUnit("db1") // 設(shè)置 JPA 的屬性 .properties(jpaProperties.getProperties()) .build(); } /** * 創(chuàng)建事務(wù)管理器的 Bean,并將其標(biāo)記為主要的事務(wù)管理器 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 接口的掃描范圍和對應(yīng)的 SqlSessionFactory 引用 @MapperScan( // 指定要掃描的 Mapper 接口所在的基礎(chǔ)包路徑 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(); // 設(shè)置 SqlSessionFactory 使用的數(shù)據(jù)源 sessionFactory.setDataSource(dataSource); // 設(shè)置 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)建事務(wù)管理器的 Bean,并將其標(biāo)記為主要的事務(wù)管理器 Bean */ @Bean("db1TransactionManager") public PlatformTransactionManager transactionManager( @Qualifier("db1DataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } }
從數(shù)據(jù)源 MyBatis 配置(略)
事務(wù)管理:跨數(shù)據(jù)源事務(wù)處理
單數(shù)據(jù)源事務(wù)
在單數(shù)據(jù)源場景下,Spring的事務(wù)管理非常簡單:
@Service public class AccountService { @Transactional // 使用默認(rèn)事務(wù)管理器 public void transfer(Long fromId, Long toId, BigDecimal amount) { // do some thing ... } }
多數(shù)據(jù)源事務(wù)挑戰(zhàn)
多數(shù)據(jù)源事務(wù)面臨的主要問題是分布式事務(wù)的挑戰(zhàn)。Spring 的 @Transactional 注解默認(rèn)只能管理單個事務(wù)管理器,無法直接協(xié)調(diào)多個數(shù)據(jù)源的事務(wù)。
解決方案對比:
方案 | 原理 | 優(yōu)點 | 缺點 | 適用場景 |
---|---|---|---|---|
JTA (Java Transaction API) | 使用全局事務(wù)協(xié)調(diào)器 | 強(qiáng)一致性 | 性能開銷大,配置復(fù)雜 | 需要強(qiáng)一致性的金融系統(tǒng) |
最終一致性 (Saga模式) | 通過補(bǔ)償操作實現(xiàn) | 高性能,松耦合 | 實現(xiàn)復(fù)雜,需要補(bǔ)償邏輯 | 高并發(fā),可接受短暫不一致 |
本地消息表 | 通過消息隊列保證 | 可靠性高 | 需要額外表存儲消息 | 需要可靠異步處理的場景 |
事務(wù)管理器:DataSourceTransactionManager 和 JpaTransactionManager
DataSourceTransactionManager 和 JpaTransactionManager 是 Spring 框架中針對不同持久層技術(shù)的事務(wù)管理器。
技術(shù)棧適配差異
1.DataSourceTransactionManager
適用場景:純 JDBC、MyBatis、JdbcTemplate 等基于原生 SQL 的數(shù)據(jù)訪問技術(shù)
事務(wù)控制對象:直接管理 java.sql.Connection ,通過數(shù)據(jù)庫連接實現(xiàn)事務(wù)
局限性:
- 無法自動綁定 JPA 或 Hibernate 的 EntityManager/Session 到當(dāng)前事務(wù)上下文
- 混合使用 JDBC 和 JPA 時可能導(dǎo)致連接隔離(各自使用獨立連接),破壞事務(wù)一致性
2.JpaTransactionManager
適用場景:JPA 規(guī)范實現(xiàn)(如 Hibernate、EclipseLink)
事務(wù)控制對象:管理 JPA EntityManager,通過其底層連接協(xié)調(diào)事務(wù)
核心優(yōu)勢:
- 自動將 EntityManager 綁定到線程上下文,確保同一事務(wù)中多次操作使用同一連接
- 支持 JPA 的延遲加載(Lazy Loading)、緩存同步等特性
3.混合技術(shù)棧的特殊情況
混合技術(shù)棧需嚴(yán)格隔離事務(wù)管理器,并考慮分布式事務(wù)需求
JPA操作使用JpaTransactionManager,MyBatis操作使用DataSourceTransactionManager
跨數(shù)據(jù)源事務(wù)需引入分布式事務(wù)(如Atomikos),否則不同數(shù)據(jù)源的事務(wù)無法保證原子性
若一個 Service 方法同時使用 JPA和 Mybatis(未驗證):
- 使用 DataSourceTransactionManager 可能導(dǎo)致兩個操作使用不同連接,違反 ACID
- 使用 JpaTransactionManager 能保證兩者共享同一連接(因 JPA 底層復(fù)用 DataSource 連接)
事務(wù)同步機(jī)制對比
特性 | DataSourceTransactionManager | JpaTransactionManager |
---|---|---|
連接資源管理 | 直接管理 Connection | 通過 EntityManager 間接管理連接 |
跨技術(shù)兼容性 | 僅限 JDBC 系技術(shù) | 支持 JPA 及其混合場景(如 JPA+JDBC) |
高級 ORM 功能支持 | 不支持(如延遲加載) | 完整支持 JPA 特性 |
配置復(fù)雜度 | 簡單(僅需 DataSource) | 需額外配置 EntityManagerFactory |
多數(shù)據(jù)源事務(wù)使用
事務(wù)配置詳見上文
多數(shù)據(jù)源事務(wù)使用示例
import org.springframework.transaction.annotation.Transactional; @Service public class AccountService { @Transactional(transactionManager = "db1TransactionManager") // 指定事務(wù)管理器 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è)置為主要的數(shù)據(jù)源 Bean */ @Bean @Primary public DataSource dynamicDataSource( @Qualifier("db1DataSource") DataSource db1DataSource, @Qualifier("db2DataSource") DataSource db2DataSource) { // 用于存儲目標(biāo)數(shù)據(jù)源的映射,鍵為數(shù)據(jù)源標(biāo)識,值為數(shù)據(jù)源實例 Map<Object, Object> targetDataSources = new HashMap<>(); // 將主數(shù)據(jù)源添加到目標(biāo)數(shù)據(jù)源映射中,使用自定義的主數(shù)據(jù)源標(biāo)識 targetDataSources.put(DynamicDataSourceContextHolder.PRIMARY_DS, db1DataSource); // 將從數(shù)據(jù)源添加到目標(biāo)數(shù)據(jù)源映射中,使用自定義的從數(shù)據(jù)源標(biāo)識 targetDataSources.put(DynamicDataSourceContextHolder.SECONDARY_DS, db2DataSource); // 創(chuàng)建自定義的動態(tài)數(shù)據(jù)源實例 DynamicDataSource dynamicDataSource = new DynamicDataSource(); // 設(shè)置動態(tài)數(shù)據(jù)源的目標(biāo)數(shù)據(jù)源映射 dynamicDataSource.setTargetDataSources(targetDataSources); // 設(shè)置動態(tài)數(shù)據(jù)源的默認(rèn)目標(biāo)數(shù)據(jù)源為主數(shù)據(jù)源 dynamicDataSource.setDefaultTargetDataSource(db1DataSource); return dynamicDataSource; } /** * 自定義動態(tài)數(shù)據(jù)源類,繼承自 AbstractRoutingDataSource */ private static class DynamicDataSource extends AbstractRoutingDataSource { /** * 確定當(dāng)前要使用的數(shù)據(jù)源的標(biāo)識 * @return 當(dāng)前數(shù)據(jù)源的標(biāo)識 */ @Override protected Object determineCurrentLookupKey() { // 從上下文持有者中獲取當(dāng)前要使用的數(shù)據(jù)源類型 return DynamicDataSourceContextHolder.getDataSourceType(); } } }
基于AOP的讀寫分離實現(xiàn)
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface ReadOnly { // 標(biāo)記為讀操作 } @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) { // 默認(rèn)使用主數(shù)據(jù)源(寫) productRepository.save(product); } @ReadOnly // 執(zhí)行該注解標(biāo)記的方法時,前后都會執(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) | 使用全局事務(wù)協(xié)調(diào)器 | 強(qiáng)一致性 | 性能開銷大,配置復(fù)雜 | 需要強(qiáng)一致性的金融系統(tǒng) |
最終一致性 (Saga模式) | 通過補(bǔ)償操作實現(xiàn) | 高性能,松耦合 | 實現(xiàn)復(fù)雜,需要補(bǔ)償邏輯 | 高并發(fā),可接受短暫不一致 |
本地消息表 | 通過消息隊列保證 | 可靠性高 | 需要額外表存儲消息 | 需要可靠異步處理的場景 |
數(shù)據(jù)源切換失敗案例分析
問題描述:
在動態(tài)數(shù)據(jù)源切換場景下,有時切換不生效,仍然使用默認(rèn)數(shù)據(jù)源。
原因分析:
- 數(shù)據(jù)源切換代碼被異常繞過,未執(zhí)行
- 線程池場景下線程復(fù)用導(dǎo)致上下文污染
- AOP 順序問題導(dǎo)致切換時機(jī)不對
解決方案:
@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 { // 恢復(fù)為原來的數(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ù)源與緩存集成
當(dāng)多數(shù)據(jù)源與緩存(如 Redis)一起使用時,需要注意緩存鍵的設(shè)計:
@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() { // 清除所有用戶緩存 } }
總結(jié)與擴(kuò)展
技術(shù)選型建議
場景 | 推薦方案 | 理由 |
---|---|---|
簡單多數(shù)據(jù)源,無交叉訪問 | 獨立配置多個數(shù)據(jù)源 | 簡單直接,易于維護(hù) |
需要動態(tài)切換數(shù)據(jù)源 | AbstractRoutingDataSource | 靈活,可運行時決定數(shù)據(jù)源 |
需要強(qiáng)一致性事務(wù) | JTA(XA) | 保證ACID,但性能較低 |
高并發(fā),最終一致性可接受 | Saga模式 | 高性能,松耦合 |
讀寫分離 | AOP+注解方式 | 透明化,對業(yè)務(wù)代碼侵入小 |
以上就是SpringBoot進(jìn)行多數(shù)據(jù)源配置的詳細(xì)步驟的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot多數(shù)據(jù)源配置的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
idea2020.1無法自動加載maven依賴的jar包問題及解決方法
這篇文章主要介紹了idea2020.1無法自動加載maven依賴的jar包問題及解決方法,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-07-07Linux?Ubuntu系統(tǒng)下配置JDK環(huán)境、MySQL環(huán)境全過程
眾所周知Ubuntu是一種基于Linux的操作系統(tǒng),它提供了一個穩(wěn)定、安全和易于使用的環(huán)境,下面這篇文章主要給大家介紹了關(guān)于Linux?Ubuntu系統(tǒng)下配置JDK環(huán)境、MySQL環(huán)境的相關(guān)資料,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-07-07Java 超詳細(xì)圖解集合框架的數(shù)據(jù)結(jié)構(gòu)
什么是集合框架呢?集合框架是為表示和操作集合而規(guī)定的一種統(tǒng)一的標(biāo)準(zhǔn)的體系結(jié)構(gòu)。最簡單的集合如數(shù)組、列表和隊列等,任何集合框架一般包含:對外的接口、接口的實現(xiàn)和對集合運算的算法2022-04-04詳解Springboot整合ActiveMQ(Queue和Topic兩種模式)
這篇文章主要介紹了詳解Springboot整合ActiveMQ(Queue和Topic兩種模式),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04