SpringBoot實(shí)現(xiàn)多數(shù)據(jù)源的切換實(shí)踐
前言
我們?cè)谶M(jìn)行軟件開發(fā)的過程中,剛開始的時(shí)候因?yàn)闊o法估量系統(tǒng)后期的訪問量和并發(fā)量,所以一開始會(huì)采用單體架構(gòu),后期如果網(wǎng)站流量變大, 并發(fā)量變大,那么就可能會(huì)將架構(gòu)擴(kuò)展為微服務(wù)架構(gòu),各個(gè)微服務(wù)對(duì)應(yīng)一個(gè)數(shù)據(jù)庫,不過這樣的成本就有點(diǎn)大了,可能只是有些模塊用的人比較多, 有些模塊沒什么人用,如果都進(jìn)行服務(wù)拆分,其實(shí)也沒那個(gè)必要,如果有些模塊用的人比較多,那么我們可以采用讀寫分離來減輕壓力,這樣的話, 可以在一定程度上提升系統(tǒng)的用戶體驗(yàn),不過這只是在數(shù)據(jù)庫的I/O上面做方案,如果系統(tǒng)的壓力很大,那么肯定要做負(fù)載均衡,我們今天就先說 實(shí)現(xiàn)數(shù)據(jù)庫的讀寫分離。我們要在代碼層面實(shí)現(xiàn)數(shù)據(jù)庫的讀寫分離,那么核心就是數(shù)據(jù)源的切換,本文基于AOP來實(shí)現(xiàn)數(shù)據(jù)源的切換。
工程結(jié)構(gòu)
└─com
└─steak
└─transaction
│ TransactionDemoApplication.java
│
├─datasource
│ │ DatasourceChooser.java
│ │ DatasourceConfiguration.java
│ │ DatasourceContext.java
│ │ DatasourceScope.java
│ │ DynamicDatasourceAspect.java
│ │
│ └─properties
│ DynamicDatasourceProperties.java
│ MainDatasourceProperties.java
│
├─execute
│ PlaceOrderExecute.java
│
├─rest
│ PlaceOrderApi.java
│
├─result
│ R.java
│
└─service
IntegralService.java
OrderService.java
StockService.java在下面的實(shí)現(xiàn)中,我們一個(gè)有三個(gè)數(shù)據(jù)源,其中有一個(gè)是默認(rèn)的,如果不指定具體的數(shù)據(jù)源,那么就使用默認(rèn)的,我們是基于申明式的方式來切換 數(shù)據(jù)源,只需要在具體的接口上加上注解便能實(shí)現(xiàn)數(shù)據(jù)源的切換。
編碼實(shí)現(xiàn)
yml文件
主數(shù)據(jù)源直接使用spring的配置,其他數(shù)據(jù)源采用自定義的方式,這里采用一個(gè)map結(jié)構(gòu)來定義,方便進(jìn)行解析,可以在yml文件里進(jìn)行添加多個(gè),在代碼 邏輯層面不用改動(dòng)。
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
username: root
password: 123456
url: jdbc:mysql://127.0.0.1:3306/db?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
dynamic:
datasource: {
slave1: {
username: 'root',
password: '123456',
url: ' url: jdbc:mysql://127.0.0.1:3306/db?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC',
driver-class-name: 'com.mysql.cj.jdbc.Driver'
},
slave2: {
username: 'root',
password: '123456',
url: ' url: jdbc:mysql://127.0.0.1:3306/db?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC',
driver-class-name: 'com.mysql.cj.jdbc.Driver'
}
}主數(shù)據(jù)源MainDatasourceProperties
對(duì)于主數(shù)據(jù)源,我們單獨(dú)拿出來放在一個(gè)類里面,不過其實(shí)也可以放到dynamic里面,只是需要做一定的處理,我們就簡(jiǎn)單的放幾個(gè)連接屬性。
/**
* @author 劉牌
* @date 2022/3/220:14
*/
@Component
@ConfigurationProperties(prefix = "spring.datasource.druid")
@Data
public class MainDatasourceProperties {
private String username;
private String password;
private String url;
private String driverClassName;
}其他數(shù)據(jù)源DynamicDatasourceProperties
其他數(shù)據(jù)源使用一個(gè)Map來接受yml文件中的數(shù)據(jù)源配置
/**
* @author 劉牌
* @date 2022/3/213:47
*/
@Component
@ConfigurationProperties(prefix = "dynamic")
@Data
public class DynamicDatasourceProperties {
private Map<String,Map<String,String>> datasource;
}數(shù)據(jù)源配置類DatasourceConfiguration
配置類中主要對(duì)DataSource進(jìn)行配置,主數(shù)據(jù)源我們按照正常的bean來定義連接屬性,而其他數(shù)據(jù)數(shù)據(jù)源則使用反射的方式來進(jìn)行連接屬性的 配置,因?yàn)橹鲾?shù)據(jù)源一般是不會(huì)變動(dòng)的,但是其他數(shù)據(jù)源可能會(huì)發(fā)生變動(dòng),可能會(huì)添加,這時(shí)候如果通過硬編碼取配置,那么每增加一個(gè)數(shù)據(jù)源,就需要 增加一個(gè)配置,顯然不太行,所以就使用反射來進(jìn)行賦值。
/**
* @author 劉牌
* @date 2022/3/111:34
*/
@Configuration
@AllArgsConstructor
public class DatasourceConfiguration {
final MainDatasourceProperties mainDatasourceProperties;
final DynamicDatasourceProperties dynamicDatasourceProperties;
@Bean
public DataSource datasource(){
Map<Object,Object> datasourceMap = new HashMap<>();
DatasourceChooser datasourceChooser = new DatasourceChooser();
/**
* main database
*/
DruidDataSource mainDataSource = new DruidDataSource();
mainDataSource.setUsername(mainDatasourceProperties.getUsername());
mainDataSource.setPassword(mainDatasourceProperties.getPassword());
mainDataSource.setUrl(mainDatasourceProperties.getUrl());
mainDataSource.setDriverClassName(mainDatasourceProperties.getDriverClassName());
datasourceMap.put("main",mainDataSource);
/**
* other database
*/
Map<String, Map<String, String>> sourceMap = dynamicDatasourceProperties.getDatasource();
sourceMap.forEach((datasourceName,datasourceMaps) -> {
DruidDataSource dataSource = new DruidDataSource();
datasourceMaps.forEach((K,V) -> {
String setField = "set" + K.substring(0, 1).toUpperCase() + K.substring(1);
//轉(zhuǎn)換yml文件中帶有-符號(hào)的屬性
String[] strings = setField.split("");
StringBuilder newStr = new StringBuilder();
for (int i = 0; i < strings.length; i++) {
if (strings[i].equals("-")) strings[i + 1] = strings[i + 1].toUpperCase();
if (!strings[i].equals("-")) newStr.append(strings[i]);
}
try {
DruidDataSource.class.getMethod(newStr.toString(),String.class).invoke(dataSource,V);
} catch (Exception e) {
e.printStackTrace();
}
});
datasourceMap.put(datasourceName,dataSource);
});
//設(shè)置目標(biāo)數(shù)據(jù)源
datasourceChooser.setTargetDataSources(datasourceMap);
//設(shè)置默認(rèn)數(shù)據(jù)源
datasourceChooser.setDefaultTargetDataSource(mainDataSource);
return datasourceChooser;
}
}上面使用數(shù)據(jù)源配置類中使用反射對(duì)其他數(shù)據(jù)源進(jìn)行連接屬性的設(shè)置,然后設(shè)置目標(biāo)數(shù)據(jù)源和默認(rèn)數(shù)據(jù)源,里面有一個(gè)DatasourceChooser。
DatasourceChooser
DatasourceChooser繼承自AbstractRoutingDataSource,AbstractRoutingDataSource可以實(shí)現(xiàn)數(shù)據(jù)源的切換,它里面的 determineCurrentLookupKey()方法需要我們返回一個(gè)數(shù)據(jù)源的名稱,它會(huì)自動(dòng)給我們匹配上數(shù)據(jù)源。
/**
* @author 劉牌
* @date 2022/3/112:21
*/
public class DatasourceChooser extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DatasourceContext.getDatasource();
}
}如下是AbstractRoutingDataSource的部分源碼,我們可以看出數(shù)據(jù)源是一個(gè)Map結(jié)構(gòu),可以通過數(shù)據(jù)源名稱查找到對(duì)應(yīng)的數(shù)據(jù)源。
package org.springframework.jdbc.datasource.lookup;
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
@Nullable
private Map<Object, DataSource> resolvedDataSources;
public AbstractRoutingDataSource() {
}
protected DataSource determineTargetDataSource() {
Object lookupKey = this.determineCurrentLookupKey();
DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
}
@Nullable
protected abstract Object determineCurrentLookupKey();
}DatasourceContext
DatasourceContext內(nèi)部是一個(gè)ThreadLocal,主要是用來存儲(chǔ)每一個(gè)線程的數(shù)據(jù)源名稱和獲取數(shù)據(jù)源名稱,而數(shù)據(jù)源的名稱我們用過AOP切面 來獲取。
/**
* @author 劉牌
* @date 2022/3/112:22
*/
public class DatasourceContext {
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void setDatasource(String key){
threadLocal.set(key);
}
public static String getDatasource(){
return threadLocal.get();
}
}數(shù)據(jù)源注解DatasourceScope
DatasourceScope標(biāo)準(zhǔn)在方法上面,通過scope來指定數(shù)據(jù)源,不指定默認(rèn)為主數(shù)據(jù)源main。
/**
* @author 劉牌
* @date 2022/3/111:22
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DatasourceScope {
String scope() default "main";
}數(shù)據(jù)源切面DynamicDatasourceAspect
我們?cè)谠L問每一個(gè)帶有DatasourceScope注解的方法時(shí),都會(huì)經(jīng)過數(shù)據(jù)源切面DynamicDatasourceAspect,獲取到注解上面的 scope的值后,通過DatasourceContext設(shè)置數(shù)據(jù)源名稱,便可實(shí)現(xiàn)對(duì)數(shù)據(jù)源的切換。
/**
* @author 劉牌
* @date 2022/3/111:34
*/
@Aspect
@Component
public class DynamicDatasourceAspect {
@Pointcut("@annotation(dataSourceScope)")
public void dynamicPointcut(DatasourceScope dataSourceScope){}
@Around(value = "dynamicPointcut(dataSourceScope)", argNames = "joinPoint,dataSourceScope")
public Object dynamicAround(ProceedingJoinPoint joinPoint , DatasourceScope dataSourceScope) throws Throwable {
String scope = dataSourceScope.scope();
DatasourceContext.setDatasource(scope);
return joinPoint.proceed();
}
}使用
只需要在具體的方法上面標(biāo)注數(shù)據(jù)源注解@DatasourceScope,并指定scope的值,便可實(shí)現(xiàn)切換,如果不指定,那么就使用主數(shù)據(jù)源。
/**
* @author 劉牌
* @date 2022/3/19:49
*/
@Service
@AllArgsConstructor
public class OrderService {
private JdbcTemplate jdbcTemplate;
@DatasourceScope(scope = "slave1")
public R saveOrder(Integer userId , Integer commodityId){
String sql = "INSERT INTO `order`(user_id,commodity_id) VALUES("+userId+","+commodityId+")";
jdbcTemplate.execute(sql);
return R.builder().code(200).msg("save order success").build();
}
}到此這篇關(guān)于SpringBoot實(shí)現(xiàn)多數(shù)據(jù)源的切換實(shí)踐的文章就介紹到這了,更多相關(guān)SpringBoot多數(shù)據(jù)源切換內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- springboot dynamic多數(shù)據(jù)源demo以及常見切換、事務(wù)的問題
- Springboot實(shí)現(xiàn)多數(shù)據(jù)源切換詳情
- SpringBoot多數(shù)據(jù)源配置并通過注解實(shí)現(xiàn)動(dòng)態(tài)切換數(shù)據(jù)源
- SpringBoot基于AbstractRoutingDataSource實(shí)現(xiàn)多數(shù)據(jù)源動(dòng)態(tài)切換
- SpringBoot多數(shù)據(jù)源切換實(shí)現(xiàn)代碼(Mybaitis)
- SpringBoot?+DynamicDataSource切換多數(shù)據(jù)源的全過程
- springboot中mybatis多數(shù)據(jù)源動(dòng)態(tài)切換實(shí)現(xiàn)
- Springboot如何設(shè)置多數(shù)據(jù)源,隨時(shí)切換
相關(guān)文章
java 中模擬UDP傳輸?shù)陌l(fā)送端和接收端實(shí)例詳解
這篇文章主要介紹了java 中模擬UDP傳輸?shù)陌l(fā)送端和接收端實(shí)例詳解的相關(guān)資料,需要的朋友可以參考下2017-03-03
SpringBoot 集成Kaptcha實(shí)現(xiàn)驗(yàn)證碼功能實(shí)例詳解
在一個(gè)web應(yīng)用中驗(yàn)證碼是一個(gè)常見的元素。今天給大家介紹一下kaptcha的和springboot一起使用的簡(jiǎn)單例子。感興趣的朋友參考下吧2017-08-08
SpringBoot項(xiàng)目集成Flyway進(jìn)行數(shù)據(jù)庫版本控制的詳細(xì)教程
這篇文章主要介紹了SpringBoot項(xiàng)目集成Flyway進(jìn)行數(shù)據(jù)庫版本控制,本文分步驟通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-07-07
淺談java socket的正確關(guān)閉姿勢(shì)
這篇文章主要介紹了java socket的正確關(guān)閉姿勢(shì),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06
解決java idea新建子目錄時(shí)命名不是樹形結(jié)構(gòu)的問題
這篇文章主要介紹了解決java idea新建子目錄時(shí)命名不是樹形結(jié)構(gòu)的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-08-08
詳解Java多態(tài)對(duì)象的類型轉(zhuǎn)換與動(dòng)態(tài)綁定
這篇文章主要介紹了詳解Java多態(tài)對(duì)象的類型轉(zhuǎn)換與動(dòng)態(tài)綁定,是Java入門學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下2015-09-09

