⼯作中我们时常会遇到跨数据库操作的情况,这时候就需要配置多数据源,那么如何配置呢?常⽤的⽅式及其背后的原理⽀撑是什么呢?我们下⾯来了解⼀下
这种⽅式的主要思路是,不同 Package 下⾯的实体和 Repository 采⽤不同的 Datasource。所以我们改造⼀下我们的 example ⽬录结构,来看看不同 Repositories 的数据源是怎么处理的。
第⼀步:规划 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);
}
}
第六步:直接启动我们的项⽬,测试⼀下。
请看这⼀步的启动⽇志:
可以看到启动的是两个数据源,如果我们分别请求 Controller 写的两个⽅法的时候,也会分别插⼊到不同的数据源⾥⾯去。
通过上⾯的六个步骤你应该知道了如何配置多数据源,那么它的原理基础是什么呢?我们看⼀下 Datasource 与 TransactionManager、EntityManagerFactory 的关系和职责分别是怎么样的。
上⼀讲只简单说明了 DataSource 的配置,其实还可以通过 HibernateJpaConfiguration,找到⽗类 JpaBaseConfiguration 类,就可以看到多数据源的参考原型,如下图所示:
通过上⾯的代码,可以看到在单个数据源情况下的 EntityManagerFactory 和 TransactionManager 的加载⽅法,并且我们在多数据源的配置⾥⾯还加载了⼀个类:EntityManagerFactoryBuilder entityManagerFactoryBuilder,也正是从上⾯的⽅法加载进去的。
我们都知道 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 实现动态数据源,实际⼯作中可能要复杂,有的需要考虑多线程、线程安全等问题,你要多加注意。
通过上⾯的两种⽅式,我们分别可以实现同⼀个 application 应⽤的多数据源配置,那么有什么注意事项呢?我简单总结如下⼏点建议。
在实际⼯作中,为了便捷省事,更多开发者喜欢配置多个数据源,但是我强烈建议不要在对⽤户直接提供的 API 服务上⾯配置多数据源,否则将出现令⼈措⼿不及的 Bug。
如果你是做后台管理界⾯,供公司内部员⼯使⽤的,那么这种 API 可以为了⽅便⽽使⽤多数据源。
微服务的⼤环境下,服务越⼩,内聚越⾼,低耦合服务越健壮,所以⼀般跨库之间⼀定是是通过 REST 的 API 协议,进⾏内部服务之间的调⽤,这是最稳妥的⽅式,原因有如下⼏点:
到这⾥,这⼀讲的内容就结束了。多数据的配置是⼀个⽐较复杂的事情,在本讲中通过两种⽅式,⾃定义 entityManager 和 transactionManager,实现了多数据源的配置。