SpringBoot實現(xiàn)多數(shù)據(jù)源的實戰(zhàn)案例
1.前言
大家好,今天給大家?guī)硪黄P于SpringBoot實現(xiàn)多數(shù)據(jù)源的實戰(zhàn)案例。好了,話不多說讓我們開始吧.
2.概述
在實際開發(fā)中,我們往往面臨一個應用需要訪問多個數(shù)據(jù)庫的情況。例如下面兩種場景。
- 業(yè)務復雜: 數(shù)據(jù)分布在不同的數(shù)據(jù)庫,數(shù)據(jù)庫拆了,應用沒拆,一個公司有多個子項目,各用各的數(shù)據(jù)庫。
- 讀寫分離: 為了解決數(shù)據(jù)庫的讀性能瓶頸(讀比寫性能更高,寫鎖會影響讀阻塞,從而影響讀的性能)
- 很多數(shù)據(jù)庫擁有主從架構,也就是說,一臺 主數(shù)據(jù)庫服務器,是對外提供增刪改查業(yè)務的生產(chǎn)服務器;
- 另一臺從數(shù)據(jù)庫服務器,主要進行讀的操作。
- 讀寫分離:解決高并發(fā)下讀寫受影響。數(shù)據(jù)更新在主庫上進行,主庫將數(shù)據(jù)變更信息同步給從庫。在查詢時,在從庫上進行,從而分擔主庫的壓力。
我們可以在代碼層面解決這種動態(tài)數(shù)據(jù)源切換的問題,而不需要使用 mycat、shardingJDBC 等其他中間件。本文將主要以自定義注解 + 繼承 AbstractRoutingDataSource
實現(xiàn)讀寫分離。
3.如何實現(xiàn)多數(shù)據(jù)源
在 SpringBoot 項目中實現(xiàn)讀寫分離通常需要以下幾步:
- 配置數(shù)據(jù)源:你需要為讀操作和寫操作分別配置一個數(shù)據(jù)源。
- 創(chuàng)建數(shù)據(jù)源路由邏輯:這通常通過擴展 Spring 的
AbstractRoutingDataSource
來實現(xiàn)。它允許你根據(jù)一定的邏輯來決定使用哪個數(shù)據(jù)源(讀或?qū)懀?/li> - 配置事務管理器:這使得你能夠在使用不同數(shù)據(jù)源時保持事務的一致性。
- 服務層或DAO層設計:確保在執(zhí)行讀操作時使用讀數(shù)據(jù)源,在執(zhí)行寫操作時使用寫數(shù)據(jù)源。
- 自定義切面,在切面中解析 @DataSource 注解。當一個方法或者類上面,有 @DataSource 注解的時候,將 @DataSource 注解所標記的數(shù)據(jù)源列出來存入到 ThreadLocal 中。
注意:這里使用ThreadLocal的原因是為了保證我們的線程安全。
4.案例實現(xiàn)
接下來我們就按照以上步驟進行編碼實現(xiàn)。
4.1 創(chuàng)建新模塊
首先我們創(chuàng)建一個新的模塊命名為:springboot-dynamic-source
1.導入依賴
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> </dependency> <!--mybatis plus--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> </dependency> </dependencies>
2.創(chuàng)建yml配置文件
server: port: 8007 spring: application: name: dynamic-source jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 datasource: type: com.alibaba.druid.pool.DruidDataSource driverClassName: com.mysql.cj.jdbc.Driver ds: # 主庫數(shù)據(jù)源 master: url: jdbc:mysql://localhost:3307/test01?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: root password: root # 從庫數(shù)據(jù)源 slave: url: jdbc:mysql://localhost:3307/test02?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: root password: root # 初始連接數(shù) initialSize: 5 # 最小連接池數(shù)量 minIdle: 10 # 最大連接池數(shù)量 maxActive: 20 # 配置獲取連接等待超時的時間 maxWait: 60000 # 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接,單位是毫秒 timeBetweenEvictionRunsMillis: 60000 # 配置一個連接在池中最小生存的時間,單位是毫秒 minEvictableIdleTimeMillis: 300000 # 配置一個連接在池中最大生存的時間,單位是毫秒 maxEvictableIdleTimeMillis: 900000 # 配置檢測連接是否有效 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false webStatFilter: enabled: true statViewServlet: enabled: true # 設置白名單,不填則允許所有訪問 allow: url-pattern: /druid/* # 控制臺管理用戶名和密碼 login-username: admin login-password: 123456 filter: stat: enabled: true # 慢SQL記錄 log-slow-sql: true slow-sql-millis: 1000 merge-sql: true wall: config: multi-statement-allow: true logging: level: org.javatop: debug pattern: dateformat: HH:mm:ss:SSS file: path: "logs/${spring.application.name}"
ds 中是我們的所有數(shù)據(jù)源。master 是默認的數(shù)據(jù)源,不可修改,其他的數(shù)據(jù)源可以修改并添加多個。
3.準備數(shù)據(jù)庫
我這里需要提前準備兩個數(shù)據(jù)庫,一個是主數(shù)據(jù)庫master,一個是從數(shù)據(jù)庫slave。
我們會后面會通過一個自定義注解去實現(xiàn)動態(tài)切換數(shù)據(jù)庫。
這里給出我們創(chuàng)建的一個user表的SQL語句。
CREATE TABLE `user` ( `id` int NOT NULL AUTO_INCREMENT, `username` varchar(50) DEFAULT NULL, `age` int DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=101 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
4.2 加載數(shù)據(jù)源
我們可以通過@ConfigurationProperties 注解加載定義的配置文件。spring.datasource 對應的注解都會匹配到。
package org.javatop.dynamic.config; import com.alibaba.druid.pool.DruidDataSource; import org.springframework.boot.context.properties.ConfigurationProperties; import javax.sql.DataSource; import java.util.Map; /** * @author : Leo * @version 1.0 * @date 2024-01-02 15:10 * @description : */ @ConfigurationProperties(prefix = "spring.datasource") public class DruidProperties { private String type; private String driverClassName; private Map<String, Map<String,String>> ds; private Integer initialSize; private Integer minIdle; private Integer maxActive; private Integer maxWait; /** *一會在外部構建好一個 DruidDataSource 對象,包含三個核心屬性 url、username、password * 在這個方法中設置公共屬性 * @param druidDataSource * @return */ public DataSource dataSource(DruidDataSource druidDataSource){ druidDataSource.setInitialSize(initialSize); druidDataSource.setMinIdle(minIdle); druidDataSource.setMaxActive(maxActive); druidDataSource.setMaxWait(maxWait); return druidDataSource; } public String getType() { return type; } public void setType(String type) { this.type = type; } public String getDriverClassName() { return driverClassName; } public void setDriverClassName(String driverClassName) { this.driverClassName = driverClassName; } public Map<String, Map<String, String>> getDs() { return ds; } public void setDs(Map<String, Map<String, String>> ds) { this.ds = ds; } public Integer getInitialSize() { return initialSize; } public void setInitialSize(Integer initialSize) { this.initialSize = initialSize; } public Integer getMinIdle() { return minIdle; } public void setMinIdle(Integer minIdle) { this.minIdle = minIdle; } public Integer getMaxActive() { return maxActive; } public void setMaxActive(Integer maxActive) { this.maxActive = maxActive; } public Integer getMaxWait() { return maxWait; } public void setMaxWait(Integer maxWait) { this.maxWait = maxWait; } }
然后我們開始通過進行加載DruidProperties
來加載數(shù)據(jù)源。
@EnableConfigurationProperties :這個注解的意思是使 ConfigurationProperties 注解生效。
package org.javatop.dynamic.config; import com.alibaba.druid.pool.DruidDataSource; import com.alibaba.druid.pool.DruidDataSourceFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.stereotype.Component; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; import java.util.Set; /** * @author : Leo * @version 1.0 * @date 2024-01-02 15:12 * @description : 加載數(shù)據(jù)源 */ @Component @EnableConfigurationProperties(DruidProperties.class) public class LoadDataSource { @Autowired DruidProperties druidProperties; public Map<String, DataSource> loadAllDataSource() { Map<String, DataSource> map =new HashMap<>(); Map<String, Map<String, String>> ds = druidProperties.getDs(); try { Set<String> keySet = ds.keySet(); for (String key : keySet) { map.put(key, druidProperties.dataSource((DruidDataSource) DruidDataSourceFactory.createDataSource(ds.get(key)))); } } catch (Exception e) { e.printStackTrace(); } return map; } }
loadAllDataSource() 方法可以通過讀取application.yml配置文件中所有數(shù)據(jù)源對象。(我們這里有一個master主數(shù)據(jù)庫,和一個slave從數(shù)據(jù)庫)
druidProperties.dataSource(DruidDataSource druidDataSource) 這個方法為每個數(shù)據(jù)源配置其他額外的屬性(最大連接池等信息)。
DruidDataSourceFactory.createDataSource(ds.get(key):創(chuàng)建一個數(shù)據(jù)源,賦予三個核心的屬性。(username、url、password)
最終,所有的數(shù)據(jù)源都會存入map中。
4.3 自定義ThreadLocal工具類
我們這里定義一個簡單的ThreadLocal工具類
package org.javatop.dynamic.utils; /** * @author : Leo * @version 1.0 * @date 2024-01-02 15:13 * @description : ThreadLocal工具類 */ public class DynamicDataSourceUtil { private static final ThreadLocal<String> CONTEXT_HOLDER =new ThreadLocal<>(); public static void setDataSourceType(String dsType){ CONTEXT_HOLDER.set(dsType); } public static String getDataSourceType(){ return CONTEXT_HOLDER.get(); } public static void clear(){ CONTEXT_HOLDER.remove(); } }
4.4 自定義注解
首先需要通過一個枚舉類來設定一下我們的默認數(shù)據(jù)源,也是是master主數(shù)據(jù)庫。
package org.javatop.dynamic.constant; /** * @author : Leo * @version 1.0 * @date 2024-01-02 15:13 * @description : */ public interface DataSourceType { String default_ds_name ="master"; }
然后自定義一個注解,后面也就是通過這個注解來動態(tài)的配置切換我們的數(shù)據(jù)源,這里就也叫Datasource吧。
package org.javatop.dynamic.annotation; import org.javatop.dynamic.constant.DataSourceType; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @author : Leo * @version 1.0 * @date 2024-01-02 15:14 * @description : 這個注解將來可以加在某一個 service 類上或者方法上,通過 value 屬性來指定類或者方法應該使用哪個數(shù)據(jù)源 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) public @interface DataSource{ /** * 如果一個方法上加了 @DataSource 注解,但是卻沒有指定數(shù)據(jù)源的名稱,那么默認使用 Master 數(shù)據(jù)源 * @return */ String value() default DataSourceType.default_ds_name; }
4.5 AOP解析自定義注解
package org.javatop.dynamic.annotation; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.javatop.dynamic.utils.DynamicDataSourceUtil; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.stereotype.Component; /** * @author : Leo * @version 1.0 * @date 2024-01-02 15:15 * @description : AOP解析自定義注解 */ @Component @Aspect public class DataSourceAspect { /** * @annotation(org.javatop.dynamic.annotation.DataSource) 表示方法上有 @DataSource 注解 就將方法攔截下來。 * @within :如果類上面有 @DataSource 注解,就將類中的方法攔截下來。 */ @Pointcut("@annotation(org.javatop.dynamic.annotation.DataSource) || " + "@within(org.javatop.dynamic.annotation.DataSource)") public void pc(){ } @Around("pc()") public Object around(ProceedingJoinPoint point){ //獲取方法上面的注解 DataSource dataSource =getDataSource(point); if(dataSource!=null){ // 注解中數(shù)據(jù)源的名稱 String value = dataSource.value(); DynamicDataSourceUtil.setDataSourceType(value); } try { return point.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); }finally { DynamicDataSourceUtil.clear(); } return null; } private DataSource getDataSource(ProceedingJoinPoint point) { /** * 先去查找方法上的注解,如果沒有,再去類中找。 */ MethodSignature signature = (MethodSignature)point.getSignature(); DataSource annotation = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class); if(annotation!=null){ return annotation; } return AnnotationUtils.findAnnotation(signature.getDeclaringType(),DataSource.class); } }
@Pointcut 定義
@Pointcut("@annotation(org.javatop.dynamic.annotation.DataSource) || " + "@within(org.javatop.dynamic.annotation.DataSource)") public void pc() { }
@Pointcut
是一個定義在方法上的注解,用來指定一個切點(即在何處進行攔截)。"@annotation(org.javatop.dynamic.annotation.DataSource)"
表示攔截所有被@DataSource
注解標記的方法。"@within(org.javatop.dynamic.annotation.DataSource)"
表示攔截所有在類級別被@DataSource
注解標記的類中的方法。pc()
方法本身是空的,因為所有的邏輯都將在與這個切點相關的通知(advice)中定義。
@Around 通知
e@Around("pc()") public Object around(ProceedingJoinPoint point) { DataSource dataSource = getDataSource(point); if (dataSource != null) { String value = dataSource.value(); DynamicDataSourceUtil.setDataSourceType(value); } try { return point.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); } finally { DynamicDataSourceUtil.clear(); } return null; }
@Around("pc()")
表示這是一個環(huán)繞通知,它會在pc()
方法所定義的切點前后執(zhí)行。ProceedingJoinPoint point
是連接點的信息,它包含了方法的所有相關信息,如方法名、參數(shù)等。getDataSource(point)
用來獲取方法或類上的@DataSource
注解。- 如果存在
@DataSource
注解,它會從注解中獲取數(shù)據(jù)源的名稱,并通過DynamicDataSourceUtil.setDataSourceType(value)
設置當前線程的數(shù)據(jù)源。 point.proceed()
是調(diào)用原始方法的地方。finally
塊中的DynamicDataSourceUtil.clear()
用于在方法執(zhí)行完畢后清理數(shù)據(jù)源設置,確保不會影響其他的數(shù)據(jù)庫操作。
最后獲取@DataSource注解
4.6 自定義動態(tài)數(shù)據(jù)源
package org.javatop.dynamic.config; import org.javatop.dynamic.constant.DataSourceType; import org.javatop.dynamic.utils.DynamicDataSourceUtil; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import org.springframework.stereotype.Component; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; /** * @author : Leo * @version 1.0 * @date 2024-01-02 15:18 * @description : 定義動態(tài)數(shù)據(jù)源 */ @Component public class DynamicDataSource extends AbstractRoutingDataSource { public DynamicDataSource(LoadDataSource loadDataSource) { // 1、設置所有的數(shù)據(jù)源 Map<String, DataSource> stringDataSourceMap = loadDataSource.loadAllDataSource(); super.setTargetDataSources(new HashMap<>(stringDataSourceMap)); // 2、設置默認的數(shù)據(jù)源 super.setDefaultTargetDataSource(stringDataSourceMap.get(DataSourceType.default_ds_name)); super.afterPropertiesSet(); } /** * 這個方法用來返回數(shù)據(jù)源名稱,當系統(tǒng)需要獲取數(shù)據(jù)源的時候,會自動調(diào)用該方法獲取數(shù)據(jù)源的名稱 * @return */ @Override protected Object determineCurrentLookupKey() { return DynamicDataSourceUtil.getDataSourceType(); } }
DynamicDataSource
類擴展自AbstractRoutingDataSource
類,這是Spring框架提供的一個抽象類,用于實現(xiàn)數(shù)據(jù)源的動態(tài)路由。- 構造函數(shù)
public DynamicDataSource(LoadDataSource loadDataSource)
接收一個LoadDataSource
類型的參數(shù)。這個參數(shù)被用于加載所有的數(shù)據(jù)源配置。Map<String, DataSource> stringDataSourceMap = loadDataSource.loadAllDataSource();
這行代碼調(diào)用了loadDataSource
的loadAllDataSource
方法來加載所有數(shù)據(jù)源配置,并將其存儲在一個名為stringDataSourceMap
的Map中,其中鍵是數(shù)據(jù)源的名稱,值是對應的DataSource
對象。super.setTargetDataSources(new HashMap<>(stringDataSourceMap));
這行代碼設置了目標數(shù)據(jù)源。它將前面加載的所有數(shù)據(jù)源stringDataSourceMap
設置為目標數(shù)據(jù)源。super.setDefaultTargetDataSource(stringDataSourceMap.get(DataSourceType.default_ds_name));
這行代碼設置了默認的數(shù)據(jù)源。它通過DataSourceType.default_ds_name
從stringDataSourceMap
中獲取默認的數(shù)據(jù)源,并設置為默認數(shù)據(jù)源。super.afterPropertiesSet();
是一個初始化方法,確保所有屬性都被正確設置。
determineCurrentLookupKey()
方法是AbstractRoutingDataSource
的一個抽象方法,必須要實現(xiàn)。這個方法用于決定使用哪個數(shù)據(jù)源,通常情況下是根據(jù)某種條件動態(tài)返回數(shù)據(jù)源名稱。return DynamicDataSourceUtil.getDataSourceType();
這行代碼返回當前線程所使用的數(shù)據(jù)源的名稱。DynamicDataSourceUtil
是一個工具類,可能提供了線程局部變量(ThreadLocal)來存儲每個線程所選擇的數(shù)據(jù)源名稱。
這樣,當應用程序需要與數(shù)據(jù)庫進行交互時,就會通過 DynamicDataSource
獲取到當前線程所指定的數(shù)據(jù)源,并進行相應的數(shù)據(jù)庫操作。這種方式能夠在不同業(yè)務場景中靈活切換數(shù)據(jù)源,非常適合多租戶、讀寫分離等復雜的數(shù)據(jù)庫應用場景。
4.7 編寫業(yè)務層
我們編寫一個service層
package org.javatop.dynamic.service; import org.javatop.dynamic.annotation.DataSource; import org.javatop.dynamic.domain.User; import org.javatop.dynamic.mapper.UserMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; /** * @author : Leo * @version 1.0 * @date 2024-01-02 15:26 * @description : */ @Service public class UserService{ @Autowired private UserMapper userMapper; @DataSource("slave") // @DataSource public List<User> getAll(){ List<User> all = userMapper.getAll(); return all; } }
我們在getAll()方法上加上@DataSource(“slave”),并指定slave從數(shù)據(jù)庫。
然后再編寫一個mapper,去操作數(shù)據(jù)庫。
package org.javatop.dynamic.mapper; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; import org.javatop.dynamic.domain.User; import java.util.List; /** * @author : Leo * @version 1.0 * @date 2024-01-02 15:26 * @description : */ @Mapper public interface UserMapper { @Select("select * from user") List<User> getAll(); }
4.8 測試
package org.javatop.dynamic; import org.javatop.dynamic.domain.User; import org.javatop.dynamic.service.UserService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.util.List; /** * @author : Leo * @version 1.0 * @date 2024-01-02 15:32 * @description : */ @SpringBootTest public class DynamicTest { @Autowired private UserService userService; /** * 用于測試: */ @Test public void test() { List<User> all = userService.getAll(); if(all !=null){ for (User user : all) { System.out.println(user); } } } }
我們查看控制臺。
可以看出來我們?nèi)ゲ樵兊氖莟est02庫中的user數(shù)據(jù)。
大功告成!!!
5.總結(jié)
以上就是SpringBoot實現(xiàn)多數(shù)據(jù)源的實戰(zhàn)案例的詳細內(nèi)容,更多關于SpringBoot實現(xiàn)多數(shù)據(jù)源的資料請關注腳本之家其它相關文章!
相關文章
springboot中不能獲取post請求參數(shù)的解決方法
這篇文章主要介紹了springboot中不能獲取post請求參數(shù)的解決方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-06-06java 使用idea將工程打成jar并創(chuàng)建成exe文件類型執(zhí)行的方法詳解
這篇文章主要介紹了java 使用idea將工程打成jar并創(chuàng)建成exe文件類型執(zhí)行,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友參考下吧2020-09-09SpringBoot使用mybatis-plus分頁查詢無效的問題解決
MyBatis-Plus提供了很多便捷的功能,包括分頁查詢,本文主要介紹了SpringBoot使用mybatis-plus分頁查詢無效的問題解決,具有一定的參考價值,感興趣的可以了解一下2023-12-12Java動態(tài)代理實現(xiàn)_動力節(jié)點Java學院整理
動態(tài)代理作為代理模式的一種擴展形式,廣泛應用于框架(尤其是基于AOP的框架)的設計與開發(fā),本文將通過實例來講解Java動態(tài)代理的實現(xiàn)過程2017-08-08Java中四種訪問控制權限解析(private、default、protected、public)
java當中有4種訪問修飾限定符privat、default(默認訪問權限),protected以及public,本文就詳細的介紹一下這四種方法的具體使用,感興趣的可以了解一下2023-05-05