Spring Boot 中的多數(shù)據(jù)源配置方案

多數(shù)據(jù)源可以理解為多數(shù)據(jù)庫鸟蟹,甚至可以是多個不同類型的數(shù)據(jù)庫泣洞,比如一個是MySql,一個是Oracle秀仲。隨著項目的擴(kuò)大融痛,有時需要數(shù)據(jù)庫的拆分或者引入另一個數(shù)據(jù)庫,這時就需要配置多個數(shù)據(jù)源神僵。

SpringBoot中使用多數(shù)據(jù)源還是比較簡單的雁刷,為了演示方便,我們在MySql中創(chuàng)建兩個數(shù)據(jù)庫:ds1保礼、ds2沛励,并在ds1數(shù)據(jù)庫中創(chuàng)建student表,在ds2數(shù)據(jù)庫中創(chuàng)建teacher表炮障。數(shù)據(jù)庫腳本如下:

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------

-- Table structure for student

-- ----------------------------

DROP TABLE IF EXISTS `student`;
CREATE TABLE `student`  (
  `id` varchar(16) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
  `name` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
  `class` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;

-- ----------------------------

-- Records of student

-- ----------------------------

INSERT INTO `student` VALUES ('123456', 'zhangsan', '北京');
INSERT INTO `student` VALUES ('123457', 'lisi', '上海');

SET FOREIGN_KEY_CHECKS = 1;
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------

-- Table structure for teacher

-- ----------------------------

DROP TABLE IF EXISTS `teacher`;
CREATE TABLE `teacher`  (
  `id` varchar(16) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
  `name` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
  `class` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;

-- ----------------------------

-- Records of teacher

-- ----------------------------

INSERT INTO `teacher` VALUES ('0000001', 'wangwu', '上海');

SET FOREIGN_KEY_CHECKS = 1;

基于MyBatis的多數(shù)據(jù)源實現(xiàn)

首先創(chuàng)建一個MyBatis項目目派,項目結(jié)構(gòu)如下:

image

這里有一點需要注意, StudentMapper 接口和 TeacherMapper 接口是分開的胁赢,它們位于不同子目錄下企蹭,這個后面會提到。

數(shù)據(jù)庫連接配置

既然是多數(shù)據(jù)源智末,數(shù)據(jù)庫連接的信息就有可能存在不同谅摄,所以需要在配置文件中配置各個數(shù)據(jù)源的連接信息(這里使用了druid數(shù)據(jù)庫連接池)。

spring: 
  datasource:
    ds1: #數(shù)據(jù)源1系馆,默認(rèn)數(shù)據(jù)源
      url: jdbc:mysql://localhost:3306/ds1?serverTimezone=GMT&useSSL=false&useUnicode=true&characterEncoding=utf8
      username: root
      password: root
      typ: com.alibaba.druid.pool.DruidDataSource
      driver-class-name: com.mysql.cj.jdbc.Driver
      filters: stat
      maxActive: 2
      initialSize: 1
      maxWait: 60000
      minIdle: 1
      timeBetweenEvictionRunsMillis: 60000
      minEvictableIdleTimeMillis: 300000
      validationQuery: SELECT 1
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      poolPreparedStatements: true
      maxOpenPreparedStatements: 20

    ds2: #數(shù)據(jù)源2
      url: jdbc:mysql://localhost:3306/ds2?serverTimezone=GMT&useSSL=false&useUnicode=true&characterEncoding=utf8
      username: root
      password: root
      typ: com.alibaba.druid.pool.DruidDataSource
      driver-class-name: com.mysql.cj.jdbc.Driver
      filters: stat
      maxActive: 2
      initialSize: 1
      maxWait: 60000
      minIdle: 1
      timeBetweenEvictionRunsMillis: 60000
      minEvictableIdleTimeMillis: 300000
      validationQuery: SELECT 1
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      poolPreparedStatements: true
      maxOpenPreparedStatements: 20

注意不同的數(shù)據(jù)源要用不同的屬性名區(qū)分送漠。

重寫SpringBoot的數(shù)據(jù)源配置

1、數(shù)據(jù)源1的配置

@Configuration
@MapperScan(basePackages = {"com.chou.easyspringboot.multipledatasource.mapper.ds1"}, sqlSessionFactoryRef = "sqlSessionFactory1")
public class Datasource1Configuration {
    @Value("${mybatis.mapper-locations}")
    private String mapperLocation;
    @Value("${spring.datasource.ds1.url}")
    private String jdbcUrl;
    @Value("${spring.datasource.ds1.driver-class-name}")
    private String driverClassName;
    @Value("${spring.datasource.ds1.username}")
    private String username;
    @Value("${spring.datasource.ds1.password}")
    private String password;
    @Value("${spring.datasource.ds1.initialSize}")
    private int initialSize;
    @Value("${spring.datasource.ds1.minIdle}")
    private int minIdle;
    @Value("${spring.datasource.ds1.maxActive}")
    private int maxActive;

    @Bean(name = "dataSource1")
    @Primary
    public DataSource dataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUrl(jdbcUrl);
        dataSource.setDriverClassName(driverClassName);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        dataSource.setInitialSize(initialSize);
        dataSource.setMinIdle(minIdle);
        dataSource.setMaxActive(maxActive);

        return dataSource;
    }

    @Bean("sqlSessionFactory1")
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource1") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setMapperLocations(
                new PathMatchingResourcePatternResolver().getResources(mapperLocation));

        return sqlSessionFactoryBean.getObject();
    }

    @Bean("sqlSessionTemplate1")
    public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory1") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    @Bean("transactionManager1")
    public DataSourceTransactionManager transactionManager(@Qualifier("dataSource1")DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

2它呀、數(shù)據(jù)源2的配置

@Configuration
@MapperScan(basePackages = {"com.chou.easyspringboot.multipledatasource.mapper.ds2"}, sqlSessionFactoryRef = "sqlSessionFactory2")
public class Datasource2Configuration {
    @Value("${mybatis.mapper-locations}")
    private String mapperLocation;
    @Value("${spring.datasource.ds2.url}")
    private String jdbcUrl;
    @Value("${spring.datasource.ds2.driver-class-name}")
    private String driverClassName;
    @Value("${spring.datasource.ds2.username}")
    private String username;
    @Value("${spring.datasource.ds2.password}")
    private String password;
    @Value("${spring.datasource.ds2.initialSize}")
    private int initialSize;
    @Value("${spring.datasource.ds2.minIdle}")
    private int minIdle;
    @Value("${spring.datasource.ds2.maxActive}")
    private int maxActive;

    @Bean(name = "dataSource2")
    public DataSource dataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUrl(jdbcUrl);
        dataSource.setDriverClassName(driverClassName);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        dataSource.setInitialSize(initialSize);
        dataSource.setMinIdle(minIdle);
        dataSource.setMaxActive(maxActive);

        return dataSource;
    }

    @Bean("sqlSessionFactory2")
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource2") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setMapperLocations(
                new PathMatchingResourcePatternResolver().getResources(mapperLocation));

        return sqlSessionFactoryBean.getObject();
    }

    @Bean("sqlSessionTemplate2")
    public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory2") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    @Bean("transactionManager2")
    public DataSourceTransactionManager transactionManager(@Qualifier("dataSource2") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

這里和單數(shù)據(jù)源不同的地方在于對 dataSource 螺男、 sqlSessionFactorysqlSessionTemplate纵穿、 transactionManager 都進(jìn)行了單獨的配置下隧。另外,數(shù)據(jù)源1和數(shù)據(jù)源2主要存在兩點不同:

  1. @MapperScan 中的包掃描路徑不一樣谓媒,數(shù)據(jù)源1只掃描 com.chou.easyspringboot.multipledatasource.mapper.ds1 路徑下的 Mapper 淆院,數(shù)據(jù)源2負(fù)責(zé) com.chou.easyspringboot.multipledatasource.mapper.ds2下Mapper ,所以在前面創(chuàng)建的時候我們要把 StudentMapperTeacherMapper 分開句惯。因為在這里已經(jīng)配置了 @MapperScan 土辩,所以在啟動類中必須不能在存在 @MapperScan 注解

  2. 數(shù)據(jù)源1中多一個 @Primary 注解,這是告訴Spring我們使用的默認(rèn)數(shù)據(jù)源抢野,也是多數(shù)據(jù)源項目中必不可少的拷淘。

測試

編寫相應(yīng)的Controller和Service層代碼,查詢所有的Student和Teacher信息指孤,并使用postman模擬發(fā)送請求启涯,會有如下的運行結(jié)果:

  • 查詢所有的Student

    image
  • 查詢所有Teacher

    image

我們連續(xù)發(fā)送兩個不同的請求贬堵,都得出了想要的結(jié)果,說明MyBatis自動幫我們切換到了對應(yīng)的數(shù)據(jù)源上结洼。

基于自定義注解實現(xiàn)多數(shù)據(jù)源

上面我們提高到數(shù)據(jù)源自動切換主要依靠MyBatis黎做,如果項目中沒有使用MyBatis該如何做呢?

多數(shù)據(jù)源自動切換原理

這里介紹一種基于自定義注解的方法實現(xiàn)多數(shù)據(jù)源的動態(tài)切換松忍。SpringBoot中有一個 AbstractRoutingDataSource 抽象類蒸殿,我們可以實現(xiàn)其抽象方法 determineCurrentLookupKey() 去指定數(shù)據(jù)源。并通過AOP編寫自定義注解處理類鸣峭,在sql語句執(zhí)行前宏所,切換到自定義注解中設(shè)置的數(shù)據(jù)源以實現(xiàn)數(shù)據(jù)源的自動切換。

數(shù)據(jù)庫連接配置

同上配置兩個數(shù)據(jù)庫連接信息叽掘。

創(chuàng)建數(shù)據(jù)源存放類

DataSource 是和線程綁在一起的楣铁,因此,我們需要一個線程安全的類來存放 DataSource 更扁,在determineCurrentLookupKey() 中通過該類獲取數(shù)據(jù)源盖腕。

AbstractRoutingDataSource 類中, DataSource 以鍵值對的形式保存浓镜,可以使用 ThreadLocal 來保存key溃列,從而實現(xiàn)多數(shù)據(jù)源的自動切換。

public class DataSourceContextHolder {
    private static Logger logger = LoggerFactory.getLogger(DataSourceContextHolder.class);

    // 使用ThreadLocal線程安全的使用變量副本
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<String>();

    /**
     * 設(shè)置數(shù)據(jù)源
     * */
    public static void setDataSource(String dataSource) {
        logger.info("切換到數(shù)據(jù)源:{}", dataSource);
        CONTEXT_HOLDER.set(dataSource);
    }

    /**
     * 獲取數(shù)據(jù)源
     * */
    public static String getDataSource() {
        return CONTEXT_HOLDER.get();
    }

    /**
     * 清空數(shù)據(jù)源
     * */
    public static void clearDataSource() {
        CONTEXT_HOLDER.remove();
    }
}

數(shù)據(jù)源持有類定義了三個方法膛薛,分別用于數(shù)據(jù)源的設(shè)置听隐、獲取和清除。

創(chuàng)建數(shù)據(jù)源枚舉類

public enum DataSourceEnum {
    PRIMARY, //默認(rèn)數(shù)據(jù)源
    DATASOURCE1
}

實現(xiàn) determineCurrentLookupKey 方法指定數(shù)據(jù)源

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSource();
    }
}

配置數(shù)據(jù)源

@Configuration
public class DynamicDataSourceConfiguration {
    @Bean(name = "primaryDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.ds1")
    public DataSource primaryDataSource(){
        return new DruidDataSource();
    }

    @Bean(name = "dataSource1")
    @ConfigurationProperties(prefix = "spring.datasource.ds2")
    public DataSource dataSource1(){
        return new DruidDataSource();
    }

    @Bean("dynamicDataSource")
    @Primary
    public DataSource dynamicDataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        //配置默認(rèn)數(shù)據(jù)源
        dynamicDataSource.setDefaultTargetDataSource(primaryDataSource());

        //配置多數(shù)據(jù)源
        HashMap<Object, Object> dataSourceMap = new HashMap();
        dataSourceMap.put(DataSourceEnum.PRIMARY.name(),primaryDataSource());
        dataSourceMap.put(DataSourceEnum.DATASOURCE1.name(),dataSource1());
        dynamicDataSource.setTargetDataSources(dataSourceMap);
        return dynamicDataSource;

    }
}

自定義注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
    DataSourceEnum value() default DataSourceEnum.PRIMARY;
}

自定義注解指定作用于方法上并在運行期生效(可以在網(wǎng)上查下如何自定義注解哄啄,這里不在講述)雅任。

AOP攔截

通過AOP在執(zhí)行sql語句前攔截,并切換到自定義注解指定的數(shù)據(jù)源上咨跌。有一點需要注意沪么,自定義數(shù)據(jù)源注解與 @Transaction 注解同一個方法時會先執(zhí)行 @Transaction ,即獲取數(shù)據(jù)源在切換數(shù)據(jù)源之前锌半,所以會導(dǎo)致自定義注解失效禽车,因此需要使用 @Order (@Order的value越小,就越先執(zhí)行)刊殉,保證該AOP在 @Transactional 之前執(zhí)行殉摔。

@Aspect
@Component
@Order(-1)
public class DataSourceAspect {
    @Pointcut("@annotation(com.chou.easyspringboot.multipledatasource.annotation.DataSource)")
    public void dataSourcePointCut() {

    }

    @Around("dataSourcePointCut()")
    public Object dataSourceArround(ProceedingJoinPoint proceed) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) proceed.getSignature();
        Method method = methodSignature.getMethod();
        DataSource dataSource = method.getAnnotation(DataSource.class);
        if(dataSource != null) {
            DataSourceContextHolder.setDataSource(dataSource.value().name());
        }

        try {
            return proceed.proceed();
        } finally {
            // 方法執(zhí)行后銷毀數(shù)據(jù)源
            DataSourceContextHolder.clearDataSource();
        }
    }
}

創(chuàng)建啟動類,編寫Controller记焊、Service層代碼

需要在啟動類的 @SpringBootApplication 注解中移除DataSource自動配置類逸月,否則會默認(rèn)自動配置,而不會使用我們自定義的DataSource遍膜,并且啟動會有循環(huán)依賴的錯誤彻采。

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class EasyspringbootMultipledatasourceApplication {

   public static void main(String[] args) {
      SpringApplication.run(EasyspringbootMultipledatasourceApplication.class, args);
   }

}

測試

  • 查詢所有Student

    image
  • 查詢所有Teacher

    image

我們得到了正確的結(jié)果腐缤,數(shù)據(jù)源自動切換了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末肛响,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子惜索,更是在濱河造成了極大的恐慌特笋,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件巾兆,死亡現(xiàn)場離奇詭異猎物,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)角塑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進(jìn)店門蔫磨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人圃伶,你說我怎么就攤上這事堤如。” “怎么了窒朋?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵搀罢,是天一觀的道長。 經(jīng)常有香客問我侥猩,道長榔至,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任欺劳,我火速辦了婚禮唧取,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘划提。我一直安慰自己枫弟,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布腔剂。 她就那樣靜靜地躺著媒区,像睡著了一般。 火紅的嫁衣襯著肌膚如雪掸犬。 梳的紋絲不亂的頭發(fā)上袜漩,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天,我揣著相機(jī)與錄音湾碎,去河邊找鬼宙攻。 笑死,一個胖子當(dāng)著我的面吹牛介褥,可吹牛的內(nèi)容都是我干的座掘。 我是一名探鬼主播递惋,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼溢陪!你這毒婦竟也來了萍虽?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤形真,失蹤者是張志新(化名)和其女友劉穎杉编,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體咆霜,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡邓馒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蛾坯。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片光酣。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖脉课,靈堂內(nèi)的尸體忽然破棺而出救军,到底是詐尸還是另有隱情,我是刑警寧澤下翎,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布缤言,位于F島的核電站,受9級特大地震影響视事,放射性物質(zhì)發(fā)生泄漏胆萧。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一俐东、第九天 我趴在偏房一處隱蔽的房頂上張望跌穗。 院中可真熱鬧,春花似錦虏辫、人聲如沸蚌吸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽羹唠。三九已至,卻和暖如春娄昆,著一層夾襖步出監(jiān)牢的瞬間佩微,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工萌焰, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留哺眯,地道東北人。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓扒俯,卻偏偏與公主長得像奶卓,于是被迫代替她去往敵國和親一疯。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容