Skip to content

18生产环境多数据源的处理方法有哪些?

上一讲我们介绍了 DataSource 的相关内容,今天我们来介绍一下多数据源的处理方法有哪些。

工作中我们时常会遇到跨数据库操作的情况,这时候就需要配置多数据源,那么如何配置呢?常用的方式及其背后的原理支撑是什么呢?我们下面来了解一下。

首先看看两种常见的配置方式,分别为通过多个 @Configuration 文件、利用 AbstractRoutingDataSource 配置多数据源。

第一种方式:多个数据源的 @Configuration 的配置方法

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

第一步:规划 Entity 和 Repository 的目录结构,为了方便配置多数据源。

将 User 和 UserAddress、UserRepository 和 UserAddressRepository 移动到 db1 里面;将 UserInfo 和 UserInfoRepository 移动到 db2 里面。如下图所示:

我们把实体和 Repository 分别放到了 db1 和 db2 两个目录里面,这时我们假设数据源 1 是 MySQL,User 表和 UserAddress 在数据源 1 里面,那么我们需要配置一个 DataSource1 的 Configuration 类,并且在里面配置 DataSource、TransactionManager 和 EntityManager。

第二步:配置 DataSource1Config 类。

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

java
@Configuration
@EnableTransactionManagement//开启事务
//利用EnableJpaRepositories配置哪些包下面的Repositories,采用哪个EntityManagerFactory和哪个trannsactionManager
@EnableJpaRepositories(
      basePackages = {"com.example.jpa.example1.db1"},//数据源1的repository的包路径
      entityManagerFactoryRef = "db1EntityManagerFactory",//改变数据源1的EntityManagerFactory的默认值,改为db1EntityManagerFactory
      transactionManagerRef = "db1TransactionManager"//改变数据源1的transactionManager的默认值,改为db1TransactionManager
      )
public class DataSource1Config {
   /**
    * 指定数据源1的dataSource配置
    * @return
    */
   @Primary
   @Bean(name = "db1DataSourceProperties")
   @ConfigurationProperties("spring.datasource1") //数据源1的db配置前缀采用spring.datasource1
   public DataSourceProperties dataSourceProperties() {
      return new DataSourceProperties();
   }
   /**
    * 可以选择不同的数据源,这里我用HikariDataSource举例,创建数据源1
    * @param db1DataSourceProperties
    * @return
    */
   @Primary
   @Bean(name = "db1DataSource")
   @ConfigurationProperties(prefix = "spring.datasource.hikari.db1") //配置数据源1所用的hikari配置key的前缀
   public HikariDataSource dataSource(@Qualifier("db1DataSourceProperties") DataSourceProperties db1DataSourceProperties) {
      HikariDataSource dataSource = db1DataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
      if (StringUtils.hasText(db1DataSourceProperties.getName())) {
         dataSource.setPoolName(db1DataSourceProperties.getName());
      }
      return dataSource;
   }
   /**
    * 配置数据源1的entityManagerFactory命名为db1EntityManagerFactory,用来对实体进行一些操作
    * @param builder
    * @param db1DataSource entityManager依赖db1DataSource
    * @return
    */
   @Primary
   @Bean(name = "db1EntityManagerFactory")
   public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder, @Qualifier("db1DataSource") DataSource db1DataSource) {
      return builder.dataSource(db2DataSource)
.packages("com.example.jpa.example1.db1") //数据1的实体所在的路径
.persistenceUnit("db1")// persistenceUnit的名字采用db1
.build();
   }
   /**
    * 配置数据源1的事务管理者,命名为db1TransactionManager依赖db1EntityManagerFactory
    * @param db1EntityManagerFactory 
    * @return
    */
   @Primary
   @Bean(name = "db1TransactionManager")
   public PlatformTransactionManager transactionManager(@Qualifier("db1EntityManagerFactory") EntityManagerFactory db1EntityManagerFactory) {
      return new JpaTransactionManager(db1EntityManagerFactory);
   }
}

到这里,数据源 1 我们就配置完了,下面再配置数据源 2。

第三步:配置 DataSource2Config类,加载数据源 2。

java
@Configuration
@EnableTransactionManagement//开启事务
//利用EnableJpaRepositories,配置哪些包下面的Repositories,采用哪个EntityManagerFactory和哪个trannsactionManager
@EnableJpaRepositories(
        basePackages = {"com.example.jpa.example1.db2"},//数据源2的repository的包路径
        entityManagerFactoryRef = "db2EntityManagerFactory",//改变数据源2的EntityManagerFactory的默认值,改为db2EntityManagerFactory
        transactionManagerRef = "db2TransactionManager"//改变数据源2的transactionManager的默认值,改为db2TransactionManager
)
public class DataSource2Config {
    /**
     * 指定数据源2的dataSource配置
     *
     * @return
     */
    @Bean(name = "db2DataSourceProperties")
    @ConfigurationProperties("spring.datasource2") //数据源2的db配置前缀采用spring.datasource2
    public DataSourceProperties dataSourceProperties() {
        return new DataSourceProperties();
    }
    /**
     * 可以选择不同的数据源,这里我用HikariDataSource举例,创建数据源2
     *
     * @param db2DataSourceProperties
     * @return
     */
    @Bean(name = "db2DataSource")
    @ConfigurationProperties(prefix = "spring.datasource.hikari.db2") //配置数据源2的hikari配置key的前缀
    public HikariDataSource dataSource(@Qualifier("db2DataSourceProperties") DataSourceProperties db2DataSourceProperties) {
        HikariDataSource dataSource = db2DataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
        if (StringUtils.hasText(db2DataSourceProperties.getName())) {
            dataSource.setPoolName(db2DataSourceProperties.getName());
        }
        return dataSource;
    }
    /**
     * 配置数据源2的entityManagerFactory命名为db2EntityManagerFactory,用来对实体进行一些操作
     *
     * @param builder
     * @param db2DataSource entityManager依赖db2DataSource
     * @return
     */
    @Bean(name = "db2EntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder, @Qualifier("db2DataSource") DataSource db2DataSource) {
        return builder.dataSource(db2DataSource)
            .packages("com.example.jpa.example1.db2") //数据2的实体所在的路径
            .persistenceUnit("db2")// persistenceUnit的名字采用db2
            .build();
    }
    /**
     * 配置数据源2的事务管理者,命名为db2TransactionManager依赖db2EntityManagerFactory
     *
     * @param db2EntityManagerFactory
     * @return
     */
    @Bean(name = "db2TransactionManager")
    public PlatformTransactionManager transactionManager(@Qualifier("db2EntityManagerFactory") EntityManagerFactory db2EntityManagerFactory) {
        return new JpaTransactionManager(db2EntityManagerFactory);
    }
}

这一步你需要注意,DataSource1Config 和 DataSource2Config 不同的是,1 里面每个 @Bean 都 @Primary,而 2 里面不是的。

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

java
###########datasource1 采用Mysql数据库
spring.datasource1.url=jdbc:mysql://localhost:3306/test2?logger=Slf4JLogger&profileSQL=true
spring.datasource1.username=root
spring.datasource1.password=root
##数据源1的连接池的名字
spring.datasource.hikari.db1.pool-name=jpa-hikari-pool-db1
##最长生命周期15分钟够了
spring.datasource.hikari.db1.maxLifetime=900000
spring.datasource.hikari.db1.maximumPoolSize=8
###########datasource2 采用h2内存数据库
spring.datasource2.url=jdbc:h2:~/test
spring.datasource2.username=sa
spring.datasource2.password=sa
##数据源2的连接池的名字
spring.datasource.hikari.db2.pool-name=jpa-hikari-pool-db2
##最长生命周期15分钟够了
spring.datasource.hikari.db2.maxLifetime=500000
##最大连接池大小和数据源1区分开,我们配置成6个
spring.datasource.hikari.db2.maximumPoolSize=6

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

java
@RestController
public class UserController {
   @Autowired
   private UserRepository userRepository;
   @Autowired
   private UserInfoRepository userInfoRepository;
   //操作user的Repository
   @PostMapping("/user")
   public User saveUser(@RequestBody User user) {
      return userRepository.save(user);
   }
   //操作userInfo的Repository
  @PostMapping("/user/info")
  public UserInfo saveUserInfo(@RequestBody UserInfo userInfo) {
     return userInfoRepository.save(userInfo);
  }
}

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

请看这一步的启动日志:

可以看到启动的是两个数据源,其对应的连接池的监控也是不一样的:数据源 1 有 8 个,数据源 2 有 6 个。

如果我们分别请求 Controller 写的两个方法的时候,也会分别插入到不同的数据源里面去。

通过上面的六个步骤你应该知道了如何配置多数据源,那么它的原理基础是什么呢?我们看一下

Datasource 与 TransactionManager、EntityManagerFactory 的关系和职责分别是怎么样的。

Datasource 与 TransactionManager、EntityManagerFactory 的关系分析

我们通过一个类的关系图来分析一下:

其中,

  1. HikariDataSource 负责实现 DataSource,交给 EntityManager 和 TransactionManager 使用;

  2. EntityManager 是利用 Datasouce 来操作数据库,而其实现类是 SessionImpl;

  3. EntityManagerFactory 是用来管理和生成 EntityManager 的,而 EntityManagerFactory 的实现类是 LocalContainerEntityManagerFactoryBean,通过实现 FactoryBean 接口实现,利用了 FactoryBean 的 Spring 中的 bean 管理机制,所以需要我们在 Datasource1Config 里面配置 LocalContainerEntityManagerFactoryBean 的 bean 的注入方式;

  4. JpaTransactionManager 是用来管理事务的,实现了 TransactionManager 并且通过 EntityFactory 和 Datasource 进行 db 操作,所以我们要在 DataSourceConfig 里面告诉 JpaTransactionManager 用的 TransactionManager 是 db1EntityManagerFactory。

上一讲我们介绍了 Datasource 的默认加载和配置方式,那么默认情况下 Datasource 的 EntityManagerFactory 和 TransactionManager 是怎么加载和配置的呢?

默认的 JpaBaseConfiguration 的加载方式分析

上一讲我只简单说明了 DataSource 的配置,其实我们还可以通过 HibernateJpaConfiguration,找到父类 JpaBaseConfiguration 类,如图所示:

接着打开 JpaBaseConfiguration 就可以看到多数据源的参考原型,如下图所示:

通过上面的代码,可以看到在单个数据源情况下的 EntityManagerFactory 和 TransactionManager 的加载方法,并且我们在多数据源的配置里面还加载了一个类:EntityManagerFactoryBuilder entityManagerFactoryBuilder,也正是从上面的方法加载进去的,看第 120 行代码就知道了。

那么除了上述的配置多数据源的方式,还有没有其他方法了呢?我们接着看一下。

第二种方式:利用 AbstractRoutingDataSource 配置多数据源

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

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

java
/**
 * 定义一个数据源的枚举类
 */
public enum RoutingDataSourceEnum {
   DB1, //实际工作中枚举的语义可以更加明确一点;
   DB2;
   public static RoutingDataSourceEnum findbyCode(String dbRouting) {
      for (RoutingDataSourceEnum e : values()) {
         if (e.name().equals(dbRouting)) {
            return e;
         }
      }
      return db1;//没找到的情况下,默认返回数据源1
   }
}

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

java
/**
 * 利用ThreadLocal来存储,当前的线程使用的数据
 */
public class DataSourceRoutingHolder {
   private static ThreadLocal<RoutingDataSourceEnum> threadLocal = new ThreadLocal<>();
   public static void setBranchContext(RoutingDataSourceEnum dataSourceEnum) {
      threadLocal.set(dataSourceEnum);
   }
   public static RoutingDataSourceEnum getBranchContext() {
      return threadLocal.get();
   }
   public static void clearBranchContext() {
      threadLocal.remove();
   }
}

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

java
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
      //数据源的repository的包路径,这里我们覆盖db1和db2的包路径
      basePackages = {"com.example.jpa.example1"},
      entityManagerFactoryRef = "routingEntityManagerFactory",
      transactionManagerRef = "routingTransactionManager"
)
public class RoutingDataSourceConfig {
   @Autowired
   @Qualifier("db1DataSource")
   private DataSource db1DataSource;
   @Autowired
   @Qualifier("db2DataSource")
   private DataSource db2DataSource;
   /**
    * 创建RoutingDataSource,引用我们之前配置的db1DataSource和db2DataSource
    *
    * @return
    */
   @Bean(name = "routingDataSource")
   public DataSource dataSource() {
      Map<Object, Object> dataSourceMap = Maps.newHashMap();
      dataSourceMap.put(RoutingDataSourceEnum.DB1, db1DataSource);
      dataSourceMap.put(RoutingDataSourceEnum.DB2, db2DataSource);
      RoutingDataSource routingDataSource = new RoutingDataSource();
      //设置RoutingDataSource的默认数据源
      routingDataSource.setDefaultTargetDataSource(db1DataSource);
      //设置RoutingDataSource的数据源列表
      routingDataSource.setTargetDataSources(dataSourceMap);
      return routingDataSource;
   }
   /**
    * 类似db1和db2的配置,唯一不同的是,这里采用routingDataSource
    * @param builder
    * @param routingDataSource entityManager依赖routingDataSource
    * @return
    */
   @Bean(name = "routingEntityManagerFactory")
   public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder, @Qualifier("routingDataSource") DataSource routingDataSource) {
      return builder.dataSource(routingDataSource).packages("com.example.jpa.example1") //数据routing的实体所在的路径,这里我们覆盖db1和db2的路径
            .persistenceUnit("db-routing")// persistenceUnit的名字采用db-routing
            .build();
   }
   /**
    * 配置数据的事务管理者,命名为routingTransactionManager依赖routtingEntityManagerFactory
    *
    * @param routingEntityManagerFactory
    * @return
    */
   @Bean(name = "routingTransactionManager")
   public PlatformTransactionManager transactionManager(@Qualifier("routingEntityManagerFactory") EntityManagerFactory routingEntityManagerFactory) {
      return new JpaTransactionManager(routingEntityManagerFactory);
   }
}

路由数据源配置与 DataSource1Config 和 DataSource2Config 有相互覆盖关系,这里我们直接覆盖 db1 和 db2 的包路径,以便于我们的动态数据源生效。

第四步:写一个 MVC 拦截器,用来指定请求分别采用什么数据源。

新建一个类 DataSourceInterceptor,用来在请求前后指定数据源,请看代码:

java
/**
 * 动态路由的实现逻辑,我们通过请求里面的db-routing,来指定此请求采用什么数据源
 */
@Component
public class DataSourceInterceptor extends HandlerInterceptorAdapter {
   /**
    * 请求处理之前更改线程里面的数据源
    */
   @Override
   public boolean preHandle(HttpServletRequest request,
                      HttpServletResponse response, Object handler) throws Exception {
      String dbRouting = request.getHeader("db-routing");
      DataSourceRoutingHolder.setBranchContext(RoutingDataSourceEnum.findByCode(dbRouting));
      return super.preHandle(request, response, handler);
   }
   /**
    * 请求结束之后清理线程里面的数据源
    */
   @Override
   public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
      super.afterCompletion(request, response, handler, ex);
      DataSourceRoutingHolder.clearBranchContext();
   }
}

同时我们需要在实现 WebMvcConfigurer 的配置里面,把我们自定义拦截器 dataSourceInterceptor 加载进去,代码如下:

java
/**
 * 实现WebMvcConfigurer
 */
@Configuration
public class MyWebMvcConfigurer implements WebMvcConfigurer {
   @Autowired
   private DataSourceInterceptor dataSourceInterceptor;
   //添加自定义拦截器
   @Override
   public void addInterceptors(InterceptorRegistry registry) {
      registry.addInterceptor(dataSourceInterceptor).addPathPatterns("/**");
      WebMvcConfigurer.super.addInterceptors(registry);
   }
...//其他不变的代码省略}

此处我们采用的是 MVC 的拦截器机制动态改变的数据配置,你也可以使用自己的 AOP 任意的拦截器,如事务拦截器、Service 的拦截器等,都可以实现。需要注意的是,要在开启事务之前配置完毕。

第五步:启动测试。

我们在 Http 请求头里面加上 db-routing:DB2,那么本次请求就会采用数据源 2 进行处理,请求代码如下:

java
POST /user/info HTTP/1.1
Host: 127.0.0.1:8089
Content-Type: application/json
db-routing: DB2
Cache-Control: no-cache
Postman-Token: 56d8dc02-7f3e-7b95-7ff1-572a4bb7d102
{"ages":10}

通过上面五个步骤,我们可以利用 AbstractRoutingDataSource 实现动态数据源,实际工作中可能会比我讲述的要复杂,有的需要考虑多线程、线程安全等问题,你要多加注意。

在实际应用场景中,对于多数据源的问题,我还有一些思考,下面分享给你。

微服务下多数据源的思考:还需要这样用吗?

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

多数据源实战注意事项

  1. 此种方式利用了当前线程事务不变的原理,所以要注意异步线程的处理方式;

  2. 此种方式利用了 DataSource 的原理,动态地返回不同的 db 连接,一般需要在开启事务之前使用,需要注意事务的生命周期;

  3. 比较适合读写操作分开的业务场景;

  4. 多数据的情况下,避免一个事务里面采用不同的数据源,这样会有意想不到的情况发生,比如死锁现象;

  5. 学会通过日志检查我们开启请求的方法和开启的数据源是否正确,可以通过 Debug 断点来观察数据源是否选择的正确,如下图所示:

微服务下的实战建议

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

如果你是做后台管理界面,供公司内部员工使用的,那么这种 API 可以为了方便而使用多数据源。

微服务的大环境下,服务越小,内聚越高,低耦合服务越健壮,所以一般跨库之间一定是是通过 REST 的 API 协议,进行内部服务之间的调用,这是最稳妥的方式,原因有如下几点:

  1. REST 的 API 协议更容易监控,更容易实现事务的原子性;

  2. db 之间解耦,使业务领域代码职责更清晰,更容易各自处理各种问题;

  3. 只读和读写的 API 更容易分离和管理。

总结

到这里,这一讲的内容就结束了。多数据的配置是一个比较复杂的事情,在本讲中我通过两种方式,带领你自定义 entityManager 和 transactionManager,实现了多数据源的配置。如果对此你有不懂的地方,欢迎你在下方留言,我会尽快给你回复。

此外,你需要掌握的一个简单的基础知识,就是线程、事务和数据源之间的关系。下一讲我们再详细分析一下事务中需要我们关心的内容有哪些。

点击下方链接查看源码(不定时更新)
https://github.com/zhangzhenhuajack/spring-boot-guide/tree/master/spring-data/spring-data-jpa