JPA多数据源实战案例详解——告别硬编码,拥抱动态路由

📚 目录导读
- 多数据源场景的痛点:为什么你离不开它?
- 核心设计思想:AbstractRoutingDataSource与ThreadLocal的黄金组合
- 完整案例演示:Spring Boot + JPA + 多数据源(读写分离)
- 常见坑与避坑指南(附代码级解决方案)
- 高频面试问答精讲(含性能优化建议)
多数据源场景的痛点:为什么你离不开它?
在企业级应用中,单一数据源往往无法满足需求,常见的场景包括:
- 读写分离:主库负责写,从库负责读,降低主库压力。
- 多租户系统:每个租户独立数据库,隔离数据安全。
- 分库分表:按业务域拆分,如订单库、用户库、日志库。
- 异构数据源:同时连接MySQL、Oracle或PostgreSQL,且需要JPA统一管理。
如果每个数据源都配置一套独立的EntityManagerFactory和TransactionManager,代码将变得臃肿,且无法实现“动态切换”。JPA多数据源方案便应运而生。
核心设计思想:AbstractRoutingDataSource与ThreadLocal的黄金组合
原理揭秘:Spring提供了AbstractRoutingDataSource,它本身是一个DataSource的代理,我们通过重写其determineCurrentLookupKey()方法,结合ThreadLocal存储当前线程的数据源标识,即可实现在同一事务中动态路由到不同数据库。
关键优势:
- 无缝融入Spring事务管理。
- 无需修改JPA的
Repository接口,对业务代码零侵入。 - 切换粒度可到服务方法级别,灵活且高效。
代码骨架:
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
}
完整案例演示:Spring Boot + JPA + 多数据源(读写分离)
项目依赖(pom.xml)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
配置文件(application.yml)
spring:
datasource:
primary:
url: jdbc:mysql://localhost:3306/write_db
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
secondary:
url: jdbc:mysql://localhost:3306/read_db
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL8Dialect
核心配置类:多数据源装配
@Configuration
public class DataSourceConfig {
@Bean("primaryDataSource")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.primary")
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean("secondaryDataSource")
@ConfigurationProperties(prefix = "spring.datasource.secondary")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean("dynamicDataSource")
public DataSource dynamicDataSource(@Qualifier("primaryDataSource") DataSource primary,
@Qualifier("secondaryDataSource") DataSource secondary) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("primary", primary);
targetDataSources.put("secondary", secondary);
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setDefaultTargetDataSource(primary);
dynamicDataSource.setTargetDataSources(targetDataSources);
return dynamicDataSource;
}
@Bean
public PlatformTransactionManager transactionManager(@Qualifier("dynamicDataSource") DataSource ds) {
return new JpaTransactionManager(Objects.requireNonNull(entityManagerFactory(ds).getObject()));
}
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(@Qualifier("dynamicDataSource") DataSource ds) {
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
vendorAdapter.setGenerateDdl(true);
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(vendorAdapter);
factory.setPackagesToScan("com.example.entity");
factory.setDataSource(ds);
return factory;
}
}
数据源切换工具类
public class DataSourceContextHolder {
private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();
public static void set(String type) { CONTEXT.set(type); }
public static String get() { return CONTEXT.get(); }
public static void clear() { CONTEXT.remove(); }
}
业务层注解式切换
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@ReadOnly // 自定义注解,见下方AOP
public User findById(Long id) {
return userRepository.findById(id).orElse(null);
}
@Transactional
public User saveUser(User user) {
return userRepository.save(user);
}
}
AOP切面实现:
@Aspect
@Component
public class DataSourceAspect {
@Before("@annotation(readOnly)")
public void setReadOnly(ReadOnly readOnly) {
DataSourceContextHolder.set("secondary");
}
@After("@annotation(readOnly)")
public void clear() {
DataSourceContextHolder.clear();
}
}
常见坑与避坑指南(附解决方案)
-
事务失效:
@Transactional必须在同一个线程中生效,避免跨Service调用时DataSource被提前清除。解决:将切面顺序调低(@Order(1))或使用TransactionSynchronizationManager注册回调。 -
JPA缓存冲突:多数据源下,一级缓存(持久化上下文)可能混乱。解决:为
EntityManager明确指定PersistenceContext(unitName = "primary")。 -
动态切换不生效:检查
determineCurrentLookupKey()是否被正确调用,并确保AbstractRoutingDataSource被标注为@Primary且未覆盖原DataSource。
高频面试问答精讲
Q1:AbstractRoutingDataSource与DynamicDataSource的区别?
AbstractRoutingDataSource是Spring提供的抽象基类,默认基于查询键路由;DynamicDataSource是我们自定义的子类,复写determineCurrentLookupKey实现动态索引,没有本质区别,前者是框架支持,后者是具体业务实现。
Q2:读写分离时,如何保证事务的强一致性?
建议强制写操作走主库,读操作走从库,对于强一致场景,可在Service层使用
@Transactional(readOnly = false)强制主库读取;或引入@TransactionManager指定特定事务管理器。
Q3:多数据源下,JPA的ddl-auto如何配置?
建议对从库设置
ddl-auto: none,避免重复建表;主库可保留update,确保packagesToScan包含所有实体类。
Q4:性能优化建议?
- 为不同数据源配置连接池独立参数(如最大连接数)。
- 使用
HikariCP默认连接池,并开启read-only连接属性。- 在AOP切面中减少
ThreadLocal的读写频率,可用@Around替代。
JPA多数据源不是魔法,而是Spring设计思想的巧妙组合,掌握AbstractRoutingDataSource + ThreadLocal,你就能在复杂架构中游刃有余,实践是检验真理的唯一标准,建议在本地搭建两个MySQL实例,跑通上述代码——技术深度,往往藏在最朴实的案例里。