在分庫的業務場景和跨資料庫例項獲取資訊之類的場景中,我們會遇到處理多個資料來源訪問的問題,通常情況下可以採用中介軟體,如cobar, tddl, mycat等。
但取決於業務需求,有時我們需要直接通過MyBatis和SpringData來完成這個任務。即使沒有,理解MyBatis多資料來源配置的過程也有助於理解其他分庫分表操作的原理。
背景依賴如下:
<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>
要進行多資料來源的配置,首先需要了解MyBatis是如何將XML中的Sql語句執行的,是哪些類提供了資料庫的連線,又是哪些類提供了配置引數。
首先來看MyBatis的SQL執行過程:
不難看出,與資料來源相關的處理是在第4、5步中完成的。第四步獲取到的SqlSessionFactory
為第五步的SqlSession
提供了連線工廠,也就是說我們只需要對第四步進行處理,替換掉原生的DefaultSqlSessionFactory
即可。
接下來,在SpringBoot框架下,我們可以通過常用的FactoryBean<T>
來嘗試獲取SqlSessionFactory
:
通過查詢FactoryBean
與SqlSessionFactory
的交集,不難找到SqlSessionFactoryBean
,這個類中包含大量與資料庫連線配置相關的欄位。
並且因為它實現了FactoryBean<T>
,可以通過getObject()
方法來獲得一個SqlSessionFactory
的例項。
通過分析SqlSessionFactoryBean
的引數,對於多資料來源的處理,基本的可以分為兩種思路:
- 不同資料來源使用的SQL語句不同(一般見於跨業務例項資料訪問),通過不同的
SqlSessionFactory
管理不同包中的mapper來實現。 - 不同資料來源使用的SQL語句相同(一般見於分表場景),通過在語句執行前動態替換執行緒所使用的資料來源來完成。
對於第一種情況,處理方式非常簡單,通過配置多個SqlSessionFactory
,為每一個配置不同的MapperLocations
來管理。本文不細講這種情況。
對於第二種情況,相對複雜一些,我們接下來一步一步分析。
SqlSessionFactory進行資料庫連線的核心是通過DataSource
完成的,因此需要獲取一個可以調整規則的非固化DataSource
通過對javax.sql.DataSource
介面進行分析,可以發現AbstractDataSource
是絕大部分Spring資料來源的父類,與此不同的是我們的連線池資料來源(如HikariDataSource
)和驅動資料來源(如MySqlDataSource
),由於我們使用SpringBoot框架進行IOC託管,並且通過mybatis-spring-boot-starter進行mybatis接入,因此我們進一步調研AbstractDataSource
。
經過簡單的父子關係跟蹤,我們發現Spring提供了一個動態配置資料來源的抽象類AbstractRoutingDataSource
,我們只需要對這個類進行routing部分的實現即可完成需求.
P.S. Spring全都想到了,tql...
這個抽象類需要重寫的方法是protected abstract Object determineCurrentLookupKey()
返回值雖然是Object型別,但意思實際上是允許我們自定義key而避免IllegalArgumentException
等相關的問題。因此我們先去看一下這個key對應的map是一個什麼結構。
省去無關程式碼後,AbstractRoutingDataSource
對dataSource的map相關操作實際上基於下面的這個部分
/**
* 資料來源Map key即determineCurrentLookupKey()方法返回的Object,value即為動態切換到的目標dataSource
*/
@Nullable
private Map<Object, Object> targetDataSources;
/**
* 當determineCurrentLookupKey()返回的結果無法獲取到一個可用的DataSource時,採用的預設資料來源
*/
@Nullable
private Object defaultTargetDataSource;
換言之,在AbstractRoutingDataSource
中實際上維護了多個DataSource,我們只需要將自定義的key獲取方法寫入determineCurrentLookupKey()
,並將資料來源map和預設資料來源set進這兩個變數中即可。
重寫AbstractRoutingDataSource
,提供determineCurrentLookupKey()
方法的實現
這個部分為了將資料來源的切換與DynamicDataSource隔離,我選擇通過編寫一個DataSourceSwitcher
來作為資料來源選擇的中介。眾所周知,MyBatis的事務和sql執行都是基於SqlSessionHolder
進行的執行緒隔離,其內部是基於ThreadLocal
完成的。這個方法很好的解決了單例物件複用時的執行緒安全問題。因此參考這種形式,switcher應該提供基於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
則應接入為
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* <p>動態資料來源</p>
*
* @author zora
* @since 2020.09.15
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceSwitcher.getDataSource();
}
}
到目前為止,動態資料來源的切換部分我們已經完成,接下來需要進行資料來源的提供。
向AbstractRoutingDataSource
中的兩個setter提供對應的內容
最簡單的當然是new幾個DataSource,但是大部分環境中,我們是通過連線池進行資料庫連線,而不是每次去建立新的連線物件。而連線池與資料庫的互動需要有最基本的4個引數。
首先建立DatabaseSetting類作為資料模版。
@Data
public class DatabaseSetting {
/**
* 使用者名稱
*/
private String username;
/**
* 密碼
*/
private String password;
/**
* 連線url
*/
private String url;
/**
* driver
*/
private String driver;
}
然後,本文以主流的HikariPool
作為示例,首先建立一個獲取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>動態資料來源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("配置失敗,資料來源為空");
builder.append('】');
log.error("多資料來源{}", 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("多資料來源配置獲取完畢,配置資訊為{}", builder.toString());
}
}
}
接下來,通過DynamicDataSourceValueMapper
提供的後設資料,開始建立對應的多個資料來源.
@Autowired
private DynamicDataSourceValueMapper dynamicDataSourceValueMapper;
/**
* 基於後設資料建立多個HikariDataSource
*
* @return 對應到AbstractRoutingDataSource中Map的資料集
*/
@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;
}
/**
* 建立真正的"動態切換"資料來源
*
* @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;
}
至此,動態資料來源的切換部分已經完成。在需要進行資料來源切換的時候,注入DataSourceSwitcher
並呼叫chooseDataSource(int index)
方法即可。可以根據具體場景,採用aop等其他形式進行增強。
結合到MyBatis中,需要更新SqlSessionFactory以提供對應的SqlSession
因為我們是基於MyBatis來做資料對映,因此我們在重寫資料來源的過程中,需要保證mybatis與我們的資料來源能夠正常關聯。因此,我們需要重新提供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);
// 調整MapperLocation指定到實際的mapper路徑即可。
bean.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources("classpath*:com/zora/demo/mapper/mapping/*.xml"));
return bean.getObject();
}
如果我的文章對您有所幫助,希望能夠點右下角?支援一下,不勝感謝?