当前位置:   article > 正文

SpringBoot 实现动态切换多数据源,这么做才叫优雅!

芋道源码获取数据源只有一个

点击上方“芋道源码”,选择“设为星标

管她前浪,还是后浪?

能浪的浪,才是好浪!

每天 10:33 更新文章,每天掉亿点点头发...

源码精品专栏

 

a6385a459b55fe46ece8bc0b9d9747ae.png


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

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

写这篇文章的目的

这篇文章将会涉及到Spring Boot 与Mybatis、数据库整合

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能。

项目地址:https://github.com/YunaiV/ruoyi-vue-pro

什么是多数据源?

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

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

  1. @Bean(name = "dataSource")
  2.     public DataSource dataSource() {
  3.         DruidDataSource druidDataSource = new DruidDataSource();
  4.         druidDataSource.setUrl(url);
  5.         druidDataSource.setUsername(username);
  6.         druidDataSource.setDriverClassName(driverClassName);
  7.         druidDataSource.setPassword(password);
  8.         return druidDataSource;
  9.     }

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

基于微服务的思想,构建在 B2C 电商场景下的项目实战。核心技术栈,是 Spring Boot + Dubbo 。未来,会重构成 Spring Cloud Alibaba 。

项目地址:https://github.com/YunaiV/onemall

何时用到多数据源?

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

  1. HIS提供视图,比如医护视图、患者视图等,而此时其他系统只需要定时的从HIS视图中读取数据同步到自己数据库中即可。

  2. HIS提供接口,无论是webService还是HTTP形式都是可行的,此时其他系统只需要按照要求调接口即可。

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

当然多数据源的使用场景还是有很多的,以上只是简单的一个场景。

整合单一的数据源

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

  1. <!--druid连接池-->
  2. <dependency>
  3.    <groupId>com.alibaba</groupId>
  4.    <artifactId>druid-spring-boot-starter</artifactId>
  5.    <version>1.1.9</version>
  6. </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):

  1. 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
  2. spring.datasource.username=root
  3. spring.datasource.password=xxxx
  4. spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
  5. spring.datasource.driver-class-name=com.mysql.jdbc.Driver
  6. #初始化连接大小
  7. spring.datasource.druid.initial-size=0
  8. #连接池最大使用连接数量
  9. spring.datasource.druid.max-active=20
  10. #连接池最小空闲
  11. spring.datasource.druid.min-idle=0
  12. #获取连接最大等待时间
  13. spring.datasource.druid.max-wait=6000
  14. spring.datasource.druid.validation-query=SELECT 1
  15. #spring.datasource.druid.validation-query-timeout=6000
  16. spring.datasource.druid.test-on-borrow=false
  17. spring.datasource.druid.test-on-return=false
  18. spring.datasource.druid.test-while-idle=true
  19. #配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
  20. spring.datasource.druid.time-between-eviction-runs-millis=60000
  21. #置一个连接在池中最小生存的时间,单位是毫秒
  22. spring.datasource.druid.min-evictable-idle-time-millis=25200000
  23. #spring.datasource.druid.max-evictable-idle-time-millis=
  24. #打开removeAbandoned功能,多少时间内必须关闭连接
  25. spring.datasource.druid.removeAbandoned=true
  26. #1800秒,也就是30分钟
  27. spring.datasource.druid.remove-abandoned-timeout=1800
  28. #<!-- 1800秒,也就是30分钟 -->
  29. spring.datasource.druid.log-abandoned=true
  30. spring.datasource.druid.filters=mergeStat

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

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

  1. @Bean(initMethod = "init")
  2.     @ConditionalOnMissingBean
  3.     public DataSource dataSource() {
  4.         LOGGER.info("Init DruidDataSource");
  5.         return new DruidDataSourceWrapper();
  6.     }

@ConditionalOnMissingBean@Bean这两个注解的结合,意味着我们可以覆盖,只需要提前在IOC中注入一个DataSource类型的Bean即可。

因此我们在自定义的配置类中定义如下配置即可:

  1. /**
  2.      * @Bean:向IOC容器中注入一个Bean
  3.      * @ConfigurationProperties:使得配置文件中以spring.datasource为前缀的属性映射到Bean的属性中
  4.      * @return
  5.      */
  6.     @ConfigurationProperties(prefix = "spring.datasource")
  7.     @Bean
  8.     public DataSource dataSource(){
  9.         //做一些其他的自定义配置,比如密码加密等......
  10.         return new DruidDataSource();
  11.     }

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

整合Mybatis

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

  1. <dependency>
  2.      <groupId>org.mybatis.spring.boot</groupId>
  3.      <artifactId>mybatis-spring-boot-starter</artifactId>
  4.      <version>2.0.0</version>
  5. </dependency>

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

@EnableConfigurationProperties(MybatisProperties.class)

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

可配置的东西很多,比如XML文件的位置类型处理器等等,如下简单的配置:

  1. mybatis.type-handlers-package=com.demo.typehandler
  2. mybatis.configuration.map-underscore-to-camel-case=true

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

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

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

  1. @Bean
  2.   @ConditionalOnMissingBean
  3.   public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {}

@ConditionalOnMissingBean@Bean真是老搭档了,意味着我们又可以覆盖,只需要在IOC容器中注入SqlSessionFactory(Mybatis六剑客之一生产者)

在自定义配置类中注入即可,如下:

  1. /**
  2.      * 注入SqlSessionFactory
  3.      */
  4.     @Bean("sqlSessionFactory1")
  5.     public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
  6.         SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
  7.         sqlSessionFactoryBean.setDataSource(dataSource);
  8.         sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:/mapper/**/*.xml"));
  9.         org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
  10.         // 自动将数据库中的下划线转换为驼峰格式
  11.         configuration.setMapUnderscoreToCamelCase(true);
  12.         configuration.setDefaultFetchSize(100);
  13.         configuration.setDefaultStatementTimeout(30);
  14.         sqlSessionFactoryBean.setConfiguration(configuration);
  15.         return sqlSessionFactoryBean.getObject();
  16.     }

以上介绍了配置Mybatis的两种方式,其实在大多数场景中使用第一种已经够用了,至于为什么介绍第二种呢?当然是为了多数据源的整合而做准备了。

MybatisAutoConfiguration中有一行很重要的代码,如下:

@ConditionalOnSingleCandidate(DataSource.class)

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

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

7d1e4dda8470ea98837edeba600e0178.png

多数据源如何整合?

上文留下的问题:为什么的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隔离线程,代码如下:

  1. /**
  2.  * 使用ThreadLocal存储切换数据源后的KEY
  3.  */
  4. public class DataSourceHolder {
  5.     //线程  本地环境
  6.     private static final ThreadLocal<String> dataSources = new InheritableThreadLocal();
  7.     //设置数据源
  8.     public static void setDataSource(String datasource) {
  9.         dataSources.set(datasource);
  10.     }
  11.     //获取数据源
  12.     public static String getDataSource() {
  13.         return dataSources.get();
  14.     }
  15.     //清除数据源
  16.     public static void clearDataSource() {
  17.         dataSources.remove();
  18.     }
  19. }

如何构造一个动态数据源?

上文说过只需继承一个抽象类AbstractRoutingDataSource,重写其中的一个方法determineCurrentLookupKey()即可。代码如下:

  1. /**
  2.  * 动态数据源,继承AbstractRoutingDataSource
  3.  */
  4. public class DynamicDataSource extends AbstractRoutingDataSource {
  5.     /**
  6.      * 返回需要使用的数据源的key,将会按照这个KEY从Map获取对应的数据源(切换)
  7.      * @return
  8.      */
  9.     @Override
  10.     protected Object determineCurrentLookupKey() {
  11.         //从ThreadLocal中取出KEY
  12.         return DataSourceHolder.getDataSource();
  13.     }
  14.     /**
  15.      * 构造方法填充Map,构建多数据源
  16.      */
  17.     public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
  18.         //默认的数据源,可以作为主数据源
  19.         super.setDefaultTargetDataSource(defaultTargetDataSource);
  20.         //目标数据源
  21.         super.setTargetDataSources(targetDataSources);
  22.         //执行afterPropertiesSet方法,完成属性的设置
  23.         super.afterPropertiesSet();
  24.     }
  25. }

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

  1. 一个多参的构造方法,指定了默认的数据源和目标数据源。

  2. 重写determineCurrentLookupKey()方法,返回数据源对应的KEY,这里是直接从ThreadLocal中取值,就是上文封装的DataSourceHolder

定义一个注解

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

  1. /**
  2.  * 切换数据源的注解
  3.  */
  4. @Target(value = ElementType.METHOD)
  5. @Retention(value = RetentionPolicy.RUNTIME)
  6. @Documented
  7. public @interface SwitchSource {
  8.     /**
  9.      * 默认切换的数据源KEY
  10.      */
  11.     String DEFAULT_NAME = "hisDataSource";
  12.     /**
  13.      * 需要切换到数据的KEY
  14.      */
  15.     String value() default DEFAULT_NAME;
  16. }

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

有注解还不行,当然还要有切面,代码如下:

  1. @Aspect
  2. //优先级要设置在事务切面执行之前
  3. @Order(1)
  4. @Component
  5. @Slf4j
  6. public class DataSourceAspect {
  7.     @Pointcut("@annotation(SwitchSource)")
  8.     public void pointcut() {
  9.     }
  10.     /**
  11.      * 在方法执行之前切换到指定的数据源
  12.      * @param joinPoint
  13.      */
  14.     @Before(value = "pointcut()")
  15.     public void beforeOpt(JoinPoint joinPoint) {
  16.         /*因为是对注解进行切面,所以这边无需做过多判定,直接获取注解的值,进行环绕,将数据源设置成远方,然后结束后,清楚当前线程数据源*/
  17.         Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
  18.         SwitchSource switchSource = method.getAnnotation(SwitchSource.class);
  19.         log.info("[Switch DataSource]:" + switchSource.value());
  20.         DataSourceHolder.setDataSource(switchSource.value());
  21.     }
  22.     /**
  23.      * 方法执行之后清除掉ThreadLocal中存储的KEY,这样动态数据源会使用默认的数据源
  24.      */
  25.     @After(value = "pointcut()")
  26.     public void afterOpt() {
  27.         DataSourceHolder.clearDataSource();
  28.         log.info("[Switch Default DataSource]");
  29.     }
  30. }

这个ASPECT很容易理解,beforeOpt()在方法之前执行,取值@SwitchSource中value属性设置到ThreadLocal中;afterOpt()方法在方法执行之后执行,清除掉ThreadLocal中的KEY,保证了如果不切换数据源,则用默认的数据源。

如何与Mybatis整合?

单一数据源与Mybatis整合上文已经详细讲解了,数据源DataSource作为参数构建了SqlSessionFactory,同样的思想,只需要把这个数据源换成动态数据源即可。注入的代码如下:

  1. /**
  2.      * 创建动态数据源的SqlSessionFactory,传入的是动态数据源
  3.      * @Primary这个注解很重要,如果项目中存在多个SqlSessionFactory,这个注解一定要加上
  4.      */
  5.     @Primary
  6.     @Bean("sqlSessionFactory2")
  7.     public SqlSessionFactory sqlSessionFactoryBean(DynamicDataSource dynamicDataSource) throws Exception {
  8.         SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
  9.         sqlSessionFactoryBean.setDataSource(dynamicDataSource);
  10.         sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:/mapper/**/*.xml"));
  11.         org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
  12.         configuration.setMapUnderscoreToCamelCase(true);
  13.         configuration.setDefaultFetchSize(100);
  14.         configuration.setDefaultStatementTimeout(30);
  15.         sqlSessionFactoryBean.setConfiguration(configuration);
  16.         return sqlSessionFactoryBean.getObject();
  17.     }

与Mybatis整合很简单,只需要把数据源替换成自定义的动态数据源DynamicDataSource

那么动态数据源如何注入到IOC容器中呢?看上文自定义的DynamicDataSource构造方法,肯定需要两个数据源了,因此必须先注入两个或者多个数据源到IOC容器中,如下:

  1. /**
  2.      * @Bean:向IOC容器中注入一个Bean
  3.      * @ConfigurationProperties:使得配置文件中以spring.datasource为前缀的属性映射到Bean的属性中
  4.      */
  5.     @ConfigurationProperties(prefix = "spring.datasource")
  6.     @Bean("dataSource")
  7.     public DataSource dataSource(){
  8.         return new DruidDataSource();
  9.     }
  10.     /**
  11.      * 向IOC容器中注入另外一个数据源
  12.      * 全局配置文件中前缀是spring.datasource.his
  13.      */
  14.     @Bean(name = SwitchSource.DEFAULT_NAME)
  15.     @ConfigurationProperties(prefix = "spring.datasource.his")
  16.     public DataSource hisDataSource() {
  17.         return DataSourceBuilder.create().build();
  18.     }

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

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

  1. /**
  2.      * 创建动态数据源的SqlSessionFactory,传入的是动态数据源
  3.      * @Primary这个注解很重要,如果项目中存在多个SqlSessionFactory,这个注解一定要加上
  4.      */
  5.     @Primary
  6.     @Bean("sqlSessionFactory2")
  7.     public SqlSessionFactory sqlSessionFactoryBean(DynamicDataSource dynamicDataSource) throws Exception {
  8.         SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
  9.         sqlSessionFactoryBean.setDataSource(dynamicDataSource);
  10.         org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
  11.         configuration.setMapUnderscoreToCamelCase(true);
  12.         configuration.setDefaultFetchSize(100);
  13.         configuration.setDefaultStatementTimeout(30);
  14.         sqlSessionFactoryBean.setConfiguration(configuration);
  15.         return sqlSessionFactoryBean.getObject();
  16.     }

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

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

  1. /**
  2.      * 重写事务管理器,管理动态数据源
  3.      */
  4.     @Primary
  5.     @Bean(value = "transactionManager2")
  6.     public PlatformTransactionManager annotationDrivenTransactionManager(DynamicDataSource dataSource) {
  7.         return new DataSourceTransactionManager(dataSource);
  8.     }

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

演示

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

  1. //不开启事务
  2.     @Transactional(propagation = Propagation.NOT_SUPPORTED)
  3.     //切换到HIS的数据源
  4.     @SwitchSource
  5.     @Override
  6.     public List<DeptInfo> list() {
  7.         return hisDeptInfoMapper.listDept();
  8.     }

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

总结

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

源码地址:https://github.com/chenjiabing666/datasource_demo



欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢

e25233efdfd0f1ee8b6c3e9ec79a494c.png

已在知识星球更新源码解析如下:

c9aabf16ab87e9cf92ff0642f476281e.png

555b3343c8df998bd6ebbd51d24a06df.png

06e75f4fe07865df557b3b95bc75d5f4.png

15d159ab72b10b2775cece28c382c766.png

最近更新《芋道 SpringBoot 2.X 入门》系列,已经 101 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。

提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。

获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

  1. 文章有帮助的话,在看,转发吧。
  2. 谢谢支持哟 (*^__^*)
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/article/detail/52360
推荐阅读
相关标签
  

闽ICP备14008679号