文章目录
多数据源核心概念
多数据源是指在一个应用程序中同时连接和使用多个数据库的能力。在实际开发中,我们经常会遇到以下场景需要多数据源:
- 同时连接生产数据库和报表数据库
- 读写分离场景(主库写,从库读)
- 微服务架构中需要访问其他服务的数据库
- 多租户系统中每个租户有独立数据库
多数据源实现示例
多数据源的配置文件以及配置类
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 { /** * 创建实体管理器工厂的 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(); } /** * 创建事务管理器的 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 { /** * 创建 SqlSessionFactory Bean */ @Bean("db1SqlSessionFactory") public SqlSessionFactory db1SqlSessionFactory( @Qualifier("db1DataSource") DataSource dataSource) throws Exception { // 创建 SqlSessionFactoryBean 实例,用于创建 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(); } /** * 创建 SqlSessionTemplate Bean */ @Bean("db1SqlSessionTemplate") public SqlSessionTemplate db1SqlSessionTemplate( @Qualifier("db1SqlSessionFactory") SqlSessionFactory sqlSessionFactory) { // 创建并返回 SqlSessionTemplate 实例,用于简化 MyBatis 的操作 return new SqlSessionTemplate(sqlSessionFactory); } /** * 创建事务管理器的 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 ... } }
多数据源事务挑战
多数据源事务面临的主要问题是分布式事务的挑战。Spring 的 @Transactional 注解默认只能管理单个事务管理器,无法直接协调多个数据源的事务。
解决方案对比:
方案 | 原理 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
JTA (Java Transaction API) | 使用全局事务协调器 | 强一致性 | 性能开销大,配置复杂 | 需要强一致性的金融系统 |
最终一致性 (Saga模式) | 通过补偿操作实现 | 高性能,松耦合 | 实现复杂,需要补偿逻辑 | 高并发,可接受短暂不一致 |
本地消息表 | 通过消息队列保证 | 可靠性高 | 需要额外表存储消息 | 需要可靠异步处理的场景 |
事务管理器:DataSourceTransactionManager 和 JpaTransactionManager
DataSourceTransactionManager 和 JpaTransactionManager 是 Spring 框架中针对不同持久层技术的事务管理器。
技术栈适配差异
DataSourceTransactionManager
- 适用场景:纯 JDBC、MyBatis、JdbcTemplate 等基于原生 SQL 的数据访问技术
- 事务控制对象:直接管理
java.sql.Connection
,通过数据库连接实现事务 - 局限性:
- 无法自动绑定 JPA 或 Hibernate 的
EntityManager
/Session
到当前事务上下文 - 混合使用 JDBC 和 JPA 时可能导致连接隔离(各自使用独立连接),破坏事务一致性
- 无法自动绑定 JPA 或 Hibernate 的
JpaTransactionManager
- 适用场景:JPA 规范实现(如 Hibernate、EclipseLink)
- 事务控制对象:管理 JPA
EntityManager
,通过其底层连接协调事务 - 核心优势:
- 自动将
EntityManager
绑定到线程上下文,确保同一事务中多次操作使用同一连接 - 支持 JPA 的延迟加载(Lazy Loading)、缓存同步等特性
- 自动将
混合技术栈的特殊情况
混合技术栈需严格隔离事务管理器,并考虑分布式事务需求
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 的动态数据源
动态数据源上下文
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(); } }
动态数据源配置
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 { /** * 创建动态数据源 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); // 创建自定义的动态数据源实例 DynamicDataSource dynamicDataSource = new DynamicDataSource(); // 设置动态数据源的目标数据源映射 dynamicDataSource.setTargetDataSources(targetDataSources); // 设置动态数据源的默认目标数据源为主数据源 dynamicDataSource.setDefaultTargetDataSource(db1DataSource); return dynamicDataSource; } /** * 自定义动态数据源类,继承自 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 // 执行该注解标记的方法时,前后都会执行ReadWriteDataSourceAspect切面类方法 @Transactional public Product getProduct(Long id) { // 使用从数据源(读) return productRepository.findById(id).orElse(null); } @ReadOnly @Transactional public List<Product> listProducts() { // 使用从数据源(读) return productRepository.findAll(); } }
常见问题与解决方案
典型问题排查表
方案 | 原理 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
JTA (Java Transaction API) | 使用全局事务协调器 | 强一致性 | 性能开销大,配置复杂 | 需要强一致性的金融系统 |
最终一致性 (Saga模式) | 通过补偿操作实现 | 高性能,松耦合 | 实现复杂,需要补偿逻辑 | 高并发,可接受短暂不一致 |
本地消息表 | 通过消息队列保证 | 可靠性高 | 需要额外表存储消息 | 需要可靠异步处理的场景 |
数据源切换失败案例分析
问题描述:
在动态数据源切换场景下,有时切换不生效,仍然使用默认数据源。
原因分析:
- 数据源切换代码被异常绕过,未执行
- 线程池场景下线程复用导致上下文污染
- AOP 顺序问题导致切换时机不对
解决方案:
@Aspect @Component @Order(Ordered.HIGHEST_PRECEDENCE) // 确保最先执行 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() { // 清除所有用户缓存 } }
总结与扩展
技术选型建议
场景 | 推荐方案 | 理由 |
---|---|---|
简单多数据源,无交叉访问 | 独立配置多个数据源 | 简单直接,易于维护 |
需要动态切换数据源 | AbstractRoutingDataSource | 灵活,可运行时决定数据源 |
需要强一致性事务 | JTA(XA) | 保证ACID,但性能较低 |
高并发,最终一致性可接受 | Saga模式 | 高性能,松耦合 |
读写分离 | AOP+注解方式 | 透明化,对业务代码侵入小 |