Spring Data JPA 之 多数据源配置

18 生产环境多数据源的处理方法

⼯作中我们时常会遇到跨数据库操作的情况,这时候就需要配置多数据源,那么如何配置呢?常⽤的⽅式及其背后的原理⽀撑是什么呢?我们下⾯来了解⼀下

18.1 第一种方式:@Configuration 配置方法

这种⽅式的主要思路是,不同 Package 下⾯的实体和 Repository 采⽤不同的 Datasource。所以我们改造⼀下我们的 example ⽬录结构,来看看不同 Repositories 的数据源是怎么处理的。

18.1.1 通过多个@Configuration 的配置方法

第⼀步:规划 Entity 和 Repository 的⽬录结构,为了⽅便配置多数据源。

com.zzn.master 创建实体类 MasterUser 和 MasterUserRepository

@Entity
@Data
public class MasterUser {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    protected Long id;
    private String name;
    private String email;
    private Integer age;
}
public interface MasterUserRepository extends JpaRepository<MasterUser, Long> {
}

com.zzn.slave 创建实体类 SlaveUser 和 SlaveUserRepository

@Entity
@Data
public class SlaveUser {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    protected Long id;
    private String name;
    private String email;
    private Integer age;
}
public interface SlaveUserRepository extends JpaRepository<SlaveUser, Long> {
}

我们把实体和 Repository 分别放到了 naster 和 slave 两个⽬录⾥⾯,这时我们假设 master 是 MySQL,User 表在 master 数据源⾥⾯,那么我们需要配置⼀个 Master DataSource 的 Configuration 类,并且在⾥⾯配置 DataSource、TransactionManager 和 EntityManager。

第⼆步:配置 MasterDataSourceConfig 类。

⽬录结构调整完之后,接下来我们开始配置数据源,完整代码如下:

@Configuration
@EnableTransactionManagement // 开启事务
@EnableJpaRepositories(  // 利⽤ EnableJpaRepositories 配置哪些包下⾯的 Repositories,采⽤哪个 EntityManagerFactory 和哪个 TransactionManagement
        basePackages = {"com.zzn.master"},// master 数据源的 repository 的包路径
        entityManagerFactoryRef = "masterEntityManagerFactory",// 改变 master 数据源的 EntityManagerFactory 的默认值,改为 masterEntityManagerFactory
        transactionManagerRef = "masterTransactionManager" // 改变 master 数据源的 TransactionManagement 的默认值,masterTransactionManager
)
public class MasterDataSourceConfig {

    /**
     * 指定 master 数据源的 dataSource 配置
     *
     * @return master 数据源配置
     */
    @Primary
    @Bean(name = "masterDataSourceProperties")
    @ConfigurationProperties("spring.datasource.master") // master 数据源的配置前缀采⽤ spring.datasource.master
    public DataSourceProperties dataSourceProperties() {
        return new DataSourceProperties();
    }

    /**
     * 可以选择不同的数据源,这⾥使⽤ HikariDataSource,创建数据源
     *
     * @param masterDataSourceProperties 数据源配置
     * @return master 数据源
     */
    @Primary
    @Bean(name = "masterDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.hikari.master") //配置 master 数据源所⽤的 hikari 配置 key 的前缀
    public DataSource dataSource(@Qualifier("masterDataSourceProperties")
                                 DataSourceProperties masterDataSourceProperties) {
        HikariDataSource dataSource =
                masterDataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
        if (StringUtils.hasText(masterDataSourceProperties.getName())) {
            dataSource.setPoolName(masterDataSourceProperties.getName());
        }
        return dataSource;
    }

    /**
     * 配置 master 数据源的 entityManagerFactory 命名为 masterEntityManagerFactory,⽤来对实体进⾏⼀些操作
     *
     * @param builder          构建器
     * @param masterDataSource master 数据源
     * @return master 实体管理工厂
     */
    @Primary
    @Bean(name = "masterEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder,
                                                                       @Qualifier("masterDataSource") DataSource masterDataSource) {
        return builder.dataSource(masterDataSource)
                // master 数据的实体所在的路径
                .packages("com.zzn.master")
                // persistenceUnit 的名字采⽤ master
                .persistenceUnit("master")
                .build();
    }

    /**
     * 配置 master 数据源的事务管理者,命名为 masterTransactionManager 依赖 masterEntityManagerFactory
     *
     * @param masterEntityManagerFactory master 实体管理工厂
     * @return master 事务管理者
     */
    @Primary
    @Bean(name = "masterTransactionManager")
    public PlatformTransactionManager transactionManager(@Qualifier("masterEntityManagerFactory") EntityManagerFactory masterEntityManagerFactory) {
        return new JpaTransactionManager(masterEntityManagerFactory);
    }
}

到这⾥,master 数据源 我们就配置完了,下⾯再配置 slave 数据源。

第三步:配置 SlaveDataSourceConfig 类,加载 slave 数据源

@Configuration
@EnableTransactionManagement // 开启事务
@EnableJpaRepositories(  // 利⽤ EnableJpaRepositories 配置哪些包下⾯的 Repositories,采⽤哪个 EntityManagerFactory 和哪个 TransactionManagement
        basePackages = {"com.zzn.slave"},// slave 数据源的 repository 的包路径
        entityManagerFactoryRef = "slaveEntityManagerFactory",// 改变 slave 数据源的 EntityManagerFactory 的默认值,改为 slaveEntityManagerFactory
        transactionManagerRef = "slaveTransactionManager" // 改变 slave 数据源的 TransactionManagement 的默认值,slaveTransactionManager
)
public class SlaveDataSourceConfig {


    /**
     * 指定 slave 数据源的 dataSource 配置
     *
     * @return slave 数据源配置
     */
    @Bean(name = "slaveDataSourceProperties")
    @ConfigurationProperties("spring.datasource.slave") // slave 数据源的配置前缀采⽤ spring.datasource.slave
    public DataSourceProperties dataSourceProperties() {
        return new DataSourceProperties();
    }

    /**
     * 可以选择不同的数据源,这⾥使⽤ HikariDataSource,创建数据源
     *
     * @param slaveDataSourceProperties 数据源配置
     * @return slave 数据源
     */
    @Bean(name = "slaveDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.hikari.slave") //配置 slave 数据源所⽤的 hikari 配置 key 的前缀
    public DataSource dataSource(@Qualifier("slaveDataSourceProperties")
                                 DataSourceProperties slaveDataSourceProperties) {
        HikariDataSource dataSource =
                slaveDataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
        if (StringUtils.hasText(slaveDataSourceProperties.getName())) {
            dataSource.setPoolName(slaveDataSourceProperties.getName());
        }
        return dataSource;
    }

    /**
     * 配置 slave 数据源的 entityManagerFactory 命名为 slaveEntityManagerFactory,⽤来对实体进⾏⼀些操作
     *
     * @param builder         构建器
     * @param slaveDataSource slave 数据源
     * @return slave 实体管理工厂
     */
    @Bean(name = "slaveEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder,
                                                                       @Qualifier("slaveDataSource") DataSource slaveDataSource) {
        return builder.dataSource(slaveDataSource)
                // slave 数据的实体所在的路径
                .packages("com.zzn.slave")
                // persistenceUnit 的名字采⽤ slave
                .persistenceUnit("slave")
                .build();
    }

    /**
     * 配置 slave 数据源的事务管理者,命名为 slaveTransactionManager 依赖 slaveEntityManagerFactory
     *
     * @param slaveEntityManagerFactory slave 实体管理工厂
     * @return slave 事务管理者
     */
    @Bean(name = "slaveTransactionManager")
    public PlatformTransactionManager transactionManager(@Qualifier("slaveEntityManagerFactory") EntityManagerFactory slaveEntityManagerFactory) {
        return new JpaTransactionManager(slaveEntityManagerFactory);
    }

}

这⼀步你需要注意,MasterDataSourceConfig 和 SlaveDataSourceConfig 不同的是,master ⾥⾯每个 @Bean 都是 @Primary,⽽ slave ⾥⾯不是的。

第四步:通过 application.yml 配置两个数据源的值,代码如下:

spring:
  datasource:
    # master 数据库采用 MySql 数据库
    master:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/test?serverTimezone=Asia/Shanghai&logger=Slf4JLogger&profileSQL=true
      username: root
      password: root
    # slave 数据库采用 h2
    slave:
      url: jdbc:h2:~/test
      username: sa
      password: sa
    hikari:
      master:
        pool-name: jpa-hikari-pool-master
        max-lifetime: 900000
        maximum-pool-size: 8
      slave:
        pool-name: jpa-hikari-pool-slave
        max-lifetime: 500000
        maximum-pool-size: 6

第五步:我们写个 Controller 测试⼀下。

@RestController
@RequestMapping("")
@RequiredArgsConstructor
public class UserController {

    private final MasterUserRepository masterUserRepository;
    private final SlaveUserRepository slaveUserRepository;


    /**
     * 操作 master
     */
    @PostMapping("/user/master")
    public MasterUser saveUser(@RequestBody MasterUser user) {
        return masterUserRepository.save(user);
    }

    /**
     * 操作 slave
     */
    @PostMapping("/user/slave")
    public SlaveUser saveUserInfo(@RequestBody SlaveUser user) {
        return slaveUserRepository.save(user);
    }


}

第六步:直接启动我们的项⽬,测试⼀下。

请看这⼀步的启动⽇志:

Spring Data JPA 之 多数据源配置_第1张图片

可以看到启动的是两个数据源,如果我们分别请求 Controller 写的两个⽅法的时候,也会分别插⼊到不同的数据源⾥⾯去。

通过上⾯的六个步骤你应该知道了如何配置多数据源,那么它的原理基础是什么呢?我们看⼀下 Datasource 与 TransactionManager、EntityManagerFactory 的关系和职责分别是怎么样的。

18.1.2 DataSource与Transaction-Manager、EntityManager-Factory的关系分析

Spring Data JPA 之 多数据源配置_第2张图片

  1. HikariDataSource 负责实现 DataSource,交给 EntityManager 和 TransactionManager 使⽤;
  2. EntityManager 是利⽤ Datasouce 来操作数据库,⽽其实现类是 SessionImpl;
  3. EntityManagerFactory 是⽤来管理和⽣成 EntityManager 的,⽽ EntityManagerFactory 的实现类是 LocalContainerEntityManagerFactoryBean,通过实现 FactoryBean 接⼝实现,利⽤了 FactoryBean 的 Spring 中的 bean 管理机制,所以需要我们在 MasterDatasourceConfig ⾥⾯配置 LocalContainerEntityManagerFactoryBean 的 bean 的注⼊⽅式;
  4. JpaTransactionManager 是⽤来管理事务的,实现了 TransactionManager 并且通过 EntityFactory 和 Datasource 进⾏ db 操作,所以我们要在 DataSourceConfig ⾥⾯告诉 JpaTransactionManager ⽤的 TransactionManager 是 masterEntityManagerFactory。
18.1.3 默认的 JpaBaseConfiguration 加载方式分析

上⼀讲只简单说明了 DataSource 的配置,其实还可以通过 HibernateJpaConfiguration,找到⽗类 JpaBaseConfiguration 类,就可以看到多数据源的参考原型,如下图所示:

Spring Data JPA 之 多数据源配置_第3张图片

通过上⾯的代码,可以看到在单个数据源情况下的 EntityManagerFactory 和 TransactionManager 的加载⽅法,并且我们在多数据源的配置⾥⾯还加载了⼀个类:EntityManagerFactoryBuilder entityManagerFactoryBuilder,也正是从上⾯的⽅法加载进去的。

18.2 第二种方式:利用 AbstractRoutingDataSource 配置

18.2.1 利用 AbstractRoutingDataSource 的配置方法

我们都知道 DataSource 的本质是获得数据库连接,⽽ AbstractRoutingDataSource 帮我们实现了动态获得数据源的可能性。下⾯还是通过⼀个例⼦看⼀下它是怎么使⽤的。

第⼀步:定⼀个数据源的枚举类,⽤来标示数据源有哪些。

public enum RoutingDataSourceEnum {

    MASTER,
    SLAVE;
    
    public static RoutingDataSourceEnum findByCode(String dbRouting) {
        return Arrays.stream(values())
                .filter(e -> e.name().equals(dbRouting))
                .findFirst()
                // 没找到的情况下,默认返回 Master
                .orElse(MASTER);
    }

}

第⼆步:新增 DataSourceRoutingHolder,⽤来存储当前线程需要采⽤的数据源。

/**
 * 利⽤ ThreadLocal 来存储,当前的线程使⽤的数据
 */
public class DataSourceRoutingHolder {

    private static final ThreadLocal<RoutingDataSourceEnum> THREAD_LOCAL = new ThreadLocal<>();

    public static void setDataSource(RoutingDataSourceEnum dataSourceEnum) {
        THREAD_LOCAL.set(dataSourceEnum);
    }

    public static RoutingDataSourceEnum getDataSource() {
        return THREAD_LOCAL.get();
    }

    public static void clearDataSource() {
        THREAD_LOCAL.remove();
    }

}

**第三步:自定义 RoutingDataSource,继承 AbstractRoutingDataSource **

public class RoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceRoutingHolder.getDataSource();
    }
}

第四步:配置 RoutingDataSourceConfig,⽤来指定哪些 Entity 和 Repository 采⽤动态数据源。

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
        // 数据源的 repository 的包路径,这⾥我们覆盖 master 和 slave 的包路径
        basePackages = {"com.zzn"},
        entityManagerFactoryRef = "routingEntityManagerFactory",
        transactionManagerRef = "routingTransactionManager"
)
public class RoutingDataSourceConfig {

    /**
     * 创建 RoutingDataSource,引⽤我们之前配置的 masterDataSource 和 slaveDataSource
     */
    @Bean(name = "routingDataSource")
    public DataSource dataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
                                 @Qualifier("slaveDataSource") DataSource slaveDataSource) {
        Map<Object, Object> dataSourceMap = Maps.newHashMap();
        dataSourceMap.put(RoutingDataSourceEnum.MASTER, masterDataSource);
        dataSourceMap.put(RoutingDataSourceEnum.SLAVE, slaveDataSource);
        RoutingDataSource routingDataSource = new RoutingDataSource();
        // 设置 RoutingDataSource 的默认数据源
        routingDataSource.setDefaultTargetDataSource(masterDataSource);
        // 设置 RoutingDataSource 的数据源列表
        routingDataSource.setTargetDataSources(dataSourceMap);
        return routingDataSource;
    }

    /**
     * 类似 master 和 slave 的配置,唯⼀不同的是,这⾥采⽤ routingDataSource
     *
     * @param builder
     * @param routingDataSource entityManager 依赖 routingDataSource
     * @return
     */
    @Bean(name = "routingEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean
    entityManagerFactory(EntityManagerFactoryBuilder builder,
                         @Qualifier("routingDataSource") DataSource routingDataSource) {
        // 数据 routing 的实体所在的路径,这⾥我们覆盖 master 和 slave 的路径
        return builder.dataSource(routingDataSource).packages("com.zzn")
                // persistenceUnit 的名字采⽤ db-routing
                .persistenceUnit("db-routing")
                .build();
    }

    /**
     * 配置数据的事务管理者,命名为routingTransactionManager依赖 routingEntityManagerFactory
     *
     * @param routingEntityManagerFactory
     * @return
     */
    @Bean(name = "routingTransactionManager")
    public PlatformTransactionManager
    transactionManager(@Qualifier("routingEntityManagerFactory")
                       EntityManagerFactory routingEntityManagerFactory) {
        return new JpaTransactionManager(routingEntityManagerFactory);
    }
}

第五步:自定义动态数据源的注解和拦截器

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface DS {

    RoutingDataSourceEnum value() default RoutingDataSourceEnum.MASTER;

}
@Aspect
// 保证该 AOP 在 @Transactional 之前执行
@Order(-10)
@Component
@Slf4j
public class RoutingDataSourceAspect {

    @Before(value = "@annotation(source)")
    public void changeDataSource(JoinPoint point, DS source) {
        RoutingDataSourceEnum currentSource = source.value();
        log.info("Change DataSource To:[" + currentSource + "]");
        DataSourceRoutingHolder.setDataSource(currentSource);
    }

    @After(value = "@annotation(source)")
    public void restoreDataSource(JoinPoint point, DS source) {
        // 方法执行完毕之后,销毁当前数据源信息,进行垃圾回收。
        DataSourceRoutingHolder.clearDataSource();
        log.info("Clear Change DataSource...");
    }

}

**第六步:使用注解切换数据源 **

@PostMapping("/user/slave")
@DS(RoutingDataSourceEnum.SLAVE)
public SlaveUser saveUserInfo(@RequestBody SlaveUser user) {
    return slaveUserRepository.save(user);
}

通过上⾯六个步骤,我们可以利⽤ AbstractRoutingDataSource 实现动态数据源,实际⼯作中可能要复杂,有的需要考虑多线程、线程安全等问题,你要多加注意。

18.2.2 微服务下多数据源的思考

通过上⾯的两种⽅式,我们分别可以实现同⼀个 application 应⽤的多数据源配置,那么有什么注意事项呢?我简单总结如下⼏点建议。

  1. 此种⽅式利⽤了当前线程事务不变的原理,所以要注意异步线程的处理⽅式;
  2. 此种⽅式利⽤了 DataSource 的原理,动态地返回不同的 db 连接,⼀般需要在开启事务之前使⽤,需要注意事务的⽣命周期;
  3. ⽐较适合读写操作分开的业务场景;
  4. 多数据的情况下,避免⼀个事务⾥⾯采⽤不同的数据源,这样会有意想不到的情况发⽣,⽐如死锁现象;
  5. 学会通过⽇志检查我们开启请求的⽅法和开启的数据源是否正确,可以通过 Debug 断点来观察数据源是否选择的正确。
18.2.3 微服务下的实战建议

在实际⼯作中,为了便捷省事,更多开发者喜欢配置多个数据源,但是我强烈建议不要在对⽤户直接提供的 API 服务上⾯配置多数据源,否则将出现令⼈措⼿不及的 Bug。

如果你是做后台管理界⾯,供公司内部员⼯使⽤的,那么这种 API 可以为了⽅便⽽使⽤多数据源。

微服务的⼤环境下,服务越⼩,内聚越⾼,低耦合服务越健壮,所以⼀般跨库之间⼀定是是通过 REST 的 API 协议,进⾏内部服务之间的调⽤,这是最稳妥的⽅式,原因有如下⼏点:

  1. REST 的 API 协议更容易监控,更容易实现事务的原⼦性;
  2. db 之间解耦,使业务领域代码职责更清晰,更容易各⾃处理各种问题;
  3. 只读和读写的 API 更容易分离和管理。

18.3 本章小结

到这⾥,这⼀讲的内容就结束了。多数据的配置是⼀个⽐较复杂的事情,在本讲中通过两种⽅式,⾃定义 entityManager 和 transactionManager,实现了多数据源的配置。

你可能感兴趣的:(Spring,Data,JPA,java,spring,mybatis)