什么是多数据源?
最常见的单一应用中最多涉及到一个数据库,即是一个数据源( Datasource )。那么顾名思义,多数据源就是在一个单一应用中涉及到了两个及以上的数据库了。
其实在配置数据源的时候就已经很明确这个定义了,如以下代码:
1 2 3 4 5 6 7 8 9
| @Bean(name = "dataSource") public DataSource dataSource() { DruidDataSource druidDataSource = new DruidDataSource(); druidDataSource.setUrl(url); druidDataSource.setUsername(username); druidDataSource.setDriverClassName(driverClassName); druidDataSource.setPassword(password); return druidDataSource; }
|
url 、 username 、 password 这三个属性已经唯一确定了一个数据库了, DataSource 则是依赖这三个创建出来的。则多数据源即是配置多个 DataSource
(暂且这么理解)。
整合单一的数据源
本文使用阿里的数据库连接池 druid ,添加依赖如下:
1 2 3 4 5 6
| <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.9</version> </dependency>
|
阿里的数据库连接池非常强大,比如 数据监控 、 数据库加密 等等内容,本文仅仅演示与Spring Boot整 合的过程,一些其他的功能后续可以自己研究添加。
Druid连接池的 starter 的自动配置类是 DruidDataSourceAutoConfigure
,类上标注如下一行注解:
1
| @EnableConfigurationProperties({DruidStatProperties.class, DataSourceProperties.class})
|
@EnableConfigurationProperties
这个注解使得配置文件中的配置生效并且映射到指定类的属性。
DruidStatProperties
中指定的前缀是 spring.datasource.druid
,这个配置主要是用来设置连接池的一些参数。
DatasourcoProperties
中指定的前级是 spring.datasource
,这个主要是用来设置数据库的 url
、username
、password
等信息。
因此我们只需要在全局配置文件中指定数据库的一些配置以及连接池的一些配置信息即可,前缀分别是spring.datasource.druid
、spring.datasource
,以下是个人随便配置的(application. Yml):
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
| spring: datasource: url: jdbc\:mysql\://localhost\:3306/xxx?useUnicode\=true&characterEncoding\=UTF-8&zeroDateTimeBehavior\=convertToNull&useSSL\=false&allowMultiQueries\=true&serverTimezone=Asia/Shanghai username: root password: xxxx type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver druid: initial-size: 0 max-active: 20 min-idle: 0 max-wait: 6000 validation-query: SELECT 1 test-on-borrow: false test-on-return: false test-while-idle: true time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 25200000 removeAbandoned: true remove-abandoned-timeout: 1800 log-abandoned: true filters: mergeStat
|
整合Mybatis
Spring Boot 整合Mybatis其实很简单,简单的几步就搞定,首先添加依赖:
1 2 3 4 5
| <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.1</version> </dependency>
|
第二步找到自动配置类 MybatisAutoConfiguration
,有如下一行代码:
1
| @EnableConfigurationProperties(MybatisProperties.class)
|
可配置的东西很多,比如 XML文件的位置 、 类型处理器 等等,如下简单的配置:
1 2 3 4
| mybatis: type-handlers-package: com.monochrome.dynamicdatasource.typehandler configuration: map-underscore-to-camel-case: true
|
如果需要通过包扫描的方式注入Mapper,则需要在配置类上加入一个注解: @MapperScan ,其中的 value属性指定需要扫描的包。
直接在全局配置文件配置各种属性是一种比较简单的方式,其实的任何组件的整合都有不少于两种的配置方式,下面来介绍下配置类如何配置。
MybatisAutoConfiguration
自动配置类有如下一段代码:
1 2 3 4
| @Bean @ConditionalOnMissingBean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { }
|
@ConditionalOnMissingBean
和 @Bean
真是老搭档了,意味着我们又可以覆盖,只需要在IOC容器中 注入 。
在自定义配置类中注入即可,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
@Bean("mabatisSqlSessionFactory") public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources("classpath*:/mapper/**/*.xml")); org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration(); configuration.setMapUnderscoreToCamelCase(true); configuration.setDefaultFetchSize(100); configuration.setDefaultStatementTimeout(30); sqlSessionFactoryBean.setConfiguration(configuration); return sqlSessionFactoryBean.getObject(); }
|
以上介绍了配置Mybatis的两种方式,其实在大多数场景中使用第一种已经够用了,至于为什么介绍第二种呢?当然是为了多数据源的整合而做准备了。
在 MybatisAutoConfiguration
中有一行很重要的代码,如下:
1
| @ConditionalOnSingleCandidate(DataSource.class)
|
@ConditionalOnSingleCandidate
这个注解的意思是当IOC容器中只有一个候选Bean的实例才会生效。
整合多数据源
上文留下的问题:为什么的Mybatis自动配置上标注如下一行代码:
1
| @ConditionalOnSingleCandidate(DataSource.class)
|
以上这行代码的言外之意:当IOC容器中只有一个数据源DataSource
,这个自动配置类才会生效。
哦?照这样搞,多数据源是不能用Mybatis吗?
可能大家会有一个误解,认为多数据源就是多个的 DataSource
并存的,当然这样说也不是不正确。
多数据源的情况下并不是多个数据源并存的,Spring 提供了 AbstractRoutingDataSource
这样一个 抽象类,使得能够在多数据源的情况下任意切换,相当于一个动态路由的作用,作者称之为动态数据源。因此Mybatis只需要配置这个动态数据源即可。
什么是动态数据源?
动态数据源简单的说就是能够自由切换的数据源,类似于一个动态路由的感觉,Spring 提供了一个抽象类 AbstractRoutingDataSource
,这个抽象类中有一个属性,如下:
1
| private Map<Object, Object> targetDataSources;
|
targetDataSources
是一个 Map
结构,所有需要切换的数据源都存放在其中,根据指定的 KEY 进行切换。当然还有一个默认的数据源。
AbstractRoutingDataSource
这个抽象类中有一个抽象方法需要子类实现,如下:
1
| protected abstract Object determineCurrentLookupKey();
|
determineCurrentLookupKey()
这个方法的返回值决定了需要切换的数据源的 KEY ,就是根据这个 KEY 从 targetDataSources
取值(数据源)。
数据源切换如何保证线程隔离?
数据源属于一个公共的资源,在多线程的情况下如何保证线程隔离呢?不能我这边切换了影响其他线程的执行。
说到线程隔离,自然会想到 ThreadLocal
了,将切换数据源的 KEY (用于从 targetDataSources
中取值)存储在 ThreadLocal 中,执行结束之后清除即可。
单独封装了一个 DataSourceHolder
,内部使用 ThreadLocal
隔离线程,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
public class DataSourceHolder { private static final ThreadLocal<String> dataSources = new InheritableThreadLocal();
public static void setDataSource(String datasource) { dataSources.set(datasource); } public static String getDataSource() { return dataSources.get(); } public static void clearDataSource() { dataSources.remove(); } }
|
如何构造一个动态数据源?
上文说过只需继承一个抽象类 AbstractRoutingDataSource
,重写其中的一个方法 determineCurrentLookupKey()
即可。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class DynamicDataSource extends AbstractRoutingDataSource {
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) { super.setDefaultTargetDataSource(defaultTargetDataSource); super.setTargetDataSources(targetDataSources); super.afterPropertiesSet(); }
@Override protected Object determineCurrentLookupKey() { return DataSourceHolder.getDataSource(); }
}
|
上述代码很简单,分析如下:
- 一个多参的构造方法,指定了默认的数据源和目标数据源。
- 重写
determineCurrentLookupKey()
方法,返回数据源对应的 KEY ,这里是直接从 ThreadLocal
中取值,就是上文封装的 DataSourceHolder
。
定义一个注解
为了操作方便且低耦合,不能每次需要切换的数据源的时候都要手动调一下接口吧,可以定义一个切换数据源的注解,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
@Target(value = ElementType.METHOD) @Retention(value = RetentionPolicy.RUNTIME) @Documented public @interface SwitchSource {
String DEFAULT_NAME = "hisDataSource";
String value() default DEFAULT_NAME; }
|
注解中只有一个 value 属性,指定了需要切换数据源的 KEY 。 有注解还不行,当然还要有切面,代码如下:
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
| @Aspect
@Order(Ordered.HIGHEST_PRECEDENCE) @Component @Slf4j public class DataSourceAspect {
@Pointcut("@annotation(SwitchSource)") public void pointcut() { }
@Before(value = "pointcut()") public void beforeOpt(JoinPoint joinPoint) {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); SwitchSource switchSource = method.getAnnotation(SwitchSource.class); log.info("[Switch DataSource]:" + switchSource.value()); DataSourceHolder.setDataSource(switchSource.value()); }
@After(value = "pointcut()") public void afterOpt() { DataSourceHolder.clearDataSource(); log.info("[Switch Default DataSource]"); } }
|
这个 ASPECT 很容易理解,beforeOpt()
在方法之前执行,取值 @SwitchSource
中value属性设置到 ThreadLocal
中;afterOpt()
方法在方法执行之后执行,清除掉 ThreadLocal
中的 KEY ,保证了如果不切换数据源,则用默认的数据源。
如何与Mybatis整合?
单一数据源与Mybatis整合上文已经详细讲解了,数据源 DataSource
作为参数构建了 SqlSessionFactory
,同样的思想,只需要把这个数据源换成动态数据源即可。注入的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
@Primary @Bean("sqlSessionFactory2") public SqlSessionFactory sqlSessionFactoryBean(DynamicDataSource dynamicDataSource) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dynamicDataSource); org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration(); configuration.setMapUnderscoreToCamelCase(true); configuration.setDefaultFetchSize(100); configuration.setDefaultStatementTimeout(30); sqlSessionFactoryBean.setConfiguration(configuration); return sqlSessionFactoryBean.getObject(); }
|
与Mybatis整合很简单,只需要把数据源替换成自定义的动态数据源 DynamicDataSource
。
那么动态数据源如何注入到IOC容器中呢?看上文自定义的 DynamicDataSource
构造方法,肯定需要两个数据源了,因此必须先注入两个或者多个数据源到IOC容器中,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
@Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource dataSource() { return new DruidDataSource(); }
@Bean(name = SwitchSource.DEFAULT_NAME) @ConfigurationProperties(prefix = "spring.datasource.his") public DataSource hisDataSource() { return DataSourceBuilder.create().build(); }
|
以上构建的两个数据源,一个是默认的数据源 ,一个是需要**切换到的数据源(targetDatasource
)**,这样就组成了动态数据源了。
这里还有一个问题:IOC中存在多个数据源了,那么事务管理器怎么办呢?它也懵逼了,到底选择 哪个数据源呢?因此事务管理器肯定还是要重新配置的。
1 2 3 4 5 6 7 8
|
@Primary @Bean(value = "transactionManager2") public PlatformTransactionManager annotationDrivenTransactionManager(DynamicDataSource dataSource){ return new DataSourceTransactionManager(dataSource); }
|
演示
使用也是很简单,在需要切换数据源的方法上方标注 @SwitchSource 切换到指定的数据源即可,如下:
1 2 3 4 5 6 7 8
| @Transactional(propagation = Propagation.NOT_SUPPORTED)
@SwitchSource @Override public List<DeptInfo> list() { return hisDeptInfoMapper.listDept(); }
|
这样只要执行到这方法将会切换到 HIS 的数据源,方法执行结束之后将会清除,执行默认的数据源。