1. 多数据源概述与核心概念
1.1 什么是多数据源?
多数据源是指在一个应用程序中同时连接和使用多个数据库的能力。在实际开发中,我们经常会遇到以下场景需要多数据源:
- 同时连接生产数据库和报表数据库
- 读写分离场景(主库写,从库读)
- 微服务架构中需要访问其他服务的数据库
- 多租户系统中每个租户有独立数据库
1.2 核心组件解析
组件 | 作用 | 类比解释 |
---|---|---|
DataSource | 数据库连接池,管理数据库连接 | 像是一个自来水厂,管理着到各个小区(数据库)的水管(连接) |
EntityManager | JPA实体管理器,负责持久化操作 | 像是小区的物业管理员,负责管理小区内(数据库)的各种事务 |
TransactionManager | 事务管理器,协调跨数据源的事务 | 像是跨小区的协调员,确保多个小区的事务能同步进行 |
1.3 多数据源实现方式对比
实现方式 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
抽象路由(AbstractRoutingDataSource) | 灵活,动态切换 | 配置复杂 | 需要运行时动态切换数据源 |
独立配置多数据源 | 清晰明确 | 代码冗余 | 固定的多个数据源 |
JPA多持久化单元 | 符合JPA规范 | 配置复杂 | 使用JPA且需要严格规范的项目 |
2. 基础配置:两种数据源实现
2.1 基于配置文件的简单多数据源
- application.yml配置示例:
spring:
datasource:
primary: # 主数据源
jdbc-url: jdbc:mysql://localhost:3306/primary_db?useSSL=false
username: root
password: root123
driver-class-name: com.mysql.cj.jdbc.Driver
secondary: # 从数据源
jdbc-url: jdbc:mysql://localhost:3306/secondary_db?useSSL=false
username: root
password: root123
driver-class-name: com.mysql.cj.jdbc.Driver
- 数据源配置类:
@Configuration
public class DataSourceConfig {
// 主数据源
@Bean
@Primary
@ConfigurationProperties(prefix = "spring.datasource.primary")
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
// 从数据源
@Bean
@ConfigurationProperties(prefix = "spring.datasource.secondary")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}
}
- 配置事务管理器:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import javax.sql.DataSource;
@Configuration
public class TransactionManagerConfig {
@Bean(name = "primaryTransactionManager")
public DataSourceTransactionManager primaryTransactionManager(@Qualifier("primaryDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean(name = "secondaryTransactionManager")
public DataSourceTransactionManager secondaryTransactionManager(@Qualifier("secondaryDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
使用示例:
@Service
public class UserService {
// 使用JdbcTemplate操作主数据源
@Autowired
@Qualifier("primaryDataSource")
private DataSource primaryDataSource;
// 使用JdbcTemplate操作从数据源
@Autowired
@Qualifier("secondaryDataSource")
private DataSource secondaryDataSource;
public void addUserToBothDBs(User user) {
JdbcTemplate primaryJdbcTemplate = new JdbcTemplate(primaryDataSource);
JdbcTemplate secondaryJdbcTemplate = new JdbcTemplate(secondaryDataSource);
String sql = "INSERT INTO users (username, email) VALUES (?, ?)";
primaryJdbcTemplate.update(sql, user.getUsername(), user.getEmail());
secondaryJdbcTemplate.update(sql, user.getUsername(), user.getEmail());
}
}
2.2 基于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();
}
}
动态数据源配置:
// 表明该类是一个配置类,Spring 会对其进行扫描并应用其中的配置信息
@Configuration
public class DynamicDataSourceConfig {
/**
* 创建主数据源 Bean
* @return 主数据源实例
*/
// 将该方法返回的对象注册为一个 Spring Bean
@Bean
// 从配置文件中读取以 "spring.datasource.primary" 为前缀的配置信息来初始化数据源
@ConfigurationProperties(prefix = "spring.datasource.primary")
public DataSource primaryDataSource() {
// 使用 DataSourceBuilder 创建数据源实例
return DataSourceBuilder.create().build();
}
/**
* 创建从数据源 Bean
* @return 从数据源实例
*/
// 将该方法返回的对象注册为一个 Spring Bean
@Bean
// 从配置文件中读取以 "spring.datasource.secondary" 为前缀的配置信息来初始化数据源
@ConfigurationProperties(prefix = "spring.datasource.secondary")
public DataSource secondaryDataSource() {
// 使用 DataSourceBuilder 创建数据源实例
return DataSourceBuilder.create().build();
}
/**
* 创建动态数据源 Bean,并将其设置为主要的数据源 Bean
* @return 动态数据源实例
*/
// 将该方法返回的对象注册为一个 Spring Bean
@Bean
// 标记该 Bean 为主要的 Bean,当有多个同类型的 Bean 时,优先使用该 Bean
@Primary
public DataSource dynamicDataSource() {
// 用于存储目标数据源的映射,键为数据源标识,值为数据源实例
Map<Object, Object> targetDataSources = new HashMap<>();
// 将主数据源添加到目标数据源映射中,使用自定义的主数据源标识
targetDataSources.put(DynamicDataSourceContextHolder.PRIMARY_DS, primaryDataSource());
// 将从数据源添加到目标数据源映射中,使用自定义的从数据源标识
targetDataSources.put(DynamicDataSourceContextHolder.SECONDARY_DS, secondaryDataSource());
// 创建自定义的动态数据源实例
DynamicDataSource dynamicDataSource = new DynamicDataSource();
// 设置动态数据源的目标数据源映射
dynamicDataSource.setTargetDataSources(targetDataSources);
// 设置动态数据源的默认目标数据源为主数据源
dynamicDataSource.setDefaultTargetDataSource(primaryDataSource());
return dynamicDataSource;
}
// 自定义动态数据源类,继承自 AbstractRoutingDataSource
private static class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 确定当前要使用的数据源的标识
* @return 当前数据源的标识
*/
@Override
protected Object determineCurrentLookupKey() {
// 从上下文持有者中获取当前要使用的数据源类型
return DynamicDataSourceContextHolder.getDataSourceType();
}
}
}
使用示例:
@Service
public class OrderService {
@Autowired
private JdbcTemplate jdbcTemplate;
public void processOrder(Long orderId) {
// 默认使用主数据源
Order order = getOrderFromPrimary(orderId);
try {
// 切换到从数据源
DynamicDataSourceContextHolder.setDataSourceType(DynamicDataSourceContextHolder.SECONDARY_DS);
// 在从数据源中记录订单日志
logOrderToSecondary(order);
} finally {
// 恢复数据源
DynamicDataSourceContextHolder.clearDataSourceType();
}
}
private Order getOrderFromPrimary(Long orderId) {
String sql = "SELECT * FROM orders WHERE id = ?";
return jdbcTemplate.queryForObject(sql, new Object[]{orderId},
(rs, rowNum) -> new Order(
rs.getLong("id"),
rs.getString("order_no"),
rs.getBigDecimal("amount")
));
}
private void logOrderToSecondary(Order order) {
String sql = "INSERT INTO order_logs (order_id, order_no, amount) VALUES (?, ?, ?)";
jdbcTemplate.update(sql, order.getId(), order.getOrderNo(), order.getAmount());
}
}
3. 高级配置:集成MyBatis与JPA
3.1 MyBatis多数据源配置
主数据源MyBatis配置:
// 表明这是一个配置类,Spring 会对其进行扫描并应用其中的配置信息
@Configuration
// 配置 MyBatis Mapper 接口的扫描路径和对应的 SqlSessionFactory 引用
// basePackages 指定要扫描的 Mapper 接口所在的包路径
// sqlSessionFactoryRef 指定使用的 SqlSessionFactory 的 Bean 名称
@MapperScan(
basePackages = "com.example.mapper.primary",
sqlSessionFactoryRef = "primarySqlSessionFactory"
)
public class PrimaryMyBatisConfig {
/**
* 创建主数据源的 Bean
* @return 主数据源实例
*/
// 将该方法返回的对象注册为一个 Spring Bean
@Bean
// 从配置文件中读取以 "spring.datasource.primary" 为前缀的配置信息来初始化数据源
@ConfigurationProperties(prefix = "spring.datasource.primary")
public DataSource primaryDataSource() {
// 使用 DataSourceBuilder 创建数据源实例
return DataSourceBuilder.create().build();
}
/**
* 创建主数据源对应的 SqlSessionFactory Bean
* @param dataSource 主数据源实例
* @return 主数据源对应的 SqlSessionFactory 实例
* @throws Exception 创建过程中可能抛出的异常
*/
// 将该方法返回的对象注册为一个 Spring Bean
@Bean
public SqlSessionFactory primarySqlSessionFactory(@Qualifier("primaryDataSource") DataSource dataSource)
throws Exception {
// 创建 SqlSessionFactoryBean 实例,用于创建 SqlSessionFactory
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
// 设置 SqlSessionFactory 使用的数据源
sessionFactory.setDataSource(dataSource);
// 设置 Mapper XML 文件的位置,使用 PathMatchingResourcePatternResolver 来查找匹配的资源
sessionFactory.setMapperLocations(
new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/primary/*.xml"));
// 获取并返回 SqlSessionFactory 实例
return sessionFactory.getObject();
}
/**
* 创建主数据源对应的 SqlSessionTemplate Bean
* @param sqlSessionFactory 主数据源对应的 SqlSessionFactory 实例
* @return 主数据源对应的 SqlSessionTemplate 实例
*/
// 将该方法返回的对象注册为一个 Spring Bean
@Bean
public SqlSessionTemplate primarySqlSessionTemplate(
@Qualifier("primarySqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
// 创建并返回 SqlSessionTemplate 实例,用于简化 MyBatis 的操作
return new SqlSessionTemplate(sqlSessionFactory);
}
}
从数据源MyBatis配置:
// 该注解表明这是一个 Spring 配置类,Spring 框架会自动扫描并处理这个类中的配置信息
@Configuration
// 此注解用于指定 MyBatis Mapper 接口的扫描范围和对应的 SqlSessionFactory 引用
// basePackages 表示要扫描的 Mapper 接口所在的基础包路径
// sqlSessionFactoryRef 指明使用的 SqlSessionFactory Bean 的名称
@MapperScan(
basePackages = "com.example.mapper.secondary",
sqlSessionFactoryRef = "secondarySqlSessionFactory"
)
public class SecondaryMyBatisConfig {
/**
* 创建并配置从数据源的 Bean
* @return 配置好的从数据源实例
*/
// 将该方法返回的对象注册为一个 Spring Bean
@Bean
// 从配置文件里读取以 "spring.datasource.secondary" 为前缀的配置项,用于初始化数据源
@ConfigurationProperties(prefix = "spring.datasource.secondary")
public DataSource secondaryDataSource() {
// 利用 DataSourceBuilder 创建数据源实例
return DataSourceBuilder.create().build();
}
/**
* 创建并配置从数据源对应的 SqlSessionFactory Bean
* @param dataSource 从数据源实例
* @return 配置好的从数据源对应的 SqlSessionFactory 实例
* @throws Exception 创建过程中可能出现的异常,如配置文件读取错误等
*/
// 将该方法返回的对象注册为一个 Spring Bean
@Bean
public SqlSessionFactory secondarySqlSessionFactory(@Qualifier("secondaryDataSource") DataSource dataSource)
throws Exception {
// 创建一个 SqlSessionFactoryBean 实例,用于构建 SqlSessionFactory
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
// 为 SqlSessionFactory 设置使用的数据源
sessionFactory.setDataSource(dataSource);
// 配置 Mapper XML 文件的位置,使用 PathMatchingResourcePatternResolver 来查找符合条件的资源
sessionFactory.setMapperLocations(
new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/secondary/*.xml"));
// 通过 SqlSessionFactoryBean 获取最终的 SqlSessionFactory 实例
return sessionFactory.getObject();
}
/**
* 创建并配置从数据源对应的 SqlSessionTemplate Bean
* @param sqlSessionFactory 从数据源对应的 SqlSessionFactory 实例
* @return 配置好的从数据源对应的 SqlSessionTemplate 实例
*/
// 将该方法返回的对象注册为一个 Spring Bean
@Bean
public SqlSessionTemplate secondarySqlSessionTemplate(
@Qualifier("secondarySqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
// 基于给定的 SqlSessionFactory 创建一个 SqlSessionTemplate 实例,简化 MyBatis 的操作
return new SqlSessionTemplate(sqlSessionFactory);
}
}
使用示例:
@Service
public class ProductService {
@Autowired
@Qualifier("primarySqlSessionTemplate")
private SqlSessionTemplate primarySqlSessionTemplate;
@Autowired
@Qualifier("secondarySqlSessionTemplate")
private SqlSessionTemplate secondarySqlSessionTemplate;
public void syncProduct(Long productId) {
// 从主数据源获取产品
ProductMapper primaryMapper = primarySqlSessionTemplate.getMapper(ProductMapper.class);
Product product = primaryMapper.selectById(productId);
// 同步到从数据源
ProductMapper secondaryMapper = secondarySqlSessionTemplate.getMapper(ProductMapper.class);
if (secondaryMapper.selectById(productId) == null) {
secondaryMapper.insert(product);
} else {
secondaryMapper.update(product);
}
}
}
3.2 JPA多数据源配置
主数据源JPA配置:
// 表明这是一个 Spring 配置类,Spring 会自动扫描并处理该类中的配置信息
@Configuration
// 启用 Spring 的事务管理功能,允许使用 @Transactional 注解来管理事务
@EnableTransactionManagement
// 启用 JPA 仓库的自动扫描和注册功能
// basePackages 指定要扫描的 JPA 仓库接口所在的包路径
// entityManagerFactoryRef 指定使用的实体管理器工厂的 Bean 名称
// transactionManagerRef 指定使用的事务管理器的 Bean 名称
@EnableJpaRepositories(
basePackages = "com.example.repository.primary",
entityManagerFactoryRef = "primaryEntityManagerFactory",
transactionManagerRef = "primaryTransactionManager"
)
public class PrimaryJpaConfig {
/**
* 创建主数据源的 Bean,并将其标记为主要的数据源 Bean
* @return 主数据源实例
*/
// 将该方法返回的对象注册为一个 Spring Bean
@Bean
// 标记该 Bean 为主要的 Bean,当有多个同类型的 Bean 时,优先使用该 Bean
@Primary
// 从配置文件中读取以 "spring.datasource.primary" 为前缀的配置信息来初始化数据源
@ConfigurationProperties(prefix = "spring.datasource.primary")
public DataSource primaryDataSource() {
// 使用 DataSourceBuilder 创建数据源实例
return DataSourceBuilder.create().build();
}
/**
* 创建主数据源对应的实体管理器工厂的 Bean,并将其标记为主要的实体管理器工厂 Bean
* @param builder 实体管理器工厂构建器
* @param dataSource 主数据源实例
* @return 主数据源对应的实体管理器工厂 Bean
*/
// 将该方法返回的对象注册为一个 Spring Bean
@Bean
// 标记该 Bean 为主要的 Bean,当有多个同类型的 Bean 时,优先使用该 Bean
@Primary
public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory(
EntityManagerFactoryBuilder builder,
@Qualifier("primaryDataSource") DataSource dataSource) {
// 使用实体管理器工厂构建器构建实体管理器工厂
return builder
// 设置数据源
.dataSource(dataSource)
// 指定要扫描的实体类所在的包路径
.packages("com.example.entity.primary")
// 设置持久化单元的名称
.persistenceUnit("primaryPersistenceUnit")
// 设置 JPA 的属性
.properties(jpaProperties())
.build();
}
/**
* 创建主数据源对应的事务管理器的 Bean,并将其标记为主要的事务管理器 Bean
* @param entityManagerFactory 主数据源对应的实体管理器工厂实例
* @return 主数据源对应的事务管理器实例
*/
// 将该方法返回的对象注册为一个 Spring Bean
@Bean
// 标记该 Bean 为主要的 Bean,当有多个同类型的 Bean 时,优先使用该 Bean
@Primary
public PlatformTransactionManager primaryTransactionManager(
@Qualifier("primaryEntityManagerFactory") EntityManagerFactory entityManagerFactory) {
// 创建 JPA 事务管理器实例
return new JpaTransactionManager(entityManagerFactory);
}
/**
* 配置 JPA 的属性
* @return 包含 JPA 属性的 Map
*/
private Map<String, Object> jpaProperties() {
// 创建一个用于存储 JPA 属性的 Map
Map<String, Object> props = new HashMap<>();
// 设置 Hibernate 使用的数据库方言为 MySQL 8
props.put("hibernate.dialect", "org.hibernate.dialect.MySQL8Dialect");
// 设置 Hibernate 在启动时自动更新数据库表结构
props.put("hibernate.hbm2ddl.auto", "update");
// 开启显示 SQL 语句的功能
props.put("hibernate.show_sql", true);
// 开启格式化 SQL 语句的功能,使 SQL 语句更易读
props.put("hibernate.format_sql", true);
return props;
}
}
从数据源JPA配置:
// 表明该类是一个 Spring 配置类,Spring 容器会自动扫描并加载其中的配置信息
@Configuration
// 启用 Spring 的事务管理功能,这样就可以在服务层等地方使用 @Transactional 注解来管理事务
@EnableTransactionManagement
// 开启 JPA 仓库的自动扫描和注册功能
// basePackages 指定要扫描的 JPA 仓库接口所在的包路径
// entityManagerFactoryRef 指定使用的实体管理器工厂的 Bean 名称
// transactionManagerRef 指定使用的事务管理器的 Bean 名称
@EnableJpaRepositories(
basePackages = "com.example.repository.secondary",
entityManagerFactoryRef = "secondaryEntityManagerFactory",
transactionManagerRef = "secondaryTransactionManager"
)
public class SecondaryJpaConfig {
/**
* 创建并配置从数据源的 Bean
* @return 配置好的从数据源实例
*/
// 将该方法返回的对象注册为一个 Spring Bean
@Bean
// 从配置文件里读取以 "spring.datasource.secondary" 为前缀的配置项,用于初始化数据源
@ConfigurationProperties(prefix = "spring.datasource.secondary")
public DataSource secondaryDataSource() {
// 利用 DataSourceBuilder 创建数据源实例
return DataSourceBuilder.create().build();
}
/**
* 创建并配置从数据源对应的实体管理器工厂的 Bean
* @param builder 用于构建实体管理器工厂的构建器
* @param dataSource 从数据源实例
* @return 配置好的从数据源对应的实体管理器工厂 Bean
*/
// 将该方法返回的对象注册为一个 Spring Bean
@Bean
public LocalContainerEntityManagerFactoryBean secondaryEntityManagerFactory(
EntityManagerFactoryBuilder builder,
@Qualifier("secondaryDataSource") DataSource dataSource) {
return builder
// 设置使用的数据源为从数据源
.dataSource(dataSource)
// 指定要扫描的实体类所在的包路径
.packages("com.example.entity.secondary")
// 设置持久化单元的名称
.persistenceUnit("secondaryPersistenceUnit")
// 设置 JPA 的相关属性
.properties(jpaProperties())
// 构建并返回实体管理器工厂 Bean
.build();
}
/**
* 创建并配置从数据源对应的事务管理器的 Bean
* @param entityManagerFactory 从数据源对应的实体管理器工厂实例
* @return 配置好的从数据源对应的事务管理器实例
*/
// 将该方法返回的对象注册为一个 Spring Bean
@Bean
public PlatformTransactionManager secondaryTransactionManager(
@Qualifier("secondaryEntityManagerFactory") EntityManagerFactory entityManagerFactory) {
// 创建一个基于 JPA 的事务管理器,并将从数据源对应的实体管理器工厂传入
return new JpaTransactionManager(entityManagerFactory);
}
/**
* 配置 JPA 的属性
* @return 包含 JPA 属性的 Map
*/
private Map<String, Object> jpaProperties() {
// 创建一个用于存储 JPA 属性的 Map
Map<String, Object> props = new HashMap<>();
// 设置 Hibernate 使用的数据库方言为 MySQL 8
props.put("hibernate.dialect", "org.hibernate.dialect.MySQL8Dialect");
// 设置 Hibernate 在启动时自动更新数据库表结构
props.put("hibernate.hbm2ddl.auto", "update");
// 开启显示 SQL 语句的功能
props.put("hibernate.show_sql", true);
// 开启格式化 SQL 语句的功能,使 SQL 语句更易读
props.put("hibernate.format_sql", true);
return props;
}
}
使用示例:
@Service
public class CustomerService {
@Autowired
@Qualifier("primaryTransactionManager")
private PlatformTransactionManager primaryTransactionManager;
@Autowired
@Qualifier("secondaryTransactionManager")
private PlatformTransactionManager secondaryTransactionManager;
@Autowired
private PrimaryCustomerRepository primaryCustomerRepository;
@Autowired
private SecondaryCustomerRepository secondaryCustomerRepository;
public void migrateCustomer(Long customerId) {
// 从主数据源获取客户
Customer customer = primaryCustomerRepository.findById(customerId)
.orElseThrow(() -> new RuntimeException("Customer not found"));
// 定义事务属性
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
// 在从数据源事务中保存客户
TransactionStatus status = secondaryTransactionManager.getTransaction(definition);
try {
secondaryCustomerRepository.save(customer);
secondaryTransactionManager.commit(status);
} catch (Exception e) {
secondaryTransactionManager.rollback(status);
throw e;
}
}
}
4. 事务管理:跨数据源事务处理
4.1 单数据源事务
在单数据源场景下,Spring的事务管理非常简单:
@Service
public class AccountService {
@Autowired
private AccountRepository accountRepository;
@Transactional // 使用默认事务管理器
public void transfer(Long fromId, Long toId, BigDecimal amount) {
Account fromAccount = accountRepository.findById(fromId)
.orElseThrow(() -> new RuntimeException("Account not found: " + fromId));
Account toAccount = accountRepository.findById(toId)
.orElseThrow(() -> new RuntimeException("Account not found: " + toId));
fromAccount.setBalance(fromAccount.getBalance().subtract(amount));
toAccount.setBalance(toAccount.getBalance().add(amount));
accountRepository.save(fromAccount);
accountRepository.save(toAccount);
}
}
4.2 多数据源事务挑战
多数据源事务面临的主要问题是分布式事务的挑战。Spring的@Transactional
注解默认只能管理单个事务管理器,无法直接协调多个数据源的事务。
解决方案对比:
方案 | 原理 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
JTA (Java Transaction API) | 使用全局事务协调器 | 强一致性 | 性能开销大,配置复杂 | 需要强一致性的金融系统 |
最终一致性 (Saga模式) | 通过补偿操作实现 | 高性能,松耦合 | 实现复杂,需要补偿逻辑 | 高并发,可接受短暂不一致 |
本地消息表 | 通过消息队列保证 | 可靠性高 | 需要额外表存储消息 | 需要可靠异步处理的场景 |
4.3 JTA实现多数据源事务
Atomikos配置示例:
// 表明这是一个 Spring 配置类,Spring 容器会自动扫描并加载该类中的配置信息
@Configuration
// 启用 Spring 的声明式事务管理功能,允许使用 @Transactional 注解来管理事务
@EnableTransactionManagement
public class JtaDataSourceConfig {
/**
* 创建并配置 UserTransactionManager Bean
* UserTransactionManager 用于管理用户事务,支持分布式事务处理
* @return 配置好的 UserTransactionManager 实例
* @throws SystemException 当系统出现异常时抛出
*/
// 定义一个 Bean,指定初始化方法为 init,销毁方法为 close
@Bean(initMethod = "init", destroyMethod = "close")
public UserTransactionManager userTransactionManager() throws SystemException {
// 创建 UserTransactionManager 实例
UserTransactionManager userTransactionManager = new UserTransactionManager();
// 设置是否强制关闭事务管理器,false 表示不强制关闭
userTransactionManager.setForceShutdown(false);
return userTransactionManager;
}
/**
* 创建并配置 UserTransaction Bean
* UserTransaction 用于表示用户发起的事务操作
* @return 配置好的 UserTransaction 实例
* @throws SystemException 当系统出现异常时抛出
*/
@Bean
public UserTransaction userTransaction() throws SystemException {
// 创建 UserTransactionImp 实例,它是 UserTransaction 的一个实现类
UserTransactionImp userTransactionImp = new UserTransactionImp();
// 设置事务的超时时间为 300 秒
userTransactionImp.setTransactionTimeout(300);
return userTransactionImp;
}
/**
* 创建并配置 JtaTransactionManager Bean
* JtaTransactionManager 是 Spring 提供的用于管理 JTA(Java Transaction API)事务的管理器
* @return 配置好的 JtaTransactionManager 实例
* @throws SystemException 当系统出现异常时抛出
*/
@Bean
public JtaTransactionManager transactionManager() throws SystemException {
// 创建 JtaTransactionManager 实例
JtaTransactionManager jtaTransactionManager = new JtaTransactionManager();
// 设置 UserTransaction,用于管理用户发起的事务
jtaTransactionManager.setUserTransaction(userTransaction());
// 设置 TransactionManager,用于管理事务的生命周期
jtaTransactionManager.setTransactionManager(userTransactionManager());
return jtaTransactionManager;
}
/**
* 创建并配置主数据源 Bean
* 主数据源使用 XA 数据源,支持分布式事务
* @return 配置好的主数据源实例
*/
// 将该 Bean 标记为主要的 Bean,当有多个数据源 Bean 时,默认使用该数据源
@Bean
@Primary
// 从配置文件中读取以 "spring.datasource.primary" 为前缀的配置信息
@ConfigurationProperties(prefix = "spring.datasource.primary")
public DataSource primaryDataSource() {
// 创建 MySQL 的 XA 数据源实例
MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource();
// 从环境变量中获取主数据源的 JDBC URL 并设置到 XA 数据源中
mysqlXaDataSource.setUrl(env.getProperty("spring.datasource.primary.jdbc-url"));
// 从环境变量中获取主数据源的用户名并设置到 XA 数据源中
mysqlXaDataSource.setUser(env.getProperty("spring.datasource.primary.username"));
// 从环境变量中获取主数据源的密码并设置到 XA 数据源中
mysqlXaDataSource.setPassword(env.getProperty("spring.datasource.primary.password"));
// 创建 Atomikos 的 XA 数据源包装器实例
AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
// 将 MySQL 的 XA 数据源设置到 Atomikos 的数据源包装器中
xaDataSource.setXaDataSource(mysqlXaDataSource);
// 为该数据源设置唯一的资源名称
xaDataSource.setUniqueResourceName("primaryDB");
// 设置数据源的连接池大小为 5
xaDataSource.setPoolSize(5);
return xaDataSource;
}
/**
* 创建并配置从数据源 Bean
* 从数据源使用 XA 数据源,支持分布式事务
* @return 配置好的从数据源实例
*/
@Bean
// 从配置文件中读取以 "spring.datasource.secondary" 为前缀的配置信息
@ConfigurationProperties(prefix = "spring.datasource.secondary")
public DataSource secondaryDataSource() {
// 创建 MySQL 的 XA 数据源实例
MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource();
// 从环境变量中获取从数据源的 JDBC URL 并设置到 XA 数据源中
mysqlXaDataSource.setUrl(env.getProperty("spring.datasource.secondary.jdbc-url"));
// 从环境变量中获取从数据源的用户名并设置到 XA 数据源中
mysqlXaDataSource.setUser(env.getProperty("spring.datasource.secondary.username"));
// 从环境变量中获取从数据源的密码并设置到 XA 数据源中
mysqlXaDataSource.setPassword(env.getProperty("spring.datasource.secondary.password"));
// 创建 Atomikos 的 XA 数据源包装器实例
AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
// 将 MySQL 的 XA 数据源设置到 Atomikos 的数据源包装器中
xaDataSource.setXaDataSource(mysqlXaDataSource);
// 为该数据源设置唯一的资源名称
xaDataSource.setUniqueResourceName("secondaryDB");
// 设置数据源的连接池大小为 5
xaDataSource.setPoolSize(5);
return xaDataSource;
}
}
使用JTA事务:
@Service
public class OrderInventoryService {
@Autowired
private OrderRepository orderRepository; // 使用主数据源
@Autowired
private InventoryRepository inventoryRepository; // 使用从数据源
@Transactional // 现在会使用JTA事务管理器
public void placeOrder(Order order) {
// 在主数据源中保存订单
orderRepository.save(order);
// 在从数据源中扣减库存
Inventory inventory = inventoryRepository.findByProductId(order.getProductId())
.orElseThrow(() -> new RuntimeException("Inventory not found"));
if (inventory.getStock() < order.getQuantity()) {
throw new RuntimeException("Insufficient inventory");
}
inventory.setStock(inventory.getStock() - order.getQuantity());
inventoryRepository.save(inventory);
}
}
4.4 Saga模式实现最终一致性
Saga实现示例:
@Service
public class OrderSagaService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private InventoryService inventoryService;
@Autowired
private SagaLogRepository sagaLogRepository;
public void createOrder(Order order) {
// 开启Saga事务
String sagaId = UUID.randomUUID().toString();
sagaLogRepository.save(new SagaLog(sagaId, "ORDER_CREATION", "STARTED"));
try {
// Step 1: 创建订单
order.setStatus(OrderStatus.PENDING);
orderRepository.save(order);
sagaLogRepository.save(new SagaLog(sagaId, "ORDER_CREATED", "SUCCESS"));
// Step 2: 扣减库存
inventoryService.decreaseStock(sagaId, order.getProductId(), order.getQuantity());
// Step 3: 完成订单
order.setStatus(OrderStatus.COMPLETED);
orderRepository.save(order);
sagaLogRepository.save(new SagaLog(sagaId, "ORDER_COMPLETED", "SUCCESS"));
} catch (Exception e) {
// 失败补偿
compensate(sagaId);
throw e;
}
}
private void compensate(String sagaId) {
List<SagaLog> logs = sagaLogRepository.findBySagaId(sagaId);
// 按相反顺序执行补偿操作
if (logs.stream().anyMatch(log -> "ORDER_COMPLETED".equals(log.getStep()))) {
// 如果订单已完成,需要取消订单
// 实现取消逻辑...
}
if (logs.stream().anyMatch(log -> "INVENTORY_DECREASED".equals(log.getStep()))) {
// 如果库存已扣减,需要恢复库存
inventoryService.compensateDecrease(sagaId);
}
if (logs.stream().anyMatch(log -> "ORDER_CREATED".equals(log.getStep()))) {
// 如果订单已创建,需要取消订单
orderRepository.findBySagaId(sagaId).ifPresent(order -> {
order.setStatus(OrderStatus.CANCELLED);
orderRepository.save(order);
});
}
sagaLogRepository.save(new SagaLog(sagaId, "SAGA_COMPENSATED", "COMPLETED"));
}
}
5. 性能优化与最佳实践
5.1 连接池配置优化
多数据源场景下,连接池配置尤为重要。以下是HikariCP的优化配置示例:
spring:
datasource:
primary:
jdbc-url: jdbc:mysql://localhost:3306/primary_db
username: root
password: root123
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
pool-name: PrimaryHikariPool
maximum-pool-size: 20
minimum-idle: 5
idle-timeout: 30000
max-lifetime: 1800000
connection-timeout: 30000
connection-test-query: SELECT 1
secondary:
jdbc-url: jdbc:mysql://localhost:3306/secondary_db
username: root
password: root123
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
pool-name: SecondaryHikariPool
maximum-pool-size: 15
minimum-idle: 3
idle-timeout: 30000
max-lifetime: 1800000
connection-timeout: 30000
connection-test-query: SELECT 1
关键参数解释:
参数 | 说明 | 推荐值 | 设置依据 |
---|---|---|---|
maximum-pool-size | 最大连接数 | 10-50 | 根据数据库性能和并发请求量 |
minimum-idle | 最小空闲连接 | 3-10 | 保证快速响应,但不过多占用资源 |
idle-timeout | 空闲连接超时时间(ms) | 30000 | 避免长时间空闲连接 |
max-lifetime | 连接最大生命周期(ms) | 1800000 | 定期刷新连接,防止连接老化 |
connection-timeout | 获取连接超时时间(ms) | 30000 | 避免长时间等待 |
5.2 读写分离实现
基于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
@Transactional
public Product getProduct(Long id) {
// 使用从数据源(读)
return productRepository.findById(id).orElse(null);
}
@ReadOnly
@Transactional
public List<Product> listProducts() {
// 使用从数据源(读)
return productRepository.findAll();
}
}
5.3 多数据源监控
集成Druid监控:
// 表明这是一个 Spring 配置类,Spring 框架会自动扫描并处理该类中的配置信息
@Configuration
public class DruidConfig {
/**
* 配置 Druid 监控的 Servlet 注册 Bean
* 该 Servlet 用于提供 Druid 监控界面,方便对数据源进行监控和管理
* @return 配置好的 Servlet 注册 Bean
*/
@Bean
public ServletRegistrationBean<StatViewServlet> druidServlet() {
// 创建一个 Servlet 注册 Bean,将 StatViewServlet 注册到 "/druid/*" 路径下
ServletRegistrationBean<StatViewServlet> servletRegistrationBean =
new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
// 设置允许访问 Druid 监控界面的 IP 白名单,这里只允许本地 127.0.0.1 访问
servletRegistrationBean.addInitParameter("allow", "127.0.0.1");
// 设置登录 Druid 监控界面的用户名
servletRegistrationBean.addInitParameter("loginUsername", "admin");
// 设置登录 Druid 监控界面的密码
servletRegistrationBean.addInitParameter("loginPassword", "admin");
// 设置是否允许重置监控数据,这里设置为不允许
servletRegistrationBean.addInitParameter("resetEnable", "false");
return servletRegistrationBean;
}
/**
* 配置 Druid 监控的过滤器注册 Bean
* 该过滤器用于收集和监控 Web 请求的相关信息
* @return 配置好的过滤器注册 Bean
*/
@Bean
public FilterRegistrationBean<WebStatFilter> filterRegistrationBean() {
// 创建一个过滤器注册 Bean,将 WebStatFilter 注册到所有请求路径上
FilterRegistrationBean<WebStatFilter> filterRegistrationBean =
new FilterRegistrationBean<>(new WebStatFilter());
// 设置过滤器的拦截路径,这里拦截所有请求
filterRegistrationBean.addUrlPatterns("/*");
// 设置不需要进行监控的资源路径,这些资源不会被 WebStatFilter 拦截
filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
return filterRegistrationBean;
}
/**
* 创建主数据源的 Bean
* 从配置文件中读取以 "spring.datasource.primary" 为前缀的配置信息来初始化 Druid 数据源
* @return 主数据源实例
*/
@Bean
@ConfigurationProperties(prefix = "spring.datasource.primary")
public DataSource primaryDataSource() {
// 使用 DruidDataSourceBuilder 创建 Druid 数据源实例
return DruidDataSourceBuilder.create().build();
}
/**
* 创建从数据源的 Bean
* 从配置文件中读取以 "spring.datasource.secondary" 为前缀的配置信息来初始化 Druid 数据源
* @return 从数据源实例
*/
@Bean
@ConfigurationProperties(prefix = "spring.datasource.secondary")
public DataSource secondaryDataSource() {
// 使用 DruidDataSourceBuilder 创建 Druid 数据源实例
return DruidDataSourceBuilder.create().build();
}
}
监控指标说明:
指标 | 说明 | 健康值范围 |
---|---|---|
ActiveCount | 活跃连接数 | < maximum-pool-size |
WaitingThreadCount | 等待连接的线程数 | 越少越好,最好为0 |
ConnectionHoldTime | 连接持有时间 | 根据业务,通常<1s |
ExecuteCount | 执行次数 | - |
ErrorCount | 错误次数 | 错误率<1% |
FetchRowCount | 获取行数 | - |
6. 常见问题与解决方案
6.1 典型问题排查表
问题现象 | 可能原因 | 解决方案 |
---|---|---|
无法启动,报错"more than one primary" | 多个@Primary注解冲突 | 确保每个数据源相关配置中只有一个@Primary |
动态切换数据源无效 | 线程污染或未正确清理 | 确保在finally块中清理数据源上下文 |
事务不生效 | 使用了错误的事务管理器 | 确保每个Repository使用对应的事务管理器 |
性能低下 | 连接池配置不合理 | 优化连接池参数,监控连接使用情况 |
跨数据源事务不一致 | 未使用分布式事务解决方案 | 引入JTA或Saga模式 |
6.2 数据源切换失败案例分析
问题描述:
在动态数据源切换场景下,有时切换不生效,仍然使用默认数据源。
原因分析:
- 数据源切换代码被异常绕过,未执行
- 线程池场景下线程复用导致上下文污染
- 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();
}
}
6.3 多数据源与缓存集成
当多数据源与缓存(如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() {
// 清除所有用户缓存
}
}
7. 实战案例:电商系统多数据源设计
7.1 场景描述
假设我们有一个电商系统,需要处理以下数据源:
- 主库(primary):处理订单、支付等核心业务
- 报表库(reporting):处理数据分析、报表生成
- 物流库(shipping):处理物流、配送信息
7.2 配置实现
application.yml:
spring:
datasource:
primary:
jdbc-url: jdbc:mysql://localhost:3306/ecommerce_primary
username: ec_user
password: ec_password
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
pool-name: PrimaryHikariPool
maximum-pool-size: 20
reporting:
jdbc-url: jdbc:mysql://reporting-host:3306/ecommerce_reporting
username: reporting_user
password: reporting_pwd
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
pool-name: ReportingHikariPool
maximum-pool-size: 15
read-only: true
shipping:
jdbc-url: jdbc:postgresql://shipping-host:5432/ecommerce_shipping
username: shipping_user
password: shipping_pwd
driver-class-name: org.postgresql.Driver
hikari:
pool-name: ShippingHikariPool
maximum-pool-size: 10
数据源配置类:
@Configuration
public class EcommerceDataSourceConfig {
public enum DataSourceType {
PRIMARY, REPORTING, SHIPPING
}
@Bean
@Primary
@ConfigurationProperties(prefix = "spring.datasource.primary")
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.reporting")
public DataSource reportingDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.shipping")
public DataSource shippingDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public DataSource dynamicDataSource() {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceType.PRIMARY, primaryDataSource());
targetDataSources.put(DataSourceType.REPORTING, reportingDataSource());
targetDataSources.put(DataSourceType.SHIPPING, shippingDataSource());
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setTargetDataSources(targetDataSources);
dynamicDataSource.setDefaultTargetDataSource(primaryDataSource());
return dynamicDataSource;
}
// 动态数据源实现
private static class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return EcommerceDataSourceContextHolder.getDataSourceType();
}
}
}
上下文持有类:
public class EcommerceDataSourceContextHolder {
private static final ThreadLocal<EcommerceDataSourceConfig.DataSourceType> CONTEXT_HOLDER =
new ThreadLocal<>();
public static void setDataSourceType(EcommerceDataSourceConfig.DataSourceType dsType) {
CONTEXT_HOLDER.set(dsType);
}
public static EcommerceDataSourceConfig.DataSourceType getDataSourceType() {
return CONTEXT_HOLDER.get();
}
public static void clearDataSourceType() {
CONTEXT_HOLDER.remove();
}
}
7.3 业务实现示例
订单服务:
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private OrderReportingRepository orderReportingRepository;
@Transactional
public void createOrder(Order order) {
// 使用主数据源
orderRepository.save(order);
try {
// 切换到报表数据源
EcommerceDataSourceContextHolder.setDataSourceType(
EcommerceDataSourceConfig.DataSourceType.REPORTING);
// 保存订单摘要到报表库
OrderSummary summary = new OrderSummary();
summary.setOrderId(order.getId());
summary.setAmount(order.getAmount());
summary.setCustomerId(order.getCustomerId());
summary.setOrderDate(new Date());
orderReportingRepository.save(summary);
} finally {
EcommerceDataSourceContextHolder.clearDataSourceType();
}
}
@Transactional(readOnly = true)
public BigDecimal getDailySales() {
try {
// 使用报表数据源
EcommerceDataSourceContextHolder.setDataSourceType(
EcommerceDataSourceConfig.DataSourceType.REPORTING);
Date today = new Date();
return orderReportingRepository.sumAmountByDate(today);
} finally {
EcommerceDataSourceContextHolder.clearDataSourceType();
}
}
}
物流服务:
@Service
public class ShippingService {
@Autowired
private ShippingRepository shippingRepository;
@Transactional
public void scheduleShipping(Long orderId, String address) {
try {
// 使用物流数据源
EcommerceDataSourceContextHolder.setDataSourceType(
EcommerceDataSourceConfig.DataSourceType.SHIPPING);
Shipping shipping = new Shipping();
shipping.setOrderId(orderId);
shipping.setAddress(address);
shipping.setStatus("PENDING");
shipping.setEstimatedDelivery(new Date(System.currentTimeMillis() + 3 * 24 * 60 * 60 * 1000));
shippingRepository.save(shipping);
} finally {
EcommerceDataSourceContextHolder.clearDataSourceType();
}
}
}
8. 总结与扩展
8.1 技术选型建议
场景 | 推荐方案 | 理由 |
---|---|---|
简单多数据源,无交叉访问 | 独立配置多个数据源 | 简单直接,易于维护 |
需要动态切换数据源 | AbstractRoutingDataSource | 灵活,可运行时决定数据源 |
需要强一致性事务 | JTA(XA) | 保证ACID,但性能较低 |
高并发,最终一致性可接受 | Saga模式 | 高性能,松耦合 |
读写分离 | AOP+注解方式 | 透明化,对业务代码侵入小 |
8.2 未来演进方向
-
多数据源与微服务:
- 考虑将部分数据源拆分为独立微服务
- 使用GraphQL聚合多个数据源的数据
-
云原生多数据源:
- 使用Service Mesh管理数据源连接
- 利用云数据库的Proxy功能简化配置
-
数据 federation:
- 使用Apache Calcite等工具实现虚拟数据库
- 对应用透明地访问多个数据源
关注不关注,你自己决定(但正确的决定只有一个)。
喜欢的点个关注,想了解更多的可以关注微信公众号 “Eric的技术杂货库” ,提供更多的干货以及资料下载保存!