淺析SpringBoot多數(shù)據(jù)源實(shí)現(xiàn)方案
SpringBoot多數(shù)據(jù)源實(shí)現(xiàn)方案
現(xiàn)在很多項(xiàng)目的開(kāi)發(fā)過(guò)程中,可能涉及到多個(gè)數(shù)據(jù)源,像讀寫(xiě)分離的場(chǎng)景,或者因?yàn)闃I(yè)務(wù)復(fù)雜,導(dǎo)致不同的業(yè)務(wù)部署在不同的數(shù)據(jù)庫(kù)上,那么這樣的場(chǎng)景,我們應(yīng)該如何在代碼中簡(jiǎn)潔方便的切換數(shù)據(jù)源呢?分析這個(gè)需求,我們發(fā)現(xiàn)要做的事情無(wú)非兩件
- 構(gòu)建多個(gè)數(shù)據(jù)源
- 封裝一個(gè)模塊能實(shí)現(xiàn)動(dòng)態(tài)的切換數(shù)據(jù)源,且數(shù)據(jù)源的切換代碼應(yīng)該盡量和業(yè)務(wù)進(jìn)行解耦
構(gòu)建多個(gè)數(shù)據(jù)源
構(gòu)建多個(gè)數(shù)據(jù)源其實(shí)比較簡(jiǎn)單,和構(gòu)建一個(gè)數(shù)據(jù)源是類(lèi)似的。在SpringBoot中,只需要做三件事
- 將數(shù)據(jù)庫(kù)的配置注冊(cè)到配置文件中
- 選擇一個(gè)數(shù)據(jù)庫(kù)連接池來(lái)構(gòu)建數(shù)據(jù)源,我們這里用阿里出品的
Druid - 選擇一個(gè)orm框架來(lái)實(shí)現(xiàn)基本的sql,我們這里選用
Mybatis
springboot配置文件
spring:
datasource:
master:
url: jdbc:mysql://localhost:3306/db_master
username: root
password: ******
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
slave:
url: jdbc:mysql://localhost:3306/db_slave
username: root
password: Hxy@950504
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
mybatis:
mapper-locations: classpath:mapper/**/*.xml注冊(cè)多個(gè)數(shù)據(jù)源
@Configuration
public class DataSourceConfig {
@Bean(name = "masterDataSource")
@ConfigurationProperties("spring.datasource.master")
public DataSource masterDataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "slaveDataSource")
@ConfigurationProperties("spring.datasource.slave")
public DataSource slaveDataSource() {
return DruidDataSourceBuilder.create().build();
}
}動(dòng)態(tài)切換數(shù)據(jù)源
spring提供的方案
關(guān)于動(dòng)態(tài)切換數(shù)據(jù)源,spring給我們提供了一套解決方案,主要通過(guò)AbstractRoutingDataSource類(lèi)實(shí)現(xiàn),這個(gè)類(lèi)是一個(gè)抽象類(lèi),每次和數(shù)據(jù)庫(kù)的交互都會(huì)調(diào)用該類(lèi)的getConnection() 方法獲取數(shù)據(jù)庫(kù)連接,而getConnection() 方法會(huì)調(diào)用determineCurrentLookupKey先選擇一個(gè)正確的數(shù)據(jù)源,數(shù)據(jù)源如何選擇呢?他的具體實(shí)現(xiàn)是,由我們開(kāi)發(fā)人員提前將所有的數(shù)據(jù)源通過(guò)K-V的格式放到一個(gè)map中,V是具體的數(shù)據(jù)源,K是數(shù)據(jù)源的唯一標(biāo)識(shí)。然后將這個(gè)map交給AbstractRoutingDataSource去管理,在需要路由的時(shí)候他會(huì)根據(jù)給定的K從map中匹配對(duì)應(yīng)的數(shù)據(jù)源。那么K又怎么來(lái)呢?哪個(gè)接口應(yīng)該用哪個(gè)key呢?AbstractRoutingDataSource給我們提供了一個(gè)抽象方法determineTargetDataSource(),供我們自定義實(shí)現(xiàn)key的確定邏輯。這個(gè)其實(shí)是對(duì)模板方法模式的典型應(yīng)用,核心代碼如下:
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
// map結(jié)構(gòu),用來(lái)保存所有的數(shù)據(jù)源
@Nullable
private Map<Object, Object> targetDataSources;
// 默認(rèn)的數(shù)據(jù)源
@Nullable
private Object defaultTargetDataSource;
@Override
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
}
this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = resolveSpecifiedLookupKey(key);
DataSource dataSource = resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}
@Override
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}
/**
* getConnection()方法和determineTargetDataSource()方法定義了獲取數(shù)據(jù)庫(kù)連接,選擇數(shù)據(jù)源的核心邏輯
*/
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = determineCurrentLookupKey();
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;
}
/**
* 根據(jù)key選擇數(shù)據(jù)源,但是哪個(gè)接口用那個(gè)key,由用戶自己決定,這就是模板方法模式
*/
@Nullable
protected abstract Object determineCurrentLookupKey();
}構(gòu)建動(dòng)態(tài)數(shù)據(jù)源
在了解了上述的基本原理后,我們就可以著手構(gòu)建我們的動(dòng)態(tài)數(shù)據(jù)源啦,首先自定義一個(gè)類(lèi)繼承AbstractRoutingDataSource,實(shí)現(xiàn)determineCurrentLookupKey()方法。
/**
* 繼承spring提供的多數(shù)據(jù)源路由類(lèi),初始化默認(rèn)數(shù)據(jù)源和實(shí)現(xiàn)選擇數(shù)據(jù)源的方法
*
* @author HXY
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
// 有參構(gòu)造器,初始化所有的數(shù)據(jù)源和默認(rèn)數(shù)據(jù)源
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> allDataSource) {
super.setDefaultTargetDataSource(defaultTargetDataSource);
super.setTargetDataSources(allDataSource);
super.afterPropertiesSet();
}
// 實(shí)現(xiàn)抽象方法,定義我們獲取K的邏輯
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
}DataSourceContextHolder類(lèi)使用ThreadLocal來(lái)存儲(chǔ)當(dāng)前線程使用的數(shù)據(jù)源名稱(chēng)。通過(guò)setDataSourceKey()方法設(shè)置數(shù)據(jù)源名稱(chēng),通過(guò)getDataSourceKey()方法獲取數(shù)據(jù)源名稱(chēng),通過(guò)clearDataSourceKey()方法清除數(shù)據(jù)源名稱(chēng)。
這里用ThreadLocal的主要原因是為了做多并發(fā)線程隔離,比如同一時(shí)間可能會(huì)有很多請(qǐng)求并發(fā)進(jìn)來(lái),假設(shè)有10個(gè)請(qǐng)求,然后系統(tǒng)分配線程1處理請(qǐng)求1,請(qǐng)求1需要用mster數(shù)據(jù)源,線程2處理請(qǐng)求2,請(qǐng)求2需要用slave數(shù)據(jù)源。他們可能同時(shí)在進(jìn)行,那么我們?nèi)绾螌⑦@些請(qǐng)求需要的key做線程隔離呢,使之不互相影響呢?ThreadLocal就可以做到。
public class DataSourceContextHolder {
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
public static void setDataSource(String dataSourceKey) {
CONTEXT_HOLDER.set(dataSourceKey);
}
public static String getDataSource() {
return CONTEXT_HOLDER.get();
}
public static void release() {
CONTEXT_HOLDER.remove();
}
}@Configuration
public class DataSourceConfig {
@Bean(name = "masterDataSource")
@ConfigurationProperties("spring.datasource.master")
public DataSource masterDataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "slaveDataSource")
@ConfigurationProperties("spring.datasource.slave")
public DataSource slaveDataSource() {
return DruidDataSourceBuilder.create().build();
}
// DynamicDataSource要交給spring管理
@Primary // 一定要寫(xiě),讓DynamicDataSource被容器優(yōu)先選擇
@Bean
public DynamicDataSource dynamicDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slaveDataSource") DataSource slaveDataSource) {
// 所有數(shù)據(jù)源放到一個(gè)map中,交給動(dòng)態(tài)數(shù)據(jù)源管理
Map<Object, Object> targetDataSources = new HashMap<>(2);
targetDataSources.put(DataSourceEnum.MASTER.name(), masterDataSource);
targetDataSources.put(DataSourceEnum.SLAVE.name(), slaveDataSource);
// 默認(rèn)數(shù)據(jù)源、所有數(shù)據(jù)源
return new DynamicDataSource(masterDataSource, targetDataSources);
}
}通過(guò)切面將業(yè)務(wù)和數(shù)據(jù)源切換模塊解耦
現(xiàn)在動(dòng)態(tài)數(shù)據(jù)源切換的方案有了,那么如何能將每一個(gè)請(qǐng)求路由的到正確的數(shù)據(jù)源,而且將這些和業(yè)務(wù)無(wú)關(guān)的代碼和業(yè)務(wù)進(jìn)行解耦呢。是的,我們可以用aop,構(gòu)建一個(gè)切面,在實(shí)現(xiàn)一個(gè)自定義注解,將注解標(biāo)記在需要切換數(shù)據(jù)源的接口上,讓每一個(gè)請(qǐng)求處理之前先去選擇數(shù)據(jù)源,在處理業(yè)務(wù)邏輯,最后返回結(jié)果是不是就OK了?說(shuō)干就干
/**
* 自定義注解用來(lái)選擇數(shù)據(jù)源
*
* @author HXY
* @since 1.0.0
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
DataSourceEnum key() default DataSourceEnum.MASTER;
}public enum DataSourceEnum {
MASTER,
SLAVE,
;
}@Aspect
@Component
public class DynamicDataSourceAspect {
// 用環(huán)繞通知攔截標(biāo)記了DataSource注解的方法,方法執(zhí)行前選擇數(shù)據(jù)源,然后執(zhí)行原來(lái)的方法,最后返回結(jié)果
@Around("@annotation(dataSource)")
public Object selectDataSource(ProceedingJoinPoint joinPoint, DataSource dataSource) throws Throwable {
try {
String selectKey = dataSource.key().name();
DataSourceContextHolder.setDataSource(selectKey);
return joinPoint.proceed();
} finally {
// 請(qǐng)求處理完成后一定要及時(shí)釋放ThreadLocal數(shù)據(jù),否則會(huì)引起內(nèi)存泄漏
DataSourceContextHolder.release();
}
}
}到此這篇關(guān)于SpringBoot多數(shù)據(jù)源實(shí)現(xiàn)方案的文章就介紹到這了,更多相關(guān)SpringBoot多數(shù)據(jù)源內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- springboot項(xiàng)目如何配置多數(shù)據(jù)源
- SpringBoot實(shí)現(xiàn)JPA多數(shù)據(jù)源配置小結(jié)
- SpringBoot實(shí)現(xiàn)多數(shù)據(jù)源配置的示例詳解
- SpringBoot中實(shí)現(xiàn)多數(shù)據(jù)源連接和切換的方案
- springboot項(xiàng)目實(shí)現(xiàn)多數(shù)據(jù)源配置使用dynamic-datasource-spring-boot-starter的操作步驟
- springboot配置多數(shù)據(jù)源的一款框架(dynamic-datasource-spring-boot-starter)
- SpringBoot利用dynamic-datasource-spring-boot-starter解決多數(shù)據(jù)源問(wèn)題
相關(guān)文章
MyBatis動(dòng)態(tài)SQL之<choose><when><o(jì)therwise>標(biāo)簽的使用
MyBatis中動(dòng)態(tài)語(yǔ)句choose-when-otherwise 類(lèi)似于Java中的switch-case-default語(yǔ)句,本文就來(lái)介紹一下MyBatis動(dòng)態(tài)SQL之<choose><when><o(jì)therwise>標(biāo)簽的使用,感興趣的可以了解一下2023-09-09
java優(yōu)先隊(duì)列PriorityQueue中Comparator的用法詳解
這篇文章主要介紹了java優(yōu)先隊(duì)列PriorityQueue中Comparator的用法詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-02-02
Java布隆過(guò)濾器的應(yīng)用實(shí)例
這篇文章主要介紹了Java布隆過(guò)濾器的應(yīng)用實(shí)例,在程序的世界中,布隆過(guò)濾器是程序員的一把利器,利用它可以快速地解決項(xiàng)目中一些比較棘手的問(wèn)題,如網(wǎng)頁(yè)?URL?去重、垃圾郵件識(shí)別、大集合中重復(fù)元素的判斷和緩存穿透等問(wèn)題,需要的朋友可以參考下2023-11-11
IDEA使用JDBC安裝配置jar包連接MySQL數(shù)據(jù)庫(kù)
這篇文章介紹了IDEA使用JDBC安裝配置jar包連接MySQL數(shù)據(jù)庫(kù)的方法,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-01-01

