多数据源是指在一个应用程序中同时连接和使用多个数据库的能力。在实际开发中,我们经常会遇到以下场景需要多数据源:
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);
}
}
主数据源 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 配置
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 是 Spring 框架中针对不同持久层技术的事务管理器。
技术栈适配差异
DataSourceTransactionManager
java.sql.Connection
,通过数据库连接实现事务EntityManager
/Session
到当前事务上下文JpaTransactionManager
EntityManager
,通过其底层连接协调事务EntityManager
绑定到线程上下文,确保同一事务中多次操作使用同一连接混合技术栈的特殊情况
混合技术栈需严格隔离事务管理器,并考虑分布式事务需求
JPA操作使用JpaTransactionManager
,MyBatis操作使用DataSourceTransactionManager
跨数据源事务需引入分布式事务(如Atomikos),否则不同数据源的事务无法保证原子性
若一个 Service 方法同时使用 JPA和 Mybatis(未验证):
DataSourceTransactionManager
可能导致两个操作使用不同连接,违反 ACIDJpaTransactionManager
能保证两者共享同一连接(因 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 ...
}
}
动态数据源上下文
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模式) | 通过补偿操作实现 | 高性能,松耦合 | 实现复杂,需要补偿逻辑 | 高并发,可接受短暂不一致 |
本地消息表 | 通过消息队列保证 | 可靠性高 | 需要额外表存储消息 | 需要可靠异步处理的场景 |
问题描述:
在动态数据源切换场景下,有时切换不生效,仍然使用默认数据源。
原因分析:
解决方案:
@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+注解方式 | 透明化,对业务代码侵入小 |