Spring Boot與多資料來源那點事兒~

愛撒謊的男孩發表於2020-10-22

持續原創輸出,點選上方藍字關注我

目錄

  • 前言
  • 寫這篇文章的目的
  • 什麼是多資料來源?
  • 何時用到多資料來源?
  • 整合單一的資料來源
  • 整合Mybatis
  • 多資料來源如何整合?
    • 什麼是動態資料來源?
    • 資料來源切換如何保證執行緒隔離?
    • 如何構造一個動態資料來源?
    • 定義一個註解
    • 如何與Mybatis整合?
    • 演示
  • 總結

前言

大約在19年的這個時候,老同事公司在做醫療系統,需要和HIS系統對接一些資訊,比如患者、醫護、醫囑、科室等資訊。但是起初並不知道如何與HIS無縫對接,於是向我取經。

最終經過討論採用了檢視對接的方式,大致就是HIS系統提供檢視,他們進行對接。

寫這篇文章的目的

這篇文章將會涉及到Spring Boot 與Mybatis、資料庫整合,類似於整合Mybatis與資料庫的文章其實網上很多,作者此前也寫過一篇文章詳細的介紹了一些整合的套路:Spring Boot 整合多點套路,少走點彎路~,有興趣的可以看看。

什麼是多資料來源?

最常見的單一應用中最多涉及到一個資料庫,即是一個資料來源(Datasource)。那麼顧名思義,多資料來源就是在一個單一應用中涉及到了兩個及以上的資料庫了。

其實在配置資料來源的時候就已經很明確這個定義了,如以下程式碼:

    @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系統對接過來,據我瞭解的大致有兩種方案如下:

  1. HIS提供檢視,比如醫護檢視、患者檢視等,而此時其他系統只需要定時的從HIS檢視中讀取資料同步到自己資料庫中即可。
  2. HIS提供介面,無論是webService還是HTTP形式都是可行的,此時其他系統只需要按照要求調介面即可。

很明顯第一種方案涉及到了至少兩個資料庫了,一個是HIS資料庫,一個自己系統的資料庫,在單一應用中必然需要用到多資料來源的切換才能達到目的。

當然多資料來源的使用場景還是有很多的,以上只是簡單的一個場景。

整合單一的資料來源

本文使用阿里的資料庫連線池druid,新增依賴如下:

<!--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、多資料來源之間的整合,希望這篇文章能夠幫助讀者理解多資料來源的整合,雖說用的不多,但是在有些領域仍然是比較重要的。

原創不易,點點贊分享一波,謝謝支援~

原始碼已經上傳,需要原始碼的朋友回覆關鍵詞多資料來源

相關文章