在分庫的業(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