公司动态
Spring Data REST多数据源配置与实战指南
1. Spring Data REST多数据源支持的核心挑战当我们需要在单个Spring Boot应用中同时连接多个数据库时传统的单数据源配置方式就捉襟见肘了。Spring Data REST作为构建在Spring Data之上的RESTful服务框架其默认配置是针对单一数据源设计的。要实现多数据源支持我们需要解决几个关键问题如何定义多个DataSource bean并避免冲突如何为不同实体指定对应的Repository和数据源如何保持事务管理的正确性如何确保REST端点能正确路由到对应的Repository在实际项目中我遇到过最典型的场景是需要同时访问业务数据库和报表数据库或者需要连接不同供应商的数据库系统。下面我将分享经过多个生产项目验证的完整解决方案。2. 基础环境配置与数据源定义2.1 依赖配置首先确保pom.xml中包含必要的依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-rest/artifactId /dependency !-- 使用HikariCP作为连接池 -- dependency groupIdcom.zaxxer/groupId artifactIdHikariCP/artifactId /dependency2.2 多数据源配置类创建主数据源配置假设使用MySQLConfiguration EnableTransactionManagement EnableJpaRepositories( basePackages com.example.primary, entityManagerFactoryRef primaryEntityManagerFactory, transactionManagerRef primaryTransactionManager ) public class PrimaryDataSourceConfig { Primary Bean(name primaryDataSource) ConfigurationProperties(prefix spring.datasource.primary) public DataSource primaryDataSource() { return DataSourceBuilder.create().type(HikariDataSource.class).build(); } Primary Bean(name primaryEntityManagerFactory) public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory( EntityManagerFactoryBuilder builder, Qualifier(primaryDataSource) DataSource dataSource) { return builder .dataSource(dataSource) .packages(com.example.primary.model) .persistenceUnit(primary) .properties(jpaProperties()) .build(); } Primary Bean(name primaryTransactionManager) public PlatformTransactionManager primaryTransactionManager( Qualifier(primaryEntityManagerFactory) EntityManagerFactory entityManagerFactory) { return new JpaTransactionManager(entityManagerFactory); } private MapString, Object jpaProperties() { MapString, Object props new HashMap(); props.put(hibernate.hbm2ddl.auto, update); props.put(hibernate.dialect, org.hibernate.dialect.MySQL8Dialect); return props; } }创建从数据源配置假设使用PostgreSQLConfiguration EnableTransactionManagement EnableJpaRepositories( basePackages com.example.secondary, entityManagerFactoryRef secondaryEntityManagerFactory, transactionManagerRef secondaryTransactionManager ) public class SecondaryDataSourceConfig { Bean(name secondaryDataSource) ConfigurationProperties(prefix spring.datasource.secondary) public DataSource secondaryDataSource() { return DataSourceBuilder.create().type(HikariDataSource.class).build(); } Bean(name secondaryEntityManagerFactory) public LocalContainerEntityManagerFactoryBean secondaryEntityManagerFactory( EntityManagerFactoryBuilder builder, Qualifier(secondaryDataSource) DataSource dataSource) { return builder .dataSource(dataSource) .packages(com.example.secondary.model) .persistenceUnit(secondary) .properties(jpaProperties()) .build(); } Bean(name secondaryTransactionManager) public PlatformTransactionManager secondaryTransactionManager( Qualifier(secondaryEntityManagerFactory) EntityManagerFactory entityManagerFactory) { return new JpaTransactionManager(entityManagerFactory); } private MapString, Object jpaProperties() { MapString, Object props new HashMap(); props.put(hibernate.hbm2ddl.auto, update); props.put(hibernate.dialect, org.hibernate.dialect.PostgreSQL10Dialect); return props; } }对应的application.yml配置spring: datasource: primary: jdbc-url: jdbc:mysql://localhost:3306/primary_db username: root password: mysqlpass driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 10 secondary: jdbc-url: jdbc:postgresql://localhost:5432/secondary_db username: postgres password: postgrespass driver-class-name: org.postgresql.Driver hikari: maximum-pool-size: 5关键点必须使用Primary标记主数据源的相关bean否则Spring无法确定默认使用哪个数据源。同时要注意不同数据源的连接池配置需要根据实际负载情况进行调优。3. 实体与Repository的组织策略3.1 实体类分包管理为了清晰区分不同数据源对应的实体建议按数据源分包src/main/java ├── com.example.primary │ ├── model │ │ ├── User.java │ │ └── Order.java │ └── repository │ ├── UserRepository.java │ └── OrderRepository.java └── com.example.secondary ├── model │ ├── Report.java │ └── AuditLog.java └── repository ├── ReportRepository.java └── AuditLogRepository.java3.2 Repository接口定义主数据源的Repository示例RepositoryRestResource(path users) public interface UserRepository extends JpaRepositoryUser, Long { // 自定义查询方法 ListUser findByStatus(String status); }从数据源的Repository示例RepositoryRestResource(path reports) public interface ReportRepository extends JpaRepositoryReport, UUID { RestResource(path byType, rel type) PageReport findByReportType(Param(type) String type, Pageable pageable); }3.3 跨数据源事务处理当需要跨数据源操作时可以使用JTA或ChainedTransactionManager。对于大多数场景我更推荐使用最终一致性模式Service public class OrderReportService { Transactional(primaryTransactionManager) public void processOrder(Order order) { // 操作主数据库 orderRepository.save(order); // 异步处理报表 eventPublisher.publishEvent(new ReportGenerationEvent(order.getId())); } Transactional(secondaryTransactionManager) Async EventListener public void handleReportEvent(ReportGenerationEvent event) { // 操作从数据库 Report report generateReport(event.getOrderId()); reportRepository.save(report); } }经验之谈尽量避免跨数据源的分布式事务可以通过事件驱动重试机制实现最终一致性。如果必须使用分布式事务考虑引入Atomikos等JTA实现。4. Spring Data REST的定制化配置4.1 基础路径配置为了避免不同数据源的端点冲突可以在application.yml中配置基础路径spring: data: rest: base-path: /api detection-strategy: annotated4.2 自定义RepositoryRestConfigurer通过实现RepositoryRestConfigurer接口可以精细控制每个Repository的REST映射Configuration public class CustomRestMvcConfiguration implements RepositoryRestConfigurer { Override public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) { config.withEntityLookup() .forRepository(UserRepository.class) .withIdMapping(User::getId) .withLookup(UserRepository::findById); config.exposeIdsFor(Order.class); // 配置CORS config.getCorsRegistry() .addMapping(/api/**) .allowedOrigins(*) .allowedMethods(GET, POST, PUT, DELETE); } }4.3 处理关联关系当实体之间存在跨数据源关联时不能使用JPA的ManyToOne等注解。可以采用以下策略使用DTO模式在服务层组装数据存储关联ID通过服务方法查询关联实体使用事件驱动架构维护数据一致性示例DTO模式RestController RequestMapping(/api/orders) public class OrderController { GetMapping(/{id}/withUser) public OrderWithUserDTO getOrderWithUser(PathVariable Long id) { Order order orderRepository.findById(id).orElseThrow(); User user userService.getUserById(order.getUserId()); return new OrderWithUserDTO(order, user); } }5. 性能优化与生产实践5.1 连接池配置建议针对不同负载特点的数据源应该采用不同的连接池配置spring: datasource: primary: hikari: maximum-pool-size: 20 minimum-idle: 5 idle-timeout: 30000 max-lifetime: 1800000 connection-timeout: 30000 secondary: hikari: maximum-pool-size: 10 minimum-idle: 2 idle-timeout: 60000 max-lifetime: 1200000 connection-timeout: 600005.2 监控与健康检查配置Actuator端点监控多数据源状态management: endpoints: web: exposure: include: health,info,metrics endpoint: health: show-details: always db: enabled: true自定义健康检查指标Component public class DataSourceHealthIndicator implements HealthIndicator { Qualifier(primaryDataSource) Autowired private DataSource primaryDataSource; Qualifier(secondaryDataSource) Autowired private DataSource secondaryDataSource; Override public Health health() { return Health.up() .withDetail(primary, checkDataSource(primaryDataSource)) .withDetail(secondary, checkDataSource(secondaryDataSource)) .build(); } private String checkDataSource(DataSource dataSource) { try (Connection conn dataSource.getConnection()) { return OK - conn.getMetaData().getDatabaseProductName(); } catch (Exception e) { return DOWN - e.getMessage(); } } }5.3 常见问题排查启动时报Bean创建冲突错误确保每个数据源配置类使用不同的bean名称检查Primary注解是否正确应用在主数据源上事务不生效确认Transactional注解指定了正确的事务管理器检查方法是否是public的Spring AOP要求REST端点404确认RepositoryRestResource的path没有冲突检查实体类是否在正确的包路径下性能问题检查连接池配置是否合理使用Spring Data REST的分页功能避免大量数据查询6. 高级场景扩展6.1 动态数据源路由对于需要运行时动态切换数据源的场景可以结合AbstractRoutingDataSource实现public class DynamicDataSourceRouter extends AbstractRoutingDataSource { Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.getDataSourceType(); } } public class DataSourceContextHolder { private static final ThreadLocalString contextHolder new ThreadLocal(); public static void setDataSourceType(String dataSourceType) { contextHolder.set(dataSourceType); } public static String getDataSourceType() { return contextHolder.get(); } public static void clearDataSourceType() { contextHolder.remove(); } } // 使用示例 Service public class MultiTenantService { public void processForTenant(String tenantId) { try { DataSourceContextHolder.setDataSourceType(tenantId); // 执行业务逻辑 } finally { DataSourceContextHolder.clearDataSourceType(); } } }6.2 与MyBatis集成如果需要混合使用JPA和MyBatis可以这样配置Configuration MapperScan(basePackages com.example.mapper, sqlSessionFactoryRef mybatisSqlSessionFactory) public class MyBatisConfig { Bean(name mybatisSqlSessionFactory) public SqlSessionFactory mybatisSqlSessionFactory( Qualifier(secondaryDataSource) DataSource dataSource) throws Exception { SqlSessionFactoryBean sessionFactory new SqlSessionFactoryBean(); sessionFactory.setDataSource(dataSource); sessionFactory.setTypeAliasesPackage(com.example.mapper.model); return sessionFactory.getObject(); } }6.3 测试策略多数据源环境下的测试需要特殊处理DataJpaTest(includeFilters ComponentScan.Filter( type FilterType.ASSIGNABLE_TYPE, classes {PrimaryDataSourceConfig.class})) AutoConfigureTestDatabase(replace Replace.NONE) Transactional(primaryTransactionManager) public class PrimaryRepositoryTest { Autowired private UserRepository userRepository; Test public void testUserSave() { User user new User(test); userRepository.save(user); assertNotNull(user.getId()); } } // 从数据源测试 DataJpaTest(includeFilters ComponentScan.Filter( type FilterType.ASSIGNABLE_TYPE, classes {SecondaryDataSourceConfig.class})) AutoConfigureTestDatabase(replace Replace.NONE) Transactional(secondaryTransactionManager) public class SecondaryRepositoryTest { // 测试代码 }在实际项目中我通常会为每个数据源创建独立的测试配置类避免测试时的配置冲突。同时建议使用Testcontainers进行集成测试确保多数据源在实际数据库环境下的行为符合预期。