Spring Boot与多数据源那点事儿~

目录

  • 前言
  • 写这篇文章的目的
  • 什么是多数据源?
  • 什么时候用到多数据源?
  • 整合单一的数据源
  • 整合Mybatis
  • 多数据源如何整合?
    • 什么是动态数据源?
    • 数据源切换如何保证线程隔离?
    • 如何构造一个动态数据源?
    • 定义一个注解
    • 如何与Mybatis整合?
    • 演示
  • 总结

前言

大约在19年的这个时候,老同事公司在作医疗系统,须要和HIS系统对接一些信息,好比患者、医护、医嘱、科室等信息。可是起初并不知道如何与HIS无缝对接,因而向我取经。java

最终通过讨论采用了视图对接的方式,大体就是HIS系统提供视图,他们进行对接。mysql

写这篇文章的目的

这篇文章将会涉及到Spring Boot 与Mybatis、数据库整合,相似于整合Mybatis与数据库的文章其实网上不少,做者此前也写过一篇文章详细的介绍了一些整合的套路:Spring Boot 整合多点套路,少走点弯路~,有兴趣的能够看看。web

什么是多数据源?

最多见的单一应用中最多涉及到一个数据库,便是一个数据源(Datasource)。那么顾名思义,多数据源就是在一个单一应用中涉及到了两个及以上的数据库了。spring

其实在配置数据源的时候就已经很明确这个定义了,如如下代码:sql

@Bean(name = "dataSource")
    public DataSource dataSource() {
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setUrl(url);
        druidDataSource.setUsername(username);
        druidDataSource.setDriverClassName(driverClassName);
        druidDataSource.setPassword(password);
        return druidDataSource;
    }
复制代码

urlusernamepassword这三个属性已经惟一肯定了一个数据库了,DataSource则是依赖这三个建立出来的。则多数据源便是配置多个DataSource(暂且这么理解)。数据库

什么时候用到多数据源?

正如前言介绍到的一个场景,相信大多数作过医疗系统的都会和HIS打交道,为了简化护士以及医生的操做流程,必需要将必要的信息从HIS系统对接过来,据我了解的大体有两种方案以下:apache

  1. HIS提供视图,好比医护视图、患者视图等,而此时其余系统只须要定时的从HIS视图中读取数据同步到本身数据库中便可。
  2. HIS提供接口,不管是webService仍是HTTP形式都是可行的,此时其余系统只须要按照要求调接口便可。

很明显第一种方案涉及到了至少两个数据库了,一个是HIS数据库,一个本身系统的数据库,在单一应用中必然须要用到多数据源的切换才能达到目的。markdown

固然多数据源的使用场景仍是有不少的,以上只是简单的一个场景。session

整合单一的数据源

本文使用阿里的数据库链接池druid,添加依赖以下:mybatis

<!--druid链接池-->
<dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>druid-spring-boot-starter</artifactId>
   <version>1.1.9</version>
</dependency>
复制代码

阿里的数据库链接池很是强大,好比数据监控数据库加密等等内容,本文仅仅演示与Spring Boot整合的过程,一些其余的功能后续能够本身研究添加。

Druid链接池的starter的自动配置类是DruidDataSourceAutoConfigure,类上标注以下一行注解:

@EnableConfigurationProperties({DruidStatProperties.class, DataSourceProperties.class})
复制代码

@EnableConfigurationProperties这个注解使得配置文件中的配置生效而且映射到指定类的属性。

DruidStatProperties中指定的前缀是spring.datasource.druid,这个配置主要是用来设置链接池的一些参数。

DataSourceProperties中指定的前缀是spring.datasource,这个主要是用来设置数据库的urlusernamepassword等信息。

所以咱们只须要在全局配置文件中指定数据库的一些配置以及链接池的一些配置信息便可,前缀分别是spring.datasource.druidspring.datasource,如下是我的随便配置的(application.properties):

spring.datasource.url=jdbc\:mysql\://120.26.101.xxx\:3306/xxx?useUnicode\=true&characterEncoding\=UTF-8&zeroDateTimeBehavior\=convertToNull&useSSL\=false&allowMultiQueries\=true&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=xxxx
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#初始化链接大小
spring.datasource.druid.initial-size=0
#链接池最大使用链接数量
spring.datasource.druid.max-active=20
#链接池最小空闲
spring.datasource.druid.min-idle=0
#获取链接最大等待时间
spring.datasource.druid.max-wait=6000
spring.datasource.druid.validation-query=SELECT 1
#spring.datasource.druid.validation-query-timeout=6000
spring.datasource.druid.test-on-borrow=false
spring.datasource.druid.test-on-return=false
spring.datasource.druid.test-while-idle=true
#配置间隔多久才进行一次检测,检测须要关闭的空闲链接,单位是毫秒
spring.datasource.druid.time-between-eviction-runs-millis=60000
#置一个链接在池中最小生存的时间,单位是毫秒
spring.datasource.druid.min-evictable-idle-time-millis=25200000
#spring.datasource.druid.max-evictable-idle-time-millis=
#打开removeAbandoned功能,多少时间内必须关闭链接
spring.datasource.druid.removeAbandoned=true
#1800秒,也就是30分钟
spring.datasource.druid.remove-abandoned-timeout=1800
#<!-- 1800秒,也就是30分钟 -->
spring.datasource.druid.log-abandoned=true
spring.datasource.druid.filters=mergeStat
复制代码

在全局配置文件application.properties文件中配置以上的信息便可注入一个数据源到Spring Boot中。其实这仅仅是一种方式,下面介绍另一种方式。

在自动配置类中DruidDataSourceAutoConfigure中有以下一段代码:

@Bean(initMethod = "init")
    @ConditionalOnMissingBean
    public DataSource dataSource() {
        LOGGER.info("Init DruidDataSource");
        return new DruidDataSourceWrapper();
    }
复制代码

@ConditionalOnMissingBean@Bean这两个注解的结合,意味着咱们能够覆盖,只须要提早在IOC中注入一个DataSource类型的Bean便可。

所以咱们在自定义的配置类中定义以下配置便可:

/** * @Bean:向IOC容器中注入一个Bean * @ConfigurationProperties:使得配置文件中以spring.datasource为前缀的属性映射到Bean的属性中 * @return */
    @ConfigurationProperties(prefix = "spring.datasource")
    @Bean
    public DataSource dataSource(){
        //作一些其余的自定义配置,好比密码加密等......
        return new DruidDataSource();
    }
复制代码

以上介绍了两种数据源的配置方式,第一种比较简单,第二种适合扩展,按需选择。

整合Mybatis

Spring Boot 整合Mybatis其实很简单,简单的几步就搞定,首先添加依赖:

<dependency>
     <groupId>org.mybatis.spring.boot</groupId>
     <artifactId>mybatis-spring-boot-starter</artifactId>
     <version>2.0.0</version>
</dependency>
复制代码

第二步找到自动配置类MybatisAutoConfiguration,有以下一行代码:

@EnableConfigurationProperties(MybatisProperties.class)
复制代码

老套路了,全局配置文件中配置前缀为mybatis的配置将会映射到该类中的属性。

可配置的东西不少,好比XML文件的位置类型处理器等等,以下简单的配置:

mybatis.type-handlers-package=com.demo.typehandler
mybatis.configuration.map-underscore-to-camel-case=true
复制代码

若是须要经过包扫描的方式注入Mapper,则须要在配置类上加入一个注解:@MapperScan,其中的value属性指定须要扫描的包。

直接在全局配置文件配置各类属性是一种比较简单的方式,其实的任何组件的整合都有很多于两种的配置方式,下面来介绍下配置类如何配置。

MybatisAutoConfiguration自动配置类有以下一断代码:

@Bean
  @ConditionalOnMissingBean
  public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {}
复制代码

@ConditionalOnMissingBean@Bean真是老搭档了,意味着咱们又能够覆盖,只须要在IOC容器中注入SqlSessionFactory(Mybatis六剑客之一辈子产者)

在自定义配置类中注入便可,以下:

/** * 注入SqlSessionFactory */
    @Bean("sqlSessionFactory1")
    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中有一行很重要的代码,以下:

@ConditionalOnSingleCandidate(DataSource.class)
复制代码

@ConditionalOnSingleCandidate这个注解的意思是当IOC容器中只有一个候选Bean的实例才会生效。

这行代码标注在Mybatis的自动配置类中有何含义呢?下面介绍,哈哈哈~

多数据源如何整合?

上文留下的问题:为何的Mybatis自动配置上标注以下一行代码:

@ConditionalOnSingleCandidate(DataSource.class)
复制代码

以上这行代码的言外之意:当IOC容器中只有一个数据源DataSource,这个自动配置类才会生效。

哦?照这样搞,多数据源是不能用Mybatis吗?

可能你们会有一个误解,认为多数据源就是多个的DataSource并存的,固然这样说也不是不正确。

多数据源的状况下并非多个数据源并存的,Spring提供了AbstractRoutingDataSource这样一个抽象类,使得可以在多数据源的状况下任意切换,至关于一个动态路由的做用,做者称之为动态数据源。所以Mybatis只须要配置这个动态数据源便可。

什么是动态数据源?

动态数据源简单的说就是可以自由切换的数据源,相似于一个动态路由的感受,Spring 提供了一个抽象类AbstractRoutingDataSource,这个抽象类中哟一个属性,以下:

private Map<Object, Object> targetDataSources;
复制代码

targetDataSources是一个Map结构,全部须要切换的数据源都存放在其中,根据指定的KEY进行切换。固然还有一个默认的数据源。

AbstractRoutingDataSource这个抽象类中有一个抽象方法须要子类实现,以下:

protected abstract Object determineCurrentLookupKey();
复制代码

determineCurrentLookupKey()这个方法的返回值决定了须要切换的数据源的KEY,就是根据这个KEYtargetDataSources取值(数据源)。

数据源切换如何保证线程隔离?

数据源属于一个公共的资源,在多线程的状况下如何保证线程隔离呢?不能我这边切换了影响其余线程的执行。

说到线程隔离,天然会想到ThreadLocal了,将切换数据源的KEY(用于从targetDataSources中取值)存储在ThreadLocal中,执行结束以后清除便可。

单独封装了一个DataSourceHolder,内部使用ThreadLocal隔离线程,代码以下:

/** * 使用ThreadLocal存储切换数据源后的KEY */
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()便可。代码以下:

/** * 动态数据源,继承AbstractRoutingDataSource */
public class DynamicDataSource extends AbstractRoutingDataSource {

    /** * 返回须要使用的数据源的key,将会按照这个KEY从Map获取对应的数据源(切换) * @return */
    @Override
    protected Object determineCurrentLookupKey() {
        //从ThreadLocal中取出KEY
        return DataSourceHolder.getDataSource();
    }

    /** * 构造方法填充Map,构建多数据源 */
    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
        //默认的数据源,能够做为主数据源
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        //目标数据源
        super.setTargetDataSources(targetDataSources);
        //执行afterPropertiesSet方法,完成属性的设置
        super.afterPropertiesSet();
    }
}
复制代码

上述代码很简单,分析以下:

  1. 一个多参的构造方法,指定了默认的数据源和目标数据源。
  2. 重写determineCurrentLookupKey()方法,返回数据源对应的KEY,这里是直接从ThreadLocal中取值,就是上文封装的DataSourceHolder

定义一个注解

为了操做方便且低耦合,不能每次须要切换的数据源的时候都要手动调一下接口吧,能够定义一个切换数据源的注解,以下:

/** * 切换数据源的注解 */
@Target(value = ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
@Documented
public @interface SwitchSource {

    /** * 默认切换的数据源KEY */
    String DEFAULT_NAME = "hisDataSource";

    /** * 须要切换到数据的KEY */
    String value() default DEFAULT_NAME;
}
复制代码

注解中只有一个value属性,指定了须要切换数据源的KEY

有注解还不行,固然还要有切面,代码以下:

@Aspect
//优先级设置到最高
@Order(Ordered.HIGHEST_PRECEDENCE)
@Component
@Slf4j
public class DataSourceAspect {


    @Pointcut("@annotation(SwitchSource)")
    public void pointcut() {
    }

    /** * 在方法执行以前切换到指定的数据源 * @param joinPoint */
    @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());
    }

    /** * 方法执行以后清除掉ThreadLocal中存储的KEY,这样动态数据源会使用默认的数据源 */
    @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,一样的思想,只须要把这个数据源换成动态数据源便可。注入的代码以下:

/** * 建立动态数据源的SqlSessionFactory,传入的是动态数据源 * @Primary这个注解很重要,若是项目中存在多个SqlSessionFactory,这个注解必定要加上 */
    @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容器中,以下:

/** * @Bean:向IOC容器中注入一个Bean * @ConfigurationProperties:使得配置文件中以spring.datasource为前缀的属性映射到Bean的属性中 */
    @ConfigurationProperties(prefix = "spring.datasource")
    @Bean("dataSource")
    public DataSource dataSource(){
        return new DruidDataSource();
    }

    /** * 向IOC容器中注入另一个数据源 * 全局配置文件中前缀是spring.datasource.his */
    @Bean(name = SwitchSource.DEFAULT_NAME)
    @ConfigurationProperties(prefix = "spring.datasource.his")
    public DataSource hisDataSource() {
        return DataSourceBuilder.create().build();
    }
复制代码

以上构建的两个数据源,一个是默认的数据源,一个是须要切换到的数据源(targetDataSources,这样就组成了动态数据源了。数据源的一些信息,好比urlusername须要本身在全局配置文件中根据指定的前缀配置便可,代码再也不贴出。

动态数据源的注入代码以下:

/** * 建立动态数据源的SqlSessionFactory,传入的是动态数据源 * @Primary这个注解很重要,若是项目中存在多个SqlSessionFactory,这个注解必定要加上 */
    @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();
    }
复制代码

这里还有一个问题:IOC中存在多个数据源了,那么事务管理器怎么办呢?它也懵逼了,到底选择哪一个数据源呢?所以事务管理器确定仍是要从新配置的。

事务管理器此时管理的数据源将是动态数据源DynamicDataSource,配置以下:

/** * 重写事务管理器,管理动态数据源 */
    @Primary
    @Bean(value = "transactionManager2")
    public PlatformTransactionManager annotationDrivenTransactionManager(DynamicDataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
复制代码

至此,Mybatis与多数据源的整合就完成了。

演示

使用也是很简单,在须要切换数据源的方法上方标注@SwitchSource切换到指定的数据源便可,以下:

//不开启事务
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    //切换到HIS的数据源
    @SwitchSource
    @Override
    public List<DeptInfo> list() {
        return hisDeptInfoMapper.listDept();
    }
复制代码

这样只要执行到这方法将会切换到HIS的数据源,方法执行结束以后将会清除,执行默认的数据源。

总结

本篇文章讲了Spring Boot与单数据源、Mybatis、多数据源之间的整合,但愿这篇文章可以帮助读者理解多数据源的整合,虽然说用的很少,可是在有些领域仍然是比较重要的。

原创不易,点点赞分享一波,谢谢支持~

源码已经上传,须要源码的朋友公众号回复关键词多数据源

相关文章
相关标签/搜索