淺析SpringBoot多數(shù)據(jù)源實現(xiàn)方案
SpringBoot多數(shù)據(jù)源實現(xiàn)方案
現(xiàn)在很多項目的開發(fā)過程中,可能涉及到多個數(shù)據(jù)源,像讀寫分離的場景,或者因為業(yè)務(wù)復(fù)雜,導(dǎo)致不同的業(yè)務(wù)部署在不同的數(shù)據(jù)庫上,那么這樣的場景,我們應(yīng)該如何在代碼中簡潔方便的切換數(shù)據(jù)源呢?分析這個需求,我們發(fā)現(xiàn)要做的事情無非兩件
- 構(gòu)建多個數(shù)據(jù)源
- 封裝一個模塊能實現(xiàn)動態(tài)的切換數(shù)據(jù)源,且數(shù)據(jù)源的切換代碼應(yīng)該盡量和業(yè)務(wù)進行解耦
構(gòu)建多個數(shù)據(jù)源
構(gòu)建多個數(shù)據(jù)源其實比較簡單,和構(gòu)建一個數(shù)據(jù)源是類似的。在SpringBoot中,只需要做三件事
- 將數(shù)據(jù)庫的配置注冊到配置文件中
- 選擇一個數(shù)據(jù)庫連接池來構(gòu)建數(shù)據(jù)源,我們這里用阿里出品的
Druid
- 選擇一個orm框架來實現(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
注冊多個數(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(); } }
動態(tài)切換數(shù)據(jù)源
spring提供的方案
關(guān)于動態(tài)切換數(shù)據(jù)源,spring給我們提供了一套解決方案,主要通過AbstractRoutingDataSource
類實現(xiàn),這個類是一個抽象類,每次和數(shù)據(jù)庫的交互都會調(diào)用該類的getConnection()
方法獲取數(shù)據(jù)庫連接,而getConnection()
方法會調(diào)用determineCurrentLookupKey
先選擇一個正確的數(shù)據(jù)源,數(shù)據(jù)源如何選擇呢?他的具體實現(xiàn)是,由我們開發(fā)人員提前將所有的數(shù)據(jù)源通過K-V的格式放到一個map中,V是具體的數(shù)據(jù)源,K是數(shù)據(jù)源的唯一標(biāo)識。然后將這個map交給AbstractRoutingDataSource
去管理,在需要路由的時候他會根據(jù)給定的K從map中匹配對應(yīng)的數(shù)據(jù)源。那么K又怎么來呢?哪個接口應(yīng)該用哪個key呢?AbstractRoutingDataSource
給我們提供了一個抽象方法determineTargetDataSource()
,供我們自定義實現(xiàn)key的確定邏輯。這個其實是對模板方法模式的典型應(yīng)用,核心代碼如下:
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean { // map結(jié)構(gòu),用來保存所有的數(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ù)庫連接,選擇數(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ù)源,但是哪個接口用那個key,由用戶自己決定,這就是模板方法模式 */ @Nullable protected abstract Object determineCurrentLookupKey(); }
構(gòu)建動態(tài)數(shù)據(jù)源
在了解了上述的基本原理后,我們就可以著手構(gòu)建我們的動態(tài)數(shù)據(jù)源啦,首先自定義一個類繼承AbstractRoutingDataSource
,實現(xiàn)determineCurrentLookupKey()
方法。
/** * 繼承spring提供的多數(shù)據(jù)源路由類,初始化默認(rèn)數(shù)據(jù)源和實現(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(); } // 實現(xiàn)抽象方法,定義我們獲取K的邏輯 @Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.getDataSource(); } }
DataSourceContextHolder
類使用ThreadLocal
來存儲當(dāng)前線程使用的數(shù)據(jù)源名稱。通過setDataSourceKey()
方法設(shè)置數(shù)據(jù)源名稱,通過getDataSourceKey()
方法獲取數(shù)據(jù)源名稱,通過clearDataSourceKey()
方法清除數(shù)據(jù)源名稱。
這里用ThreadLocal
的主要原因是為了做多并發(fā)線程隔離,比如同一時間可能會有很多請求并發(fā)進來,假設(shè)有10個請求,然后系統(tǒng)分配線程1處理請求1,請求1需要用mster數(shù)據(jù)源,線程2處理請求2,請求2需要用slave數(shù)據(jù)源。他們可能同時在進行,那么我們?nèi)绾螌⑦@些請求需要的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 // 一定要寫,讓DynamicDataSource被容器優(yōu)先選擇 @Bean public DynamicDataSource dynamicDataSource(@Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("slaveDataSource") DataSource slaveDataSource) { // 所有數(shù)據(jù)源放到一個map中,交給動態(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); } }
通過切面將業(yè)務(wù)和數(shù)據(jù)源切換模塊解耦
現(xiàn)在動態(tài)數(shù)據(jù)源切換的方案有了,那么如何能將每一個請求路由的到正確的數(shù)據(jù)源,而且將這些和業(yè)務(wù)無關(guān)的代碼和業(yè)務(wù)進行解耦呢。是的,我們可以用aop,構(gòu)建一個切面,在實現(xiàn)一個自定義注解,將注解標(biāo)記在需要切換數(shù)據(jù)源的接口上,讓每一個請求處理之前先去選擇數(shù)據(jù)源,在處理業(yè)務(wù)邏輯,最后返回結(jié)果是不是就OK了?說干就干
/** * 自定義注解用來選擇數(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í)行原來的方法,最后返回結(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 { // 請求處理完成后一定要及時釋放ThreadLocal數(shù)據(jù),否則會引起內(nèi)存泄漏 DataSourceContextHolder.release(); } } }
到此這篇關(guān)于SpringBoot多數(shù)據(jù)源實現(xiàn)方案的文章就介紹到這了,更多相關(guān)SpringBoot多數(shù)據(jù)源內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- springboot項目如何配置多數(shù)據(jù)源
- SpringBoot實現(xiàn)JPA多數(shù)據(jù)源配置小結(jié)
- SpringBoot實現(xiàn)多數(shù)據(jù)源配置的示例詳解
- SpringBoot中實現(xiàn)多數(shù)據(jù)源連接和切換的方案
- springboot項目實現(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ù)源問題
相關(guān)文章
Spring?@Conditional注解示例詳細(xì)講解
@Conditional是Spring4新提供的注解,它的作用是按照一定的條件進行判斷,滿足條件給容器注冊bean,這篇文章主要介紹了Spring?@Conditional注解示例詳細(xì)講解,需要的朋友可以參考下2022-11-11MyBatis的各種查詢功能結(jié)果接收類型的選擇(推薦)
文章介紹了MyBatis中查詢結(jié)果的不同接收方式,包括單條數(shù)據(jù)和多條數(shù)據(jù)的處理方法,以及MyBatis的默認(rèn)類型別名,感興趣的朋友跟隨小編一起看看吧2024-11-11mybatis plus saveOrUpdate實現(xiàn)有重復(fù)數(shù)據(jù)就更新,否則新增方式
這篇文章主要介紹了mybatis plus saveOrUpdate實現(xiàn)有重復(fù)數(shù)據(jù)就更新,否則新增方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-12-12一文了解Java讀寫鎖ReentrantReadWriteLock的使用
ReentrantReadWriteLock稱為讀寫鎖,它提供一個讀鎖,支持多個線程共享同一把鎖。這篇文章主要講解一下ReentrantReadWriteLock的使用和應(yīng)用場景,感興趣的可以了解一下2022-10-10