springboot动态选择多数据源

springboot动态选择数据源

前情回顾:

前面介绍了springboot如何配置多数据源,最近又捣鼓了一下怎么去动态选择数据源,这里先提醒一下,动态选择数据源不等于主从复制,也不等于读写分离,这三的概念必须先理清楚,动态选择数据源是读写分离的基础,而主从复制是读写分离的依据。读写分离可以减少数据访问时的压力,并提高性能,同时也是作为负载均衡的一个重要基础

基础原理

既然要动态选择数据源,本质上还是进行动态选择数据库连接。查看一下java.sql.DataSource的接口,会发现有其有返回对应的Connection
DataSource

然后在IDEA中按Ctrl + Alt 同时点击getConnection()方法查看其实现类。在实现类列表中找到AbstractRoutingDataSource这个类,这个类由Spring框架提供实现,点进去

getConnectionRouting

在上图,由determineTargetDataSource实现了getConnection方法,点进去
determineTargetDataSource

上图中,determineCurrentLookupKey是一个抽象方法,其返回值可以通过传给resolvedDataSource从而决定一个连接的DataSource。而resolvedDataSource是一个Map<Object, Object>类型,其中约摸估计应该包含有用户所定义的所有的数据库DataSource。然后点击resolvedDataSource可以看到其是一个Map<Object, Object>
resolvedDataSource

继续往下看,看到一个方法afterPropertiesSet,里面会对resolvedDataSource进行初始化操作

afterPropertiesSet

其中的resolvedDataSource的值是由targetDataSource设置二来,而targetDataSource也是一个Map<Object, Object>类型。

看到这里,相信大家都应该知道要做什么了,先把流程理一下

  1. 继承AbstractRoutingDataSource,并将所有的数据库(包括主从数据库)的DataSource设置进targetDataSource,设置格式为Map<Obejct, Object>,第一个Object表示一个Key,将通过determinLookupKey进行筛选,第二个Object就是数据源
  2. 然后targetDataSources将自身设置给resolvedDataSources
  3. 通过重写determineLookupKey()方法,选择相应的数据源的key值。
  4. 用户通过某种自定义的方法动态选择数据源(这里使用AOP)

项目配置

  • springboot框架
  • SpringDataJPA
  • Lombok
  • Aspect

基本操作

在前面我们知道如何配置多数据源,而配置多数据源又是动态选择数据源的基础,但是此处配置又与前面有所不同(进度条充足,请继续食用)

在开始介绍之前,首先来看一下项目结构:

TestDataSource2结构

配置数据库连接(application.properties)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring.datasource.primary.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.primary.password=root
spring.datasource.primary.username=root
spring.datasource.primary.jdbc-url=jdbc:mysql://localhost:3306/shanpu

spring.datasource.secondary.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.secondary.jdbc-url=jdbc:mysql://localhost:3306/time_designer
spring.datasource.secondary.password=root
spring.datasource.secondary.username=root

spring.jpa.database=mysql
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect

数据库连接这块与前面没有什么不同之处,为了方便,我们使用同一服务下的不同库进行模拟演示

建立实体类(entity)

在前面配置多数据源的时候,我是将entity下再进行分层primary和slave,这样是方便演示,实际上这在多数据源中是不需要进行再分包的,可以直接在entity建立实体,如下图

Entity结构

  • User类;

User类

  • Admin类

Admin类

建立数据操作类

数据操作类也不必像配置多数据源一样进行分层,可以直接在repository下建立

  • repository分类

repository分类

  • UserRepository
1
2
public interface UserRepository extends JpaRepository<User, Integer>{
}
  • AdminRepository
1
2
public interface AdminRepository extends JpaRepository<Admin, Integer> {
}

配置动态数据源

动态数据源配置与配置多数据源的不太一样,前者是以后者为基础的,话不多说,先贴图

动态数据源配置结构

说明:

  • Datasource:自定义一个注解
  • DatasourceAspect:自定义的一个切面
  • DatasourceConfig:用来扫描配置文件中所配置的多个数据源,并生成Bean
  • DatasourceType:定义一个枚举类,里面有所要表示的各种配置类的名称
  • DynamicDatasource:继承了AbstractRoutingDatasource,并且实现了其抽象类determineCurrentLookupKey方法
  • DynamicDatasourceConfig:动态选择数据源的配置类
  • DynamicDatasourceHolder:动态数据源决策类

DataSourceConfig

有多少个数据源,就配置多少个Bean
DataSourceConfig

DynamicDataSouceConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
entityManagerFactoryRef = "dynamicEntityManagerFactory",
basePackages = {"com.shanyu.datasource.repository"}
)
public class DynamicDatasourceConfig {
@Autowired
private JpaProperties jpaProperties;

@Bean
@Primary
public AbstractRoutingDataSource routingDataSource(
@Qualifier("masterDatasource")DataSource masterDatasource,
@Qualifier("slaveDatasource")DataSource slaveDatasource
) {
System.out.println("---------begin-------------");
DynamicDatasource dynamicDatasource = new DynamicDatasource();
Map<Object, Object> targetDatasource = new HashMap<Object, Object>();
targetDatasource.put(DatasourceType.MASTER.type(), masterDatasource);
targetDatasource.put(DatasourceType.SLAVE.type(), slaveDatasource);
dynamicDatasource.setTargetDataSources(targetDatasource);//设置targetDataSources
dynamicDatasource.setDefaultTargetDataSource(masterDatasource);//设置默认的targetDataSources
System.out.println("------------end-------------");
return dynamicDatasource;

}

@Bean(name = "dynamicEntityManagerFactoryBean")
@Primary
public LocalContainerEntityManagerFactoryBean dynamicEntityFactoryBean(EntityManagerFactoryBuilder entityManagerFactoryBuilder,
DataSource dataSource) {
return entityManagerFactoryBuilder.dataSource(dataSource)
.packages("com.shanyu.datasource.entity")
.properties(jpaProperties.getProperties())
.persistenceUnit("dynamicDataSource")
.build();
}

@Bean(name = "dynamicEntityManagerFactory")
public EntityManagerFactory dynamicEntityFactory(EntityManagerFactoryBuilder entityManagerFactoryBuilder, DataSource dataSource) {
return this.dynamicEntityFactoryBean(entityManagerFactoryBuilder, dataSource).getObject();
}

@Bean(name = "transactionManager")
public PlatformTransactionManager transactionManager(@Qualifier("dynamicEntityManagerFactory") EntityManagerFactory entityManagerFactory) {

return new JpaTransactionManager(entityManagerFactory);
}
}

DynamicDataSourceConfig里面也是主要配置三个东西

  • 配置动态的DataSource(将key值与DataSource设置成targetDataSource)
  • 配置EntityManagerFactory(实体管理工厂)
  • 配置TransactionManager(事务管理器)

配置动态的DataSource需要依赖DynamicDataSource去提供一个key值,在里面同时将key值与相应的数据源匹配到一起,比如key值为master,value就是masterDataSource;key值是slave,value就是slaveDataSource。

其中在配置EntityManagerFactory中遇到一个坑,就是现在如代码所示在dynamicEntityFactoryBean这个方法里,通过builder去构建一个LocalContainerEntityManagerFactoryBean,但是如果直接在dynamicEntityFactoryBean里直接构建一个EntityMangerFactory时,是得通过builer.build().getObject()的方法进行构建的,**但是,getObject()在此方法中会产生NullPointerException的空指针异常。具体原因不是特别清楚,如果将其分开来的话,就如上面代码所示,通过一个dynamicEntityFactory方法可以构建一个EntityManagerFactory,这样就可以运行,至于为什么前者不行,还有待深入研究。

之后配置事务管理器,就直接将EntityManagerFactory这个Bean注入进去就可以了。

以上关于动态数据源在哪里的配置就已经结束了,但是怎么动态选择数据源的问题将在下面解决

DataSourceType

在DataSourceType里定义了Map<Object, Opject>结构里的key值,随后可以动态选择key值,然后通过DynamicDataSourceHolder决策类进行设置。

DataSourceType

其是一个Enum类,MASTER表示masterDataSource,SLAVE表示slaveDataSource

DynamicDataSource

继承自AbstractRoutingDataSource,并且重写了determineCurrentLookupKey方法

DynamicDataSource

就简单地选择了一下,其依赖DynamicDataSourceHolder决策类,通过决策类进行选择,如果决策类里有,就返回,否则默认返回masterDataSource。

DynamicDataSourceHolder

动态数据源的决策,如果用户选择了某个数据库连接,那么DataSourceType将会通过设值到DynamicDataSourceHolder,然后determineCurrentLookupKey将会取出Holder里面的值,然后再源码层面上将这个key值取出targetDataSource里面对应的DataSource,并getConnection。

DynamicDataSourceHolder

注解DataSource和切面DataSourceAspect

在本次测试项目中,将通过AOP进行动态的数据源选择

  • 注解DataSource

DataSourceAnnotation

  • 切面DataSourceAspect

DataSourceAspect

用完清除是个好习惯~

编写测试

由于自定义了一个@DataSource,此注解应用在service层,因此,将其注解在service层,就能实现动态选择数据源

testService

由于尽量模拟读写分离,因此读就从SLAVE读,而写就写进MASTER。

至此,关于sprinboot动态选择数据源就已经介绍到这里,下次继续向读写分离进发,最近考试月2333333333333333333

-------------本文结束感谢您的阅读-------------