全程解析,MyBatis在SpringBoot中的動態(tài)多數(shù)據(jù)源配置

在分庫的業(yè)務(wù)場景和跨數(shù)據(jù)庫實例獲取信息之類的場景中践宴,我們會遇到處理多個數(shù)據(jù)源訪問的問題,通常情況下可以采用中間件爷怀,如cobar, tddl, mycat等阻肩。

但取決于業(yè)務(wù)需求,有時我們需要直接通過MyBatis和SpringData來完成這個任務(wù)运授。即使沒有烤惊,理解MyBatis多數(shù)據(jù)源配置的過程也有助于理解其他分庫分表操作的原理。

背景依賴如下:

<dependencies>

? ? <dependency>

? ? ? <groupId>org.springframework.boot</groupId>

? ? ? <artifactId>spring-boot-starter-jdbc</artifactId>

? ? </dependency>

? ? <dependency>

? ? ? <groupId>mysql</groupId>

? ? ? <artifactId>mysql-connector-java</artifactId>

? ? </dependency>

? ? <dependency>

? ? ? <groupId>org.mybatis.spring.boot</groupId>

? ? ? <artifactId>mybatis-spring-boot-starter</artifactId>

? ? </dependency>

</dependencies>

要進(jìn)行多數(shù)據(jù)源的配置吁朦,首先需要了解MyBatis是如何將XML中的Sql語句執(zhí)行的柒室,是哪些類提供了數(shù)據(jù)庫的連接,又是哪些類提供了配置參數(shù)逗宜。

首先來看MyBatis的SQL執(zhí)行過程:

MyBatisSQL執(zhí)行過程

不難看出雄右,與數(shù)據(jù)源相關(guān)的處理是在第4、5步中完成的纺讲。第四步獲取到的SqlSessionFactory為第五步的SqlSession提供了連接工廠不脯,也就是說我們只需要對第四步進(jìn)行處理,替換掉原生的DefaultSqlSessionFactory即可刻诊。

接下來,在SpringBoot框架下牺丙,我們可以通過常用的FactoryBean<T>來嘗試獲取SqlSessionFactory:

通過查找FactoryBean與SqlSessionFactory的交集则涯,不難找到SqlSessionFactoryBean,這個類中包含大量與數(shù)據(jù)庫連接配置相關(guān)的字段冲簿。

SqlSessionFactoryBean

并且因為它實現(xiàn)了FactoryBean<T>粟判,可以通過getObject()方法來獲得一個SqlSessionFactory的實例。

通過分析SqlSessionFactoryBean的參數(shù)峦剔,對于多數(shù)據(jù)源的處理档礁,基本的可以分為兩種思路:

不同數(shù)據(jù)源使用的SQL語句不同(一般見于跨業(yè)務(wù)實例數(shù)據(jù)訪問),通過不同的SqlSessionFactory管理不同包中的mapper來實現(xiàn)吝沫。

不同數(shù)據(jù)源使用的SQL語句相同(一般見于分表場景)呻澜,通過在語句執(zhí)行前動態(tài)替換線程所使用的數(shù)據(jù)源來完成递礼。

對于第一種情況,處理方式非常簡單羹幸,通過配置多個SqlSessionFactory脊髓,為每一個配置不同的MapperLocations來管理。本文不細(xì)講這種情況栅受。

對于第二種情況将硝,相對復(fù)雜一些,我們接下來一步一步分析屏镊。

SqlSessionFactory進(jìn)行數(shù)據(jù)庫連接的核心是通過DataSource完成的依疼,因此需要獲取一個可以調(diào)整規(guī)則的非固化DataSource

通過對javax.sql.DataSource接口進(jìn)行分析,可以發(fā)現(xiàn)AbstractDataSource是絕大部分Spring數(shù)據(jù)源的父類而芥,與此不同的是我們的連接池數(shù)據(jù)源(如HikariDataSource)和驅(qū)動數(shù)據(jù)源(如MySqlDataSource)律罢,由于我們使用SpringBoot框架進(jìn)行IOC托管,并且通過mybatis-spring-boot-starter進(jìn)行mybatis接入蔚出,因此我們進(jìn)一步調(diào)研AbstractDataSource弟翘。

經(jīng)過簡單的父子關(guān)系跟蹤,我們發(fā)現(xiàn)Spring提供了一個動態(tài)配置數(shù)據(jù)源的抽象類AbstractRoutingDataSource骄酗,我們只需要對這個類進(jìn)行routing部分的實現(xiàn)即可完成需求.

P.S. Spring全都想到了稀余,tql...

這個抽象類需要重寫的方法是protected abstract Object determineCurrentLookupKey()返回值雖然是Object類型,但意思實際上是允許我們自定義key而避免IllegalArgumentException等相關(guān)的問題趋翻。因此我們先去看一下這個key對應(yīng)的map是一個什么結(jié)構(gòu)睛琳。

省去無關(guān)代碼后,AbstractRoutingDataSource對dataSource的map相關(guān)操作實際上基于下面的這個部分

/**

* 數(shù)據(jù)源Map key即determineCurrentLookupKey()方法返回的Object踏烙,value即為動態(tài)切換到的目標(biāo)dataSource

*/

@Nullable

private Map<Object, Object> targetDataSources;

/**

* 當(dāng)determineCurrentLookupKey()返回的結(jié)果無法獲取到一個可用的DataSource時师骗,采用的默認(rèn)數(shù)據(jù)源

*/

@Nullable

private Object defaultTargetDataSource;

換言之,在AbstractRoutingDataSource中實際上維護了多個DataSource讨惩,我們只需要將自定義的key獲取方法寫入determineCurrentLookupKey()辟癌,并將數(shù)據(jù)源map和默認(rèn)數(shù)據(jù)源set進(jìn)這兩個變量中即可。

重寫AbstractRoutingDataSource荐捻,提供determineCurrentLookupKey()方法的實現(xiàn)

這個部分為了將數(shù)據(jù)源的切換與DynamicDataSource隔離黍少,我選擇通過編寫一個DataSourceSwitcher來作為數(shù)據(jù)源選擇的中介。眾所周知处面,MyBatis的事務(wù)和sql執(zhí)行都是基于SqlSessionHolder進(jìn)行的線程隔離厂置,其內(nèi)部是基于ThreadLocal完成的。這個方法很好的解決了單例對象復(fù)用時的線程安全問題魂角。因此參考這種形式昵济,switcher應(yīng)該提供基于ThreadLocal的DataSource選擇機制。

// DataSourceSwitcher.java

@Component

public class DataSourceSwitcher {

? ? private static final ThreadLocal<Integer> DATA_SOURCE = new ThreadLocal<>();

? ? public int chooseDefaultDataSource() {

? ? ? ? DATA_SOURCE.set(0);

? ? ? ? return 0;

? ? }

? ? public void chooseDataSource(int index) {

? ? ? ? DATA_SOURCE.set(index);

? ? }

? ? public static Integer getDataSource() {

? ? ? ? return DATA_SOURCE.get();

? ? }

? ? public void clear() {

? ? ? ? DATA_SOURCE.remove();

? ? }

}

而我們重寫的AbstractRoutingDataSource則應(yīng)接入為

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**

* <p>動態(tài)數(shù)據(jù)源</p>

*

* @author zora

* @since 2020.09.15

*/

public class DynamicDataSource extends AbstractRoutingDataSource {

? ? @Override

? ? protected Object determineCurrentLookupKey() {

? ? ? ? return DataSourceSwitcher.getDataSource();

? ? }

}

到目前為止,動態(tài)數(shù)據(jù)源的切換部分我們已經(jīng)完成访忿,接下來需要進(jìn)行數(shù)據(jù)源的提供瞧栗。

向AbstractRoutingDataSource中的兩個setter提供對應(yīng)的內(nèi)容

最簡單的當(dāng)然是new幾個DataSource,但是大部分環(huán)境中醉顽,我們是通過連接池進(jìn)行數(shù)據(jù)庫連接沼溜,而不是每次去創(chuàng)建新的連接對象。而連接池與數(shù)據(jù)庫的交互需要有最基本的4個參數(shù)游添。

首先創(chuàng)建DatabaseSetting類作為數(shù)據(jù)模版系草。

@Data

public class DatabaseSetting {

? ? /**

? ? * 用戶名

? ? */

? ? private String username;

? ? /**

? ? * 密碼

? ? */

? ? private String password;

? ? /**

? ? * 連接url

? ? */

? ? private String url;

? ? /**

? ? * driver

? ? */

? ? private String driver;

}

然后,本文以主流的HikariPool作為示例唆涝,首先創(chuàng)建一個獲取hikari配置的映射器找都。

import com.zaxxer.hikari.HikariConfig;

import lombok.Setter;

import lombok.extern.slf4j.Slf4j;

import org.apache.commons.collections.CollectionUtils;

import org.springframework.boot.context.properties.ConfigurationProperties;

import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;

import java.util.Comparator;

import java.util.List;

import java.util.stream.Collectors;

/**

* <h3>cloud-userPlayTime</h3>

* <h4>com.metaapp.cloud.userplaytime.config.db</h4>

* <p>動態(tài)數(shù)據(jù)源yml配置映射</p>

*

* @author zora

* @since 2020.09.15

*/

@Slf4j

@Configuration

@ConfigurationProperties(prefix = "spring.datasource")

public class DynamicDataSourceValueMapper {

? ? @Setter

? ? @Getter

? ? private List<DatabaseSetting> dynamic;

? ? @Setter

? ? @Getter

? ? private HikariConfig hikari;

? ? @PostConstruct

? ? private void statePrint() {

? ? ? ? dynamic = dynamic.stream().sorted(Comparator.comparingInt(DatabaseSetting::getId)).collect(Collectors.toList());

? ? ? ? StringBuilder builder = new StringBuilder();

? ? ? ? builder.append("【");

? ? ? ? if (CollectionUtils.isEmpty(dynamic)) {

? ? ? ? ? ? builder.append("配置失敗,數(shù)據(jù)源為空");

? ? ? ? ? ? builder.append('】');

? ? ? ? ? ? log.error("多數(shù)據(jù)源{}", builder.toString());

? ? ? ? } else {

? ? ? ? ? ? for (DatabaseSetting databaseSetting : dynamic) {

? ? ? ? ? ? ? ? builder.append('{').append("UserName=").append(databaseSetting.getUrl()).append(", ").append("Url=").append(databaseSetting.getUrl()).append("} ,");

? ? ? ? ? ? }

? ? ? ? ? ? builder.deleteCharAt(builder.lastIndexOf(","));

? ? ? ? ? ? builder.append('】');

? ? ? ? ? ? log.info("多數(shù)據(jù)源配置獲取完畢廊酣,配置信息為{}", builder.toString());

? ? ? ? }

? ? }

}

接下來能耻,通過DynamicDataSourceValueMapper提供的元數(shù)據(jù),開始創(chuàng)建對應(yīng)的多個數(shù)據(jù)源.

@Autowired

private DynamicDataSourceValueMapper dynamicDataSourceValueMapper;

/**

* 基于元數(shù)據(jù)創(chuàng)建多個HikariDataSource

*

* @return 對應(yīng)到AbstractRoutingDataSource中Map的數(shù)據(jù)集

*/

@Bean(name = "dynamicDataSourceList")

public List<DataSource> getDataSourceList() {

? ? List<DatabaseSetting> settingList = dynamicDataSourceValueMapper.getDynamic();

? ? HikariConfig hikariPoolConfig = dynamicDataSourceValueMapper.getHikari();

? ? List<DataSource> dataSourceList = new ArrayList<>(settingList.size());

? ? for (DatabaseSetting databaseSetting : settingList) {

? ? ? ? HikariConfig currentHikariConfig = new HikariConfig();

? ? ? ? hikariPoolConfig.copyStateTo(currentHikariConfig);

? ? ? ? currentHikariConfig.setDataSource(DataSourceBuilder.create()

? ? ? ? ? ? ? ? .driverClassName(databaseSetting.getDriver())

? ? ? ? ? ? ? ? .url(databaseSetting.getUrl())

? ? ? ? ? ? ? ? .password(databaseSetting.getPassword())

? ? ? ? ? ? ? ? .username(databaseSetting.getUsername())

? ? ? ? ? ? ? ? .build());

? ? ? ? dataSourceList.add(new HikariDataSource(currentHikariConfig));

? ? }

? ? return dataSourceList;

}

/**

* 創(chuàng)建真正的"動態(tài)切換"數(shù)據(jù)源

*

* @param dataSourceList 上面方法提供的HikariDataSource

* @return 實際使用的DynamicDataSource

*/

@Bean(name = "dynamicDataSource")

public DynamicDataSource getDynamicDataSource(@Qualifier(value = "dynamicDataSourceList") List<DataSource> dataSourceList) {

? Map<Object, Object> targetDataSource = new HashMap<>(dataSourceList.size());

? for (int i = 0; i < dataSourceList.size(); i++) {

? ? DataSource dataSource = dataSourceList.get(i);

? ? targetDataSource.put(i, dataSource);

? }

? DynamicDataSource dataSource = new DynamicDataSource();

? dataSource.setTargetDataSources(targetDataSource);

? dataSource.setDefaultTargetDataSource(dataSourceList.get(0));

? return dataSource;

}

至此亡驰,動態(tài)數(shù)據(jù)源的切換部分已經(jīng)完成晓猛。在需要進(jìn)行數(shù)據(jù)源切換的時候,注入DataSourceSwitcher并調(diào)用chooseDataSource(int index)方法即可凡辱〗渲埃可以根據(jù)具體場景,采用aop等其他形式進(jìn)行增強透乾。

結(jié)合到MyBatis中洪燥,需要更新SqlSessionFactory以提供對應(yīng)的SqlSession

因為我們是基于MyBatis來做數(shù)據(jù)映射,因此我們在重寫數(shù)據(jù)源的過程中乳乌,需要保證mybatis與我們的數(shù)據(jù)源能夠正常關(guān)聯(lián)捧韵。因此,我們需要重新提供sqlSessionFactory給容器汉操。

@Bean(name = "MybatisConfiguration")

@ConfigurationProperties("mybatis.configuration")

public org.apache.ibatis.session.Configuration mybatisConfiguration() {

? ? return new org.apache.ibatis.session.Configuration();

}

@Bean(name = "SqlSessionFactory")

public SqlSessionFactory dynamicSqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource, @Qualifier("MybatisConfiguration") org.apache.ibatis.session.Configuration configuration)

? ? ? ? throws Exception {

? ? SqlSessionFactoryBean bean = new SqlSessionFactoryBean();

? ? bean.setDataSource(dynamicDataSource);

? ? bean.setConfiguration(configuration);

? ? // 調(diào)整MapperLocation指定到實際的mapper路徑即可再来。

? ? bean.setMapperLocations(

? ? ? ? ? ? new PathMatchingResourcePatternResolver().getResources("classpath*:com/zora/demo/mapper/mapping/*.xml"));

? ? return bean.getObject();

}

亞馬遜測評 www.yisuping.com

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市磷瘤,隨后出現(xiàn)的幾起案子其弊,更是在濱河造成了極大的恐慌,老刑警劉巖膀斋,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異痹雅,居然都是意外死亡仰担,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來摔蓝,“玉大人赂苗,你說我怎么就攤上這事≈荆” “怎么了拌滋?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長猜谚。 經(jīng)常有香客問我败砂,道長,這世上最難降的妖魔是什么魏铅? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任昌犹,我火速辦了婚禮,結(jié)果婚禮上览芳,老公的妹妹穿的比我還像新娘斜姥。我一直安慰自己,他們只是感情好沧竟,可當(dāng)我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布铸敏。 她就那樣靜靜地躺著,像睡著了一般悟泵。 火紅的嫁衣襯著肌膚如雪杈笔。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天魁袜,我揣著相機與錄音桩撮,去河邊找鬼。 笑死峰弹,一個胖子當(dāng)著我的面吹牛店量,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播鞠呈,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼融师,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蚁吝?” 一聲冷哼從身側(cè)響起旱爆,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎窘茁,沒想到半個月后怀伦,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡山林,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年房待,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡桑孩,死狀恐怖拜鹤,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情流椒,我是刑警寧澤敏簿,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站宣虾,受9級特大地震影響惯裕,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜安岂,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一轻猖、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧域那,春花似錦咙边、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至淑蔚,卻和暖如春市殷,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背刹衫。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工醋寝, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人带迟。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓音羞,卻偏偏與公主長得像,于是被迫代替她去往敵國和親仓犬。 傳聞我的和親對象是個殘疾皇子嗅绰,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,611評論 2 353

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