JPA多数据源案例

wen java案例 2

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

JPA多数据源案例

📚 目录导读

  • 多数据源场景的痛点:为什么你离不开它?
  • 核心设计思想:AbstractRoutingDataSource与ThreadLocal的黄金组合
  • 完整案例演示:Spring Boot + JPA + 多数据源(读写分离)
  • 常见坑与避坑指南(附代码级解决方案)
  • 高频面试问答精讲(含性能优化建议)

多数据源场景的痛点:为什么你离不开它?

在企业级应用中,单一数据源往往无法满足需求,常见的场景包括:

  • 读写分离:主库负责写,从库负责读,降低主库压力。
  • 多租户系统:每个租户独立数据库,隔离数据安全。
  • 分库分表:按业务域拆分,如订单库、用户库、日志库。
  • 异构数据源:同时连接MySQL、Oracle或PostgreSQL,且需要JPA统一管理。

如果每个数据源都配置一套独立的EntityManagerFactoryTransactionManager,代码将变得臃肿,且无法实现“动态切换”。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();
    }
}

常见坑与避坑指南(附解决方案)

  1. 事务失效@Transactional必须在同一个线程中生效,避免跨Service调用时DataSource被提前清除。解决:将切面顺序调低(@Order(1))或使用TransactionSynchronizationManager注册回调。

  2. JPA缓存冲突:多数据源下,一级缓存(持久化上下文)可能混乱。解决:为EntityManager明确指定PersistenceContext(unitName = "primary")

  3. 动态切换不生效:检查determineCurrentLookupKey()是否被正确调用,并确保AbstractRoutingDataSource被标注为@Primary且未覆盖原DataSource。


高频面试问答精讲

Q1:AbstractRoutingDataSourceDynamicDataSource的区别?

AbstractRoutingDataSource是Spring提供的抽象基类,默认基于查询键路由;DynamicDataSource是我们自定义的子类,复写determineCurrentLookupKey实现动态索引,没有本质区别,前者是框架支持,后者是具体业务实现。

Q2:读写分离时,如何保证事务的强一致性?

建议强制写操作走主库,读操作走从库,对于强一致场景,可在Service层使用@Transactional(readOnly = false)强制主库读取;或引入@TransactionManager指定特定事务管理器。

Q3:多数据源下,JPA的ddl-auto如何配置?

建议对从库设置ddl-auto: none,避免重复建表;主库可保留update,确保packagesToScan包含所有实体类。

Q4:性能优化建议?

  1. 为不同数据源配置连接池独立参数(如最大连接数)。
  2. 使用HikariCP默认连接池,并开启read-only连接属性。
  3. 在AOP切面中减少ThreadLocal的读写频率,可用@Around替代。

JPA多数据源不是魔法,而是Spring设计思想的巧妙组合,掌握AbstractRoutingDataSource + ThreadLocal,你就能在复杂架构中游刃有余,实践是检验真理的唯一标准,建议在本地搭建两个MySQL实例,跑通上述代码——技术深度,往往藏在最朴实的案例里。

抱歉,评论功能暂时关闭!