SpringBoot項目中如何動態(tài)切換數(shù)據(jù)源、數(shù)據(jù)庫
前言
本文參考若依源碼,介紹了如何在SpringBoot項目中使用AOP和自定義注解實現(xiàn)MySQL主從數(shù)據(jù)庫的動態(tài)切換,當(dāng)從庫故障時,能自動切換到主庫,確保服務(wù)的高可用性。
實現(xiàn)效果:如果 服務(wù)器搭建的是一主多從多個mysql數(shù)據(jù)源,主服務(wù)器用來讀。從服務(wù)器用來寫。此時你在代碼層面用注解指定了一個增刪改方法到從數(shù)據(jù)源,但是碰巧此時從數(shù)據(jù)源失效了,那么就會自動的切換到其它服務(wù)器。
為什么要切換數(shù)據(jù)源,有哪些應(yīng)用場景?
動態(tài)切換數(shù)據(jù)源通常是為了滿足以下需求:
- 讀寫分離:在數(shù)據(jù)庫架構(gòu)中,為了提高性能和可用性,常常使用主從復(fù)制的方式。主數(shù)據(jù)庫處理寫操作,而從數(shù)據(jù)庫處理讀操作。動態(tài)切換數(shù)據(jù)源可以在不同的操作中使用不同的數(shù)據(jù)庫,以達到優(yōu)化性能的目的。
- 多租戶架構(gòu):在SaaS(Software as a Service)應(yīng)用中,不同的租戶可能需要操作不同的數(shù)據(jù)庫。動態(tài)數(shù)據(jù)源允許系統(tǒng)根據(jù)租戶的身份來切換到對應(yīng)的數(shù)據(jù)源。
- 分庫分表:在處理大規(guī)模數(shù)據(jù)時,可能會采用分庫分表的策略來分散數(shù)據(jù)存儲的壓力。動態(tài)切換數(shù)據(jù)源可以在執(zhí)行跨庫或跨表操作時,根據(jù)需要切換到正確的數(shù)據(jù)源。
- 環(huán)境隔離:在開發(fā)、測試和生產(chǎn)環(huán)境中,可能需要連接到不同的數(shù)據(jù)庫。動態(tài)數(shù)據(jù)源可以在不同環(huán)境之間無縫切換,以確保數(shù)據(jù)的隔離和安全性。
- 靈活的數(shù)據(jù)庫管理:在復(fù)雜的業(yè)務(wù)場景下,可能需要根據(jù)不同的業(yè)務(wù)邏輯來選擇不同的數(shù)據(jù)源。動態(tài)數(shù)據(jù)源提供了這種靈活性,允許開發(fā)者根據(jù)運行時的條件來選擇最合適的數(shù)據(jù)源。
- 故障轉(zhuǎn)移和高可用性:當(dāng)主數(shù)據(jù)庫不可用時,動態(tài)切換數(shù)據(jù)源可以自動或手動切換到備用數(shù)據(jù)庫,以保證服務(wù)的連續(xù)性和數(shù)據(jù)的可用性。
如何切換數(shù)據(jù)源?
SpringBoot版本:3.0.4
jdk版本:JDK17
1.pom文件
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <!-- aop切面--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!--druid連接池--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.20</version> </dependency> <!--mysql驅(qū)動--> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> </dependency> <!--MybatisPlus--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency>
2.配置文件:application.yml、application-druid.yml
application.yml配置文件:
#application.yml server: port: 8000 spring: profiles: active: druid
application-druid.yml配置文件:
# 數(shù)據(jù)源配置 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driverClassName: com.mysql.cj.jdbc.Driver druid: # 主庫數(shù)據(jù)源 master: url: jdbc:mysql://localhost:3306/study?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: root password: 123456 # 從庫數(shù)據(jù)源 slave: # 從數(shù)據(jù)源開關(guān)/默認關(guān)閉 enabled: true url: jdbc:mysql://localhost:3306/t_lyj?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: root password: 123456 # 初始連接數(shù) initialSize: 5 # 最小連接池數(shù)量 minIdle: 10 # 最大連接池數(shù)量 maxActive: 20 # 配置獲取連接等待超時的時間 maxWait: 60000 # 配置連接超時時間 connectTimeout: 30000 # 配置網(wǎng)絡(luò)超時時間 socketTimeout: 60000 # 配置間隔多久才進行一次檢測,檢測需要關(guān)閉的空閑連接,單位是毫秒 timeBetweenEvictionRunsMillis: 60000 # 配置一個連接在池中最小生存的時間,單位是毫秒 minEvictableIdleTimeMillis: 300000 # 配置一個連接在池中最大生存的時間,單位是毫秒 maxEvictableIdleTimeMillis: 900000
3、數(shù)據(jù)源名稱枚舉DataSourceType
/** * 數(shù)據(jù)源 * * @author ruoyi */ public enum DataSourceType { /** * 主庫 */ MASTER, /** * 從庫 */ SLAVE }
4、Bean工具類SpringUtils
@Component public final class SpringUtils implements BeanFactoryPostProcessor, ApplicationContextAware { /** Spring應(yīng)用上下文環(huán)境 */ private static ConfigurableListableBeanFactory beanFactory; private static ApplicationContext applicationContext; @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { SpringUtils.beanFactory = beanFactory; } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { SpringUtils.applicationContext = applicationContext; } /** * 獲取對象 * * @param name * @return Object 一個以所給名字注冊的bean的實例 * @throws BeansException * */ @SuppressWarnings("unchecked") public static <T> T getBean(String name) throws BeansException { return (T) beanFactory.getBean(name); } /** * 獲取類型為requiredType的對象 * * @param clz * @return * @throws BeansException * */ public static <T> T getBean(Class<T> clz) throws BeansException { T result = (T) beanFactory.getBean(clz); return result; } /** * 如果BeanFactory包含一個與所給名稱匹配的bean定義,則返回true * * @param name * @return boolean */ public static boolean containsBean(String name) { return beanFactory.containsBean(name); } /** * 判斷以給定名字注冊的bean定義是一個singleton還是一個prototype。 如果與給定名字相應(yīng)的bean定義沒有被找到,將會拋出一個異常(NoSuchBeanDefinitionException) * * @param name * @return boolean * @throws NoSuchBeanDefinitionException * */ public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException { return beanFactory.isSingleton(name); } /** * @param name * @return Class 注冊對象的類型 * @throws NoSuchBeanDefinitionException * */ public static Class<?> getType(String name) throws NoSuchBeanDefinitionException { return beanFactory.getType(name); } /** * 如果給定的bean名字在bean定義中有別名,則返回這些別名 * * @param name * @return * @throws NoSuchBeanDefinitionException * */ public static String[] getAliases(String name) throws NoSuchBeanDefinitionException { return beanFactory.getAliases(name); } /** * 獲取aop代理對象 * * @param invoker * @return */ @SuppressWarnings("unchecked") public static <T> T getAopProxy(T invoker) { return (T) AopContext.currentProxy(); } /** * 獲取當(dāng)前的環(huán)境配置,無配置返回null * * @return 當(dāng)前的環(huán)境配置 */ public static String[] getActiveProfiles() { return applicationContext.getEnvironment().getActiveProfiles(); } /** * 獲取當(dāng)前的環(huán)境配置,當(dāng)有多個環(huán)境配置時,只獲取第一個 * * @return 當(dāng)前的環(huán)境配置 */ public static String getActiveProfile() { final String[] activeProfiles = getActiveProfiles(); return StringUtils.isNotEmpty(Arrays.toString(activeProfiles)) ? activeProfiles[0] : null; } /** * 獲取配置文件中的值 * * @param key 配置文件的key * @return 當(dāng)前的配置文件的值 * */ public static String getRequiredProperty(String key) { return applicationContext.getEnvironment().getRequiredProperty(key); } }
5、多數(shù)據(jù)源切換注解DataSource
/** * 自定義多數(shù)據(jù)源切換注解 * * 優(yōu)先級:先方法,后類,如果方法覆蓋了類上的數(shù)據(jù)源類型,以方法的為準,否則以類上的為準 * * @author lyj */ @Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface DataSource { /** * 切換數(shù)據(jù)源名稱 */ public DataSourceType value() default DataSourceType.MASTER; }
6、數(shù)據(jù)源解析配置類DruidConfig
@Configuration public class DruidConfig { @Bean @ConfigurationProperties("spring.datasource.druid.master") public DataSource masterDataSource(DruidProperties druidProperties){ DruidDataSource dataSource = DruidDataSourceBuilder.create().build(); return druidProperties.dataSource(dataSource); } @Bean @ConfigurationProperties("spring.datasource.druid.slave") @ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true") public DataSource slaveDataSource(DruidProperties druidProperties) { DruidDataSource dataSource = DruidDataSourceBuilder.create().build(); return druidProperties.dataSource(dataSource); } @Bean(name = "dynamicDataSource") @Primary public DynamicDataSource dataSource(DataSource masterDataSource) { Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource); setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource"); return new DynamicDataSource(masterDataSource, targetDataSources); } /** * 設(shè)置數(shù)據(jù)源 * * @param targetDataSources 備選數(shù)據(jù)源集合 * @param sourceName 數(shù)據(jù)源名稱 * @param beanName bean名稱 */ public void setDataSource(Map<Object, Object> targetDataSources, String sourceName, String beanName) { try { DataSource dataSource = SpringUtils.getBean(beanName); targetDataSources.put(sourceName, dataSource); } catch (Exception e) { } } }
7、數(shù)據(jù)源注入核心類DynamicDataSource
/** * 動態(tài)數(shù)據(jù)源 * * @author lyj */ public class DynamicDataSource extends AbstractRoutingDataSource { public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) { //設(shè)置默認數(shù)據(jù)源 super.setDefaultTargetDataSource(defaultTargetDataSource); //設(shè)置目標數(shù)據(jù)源的映射 super.setTargetDataSources(targetDataSources); //初始化 super.afterPropertiesSet(); } @Override protected Object determineCurrentLookupKey() { return DynamicDataSourceContextHolder.getDataSourceType(); } }
8、數(shù)據(jù)源切換處理類DynamicDataSourceContextHolder
/** * 數(shù)據(jù)源切換處理 * * @author lyj */ public class DynamicDataSourceContextHolder { public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class); /** * 使用ThreadLocal維護變量,ThreadLocal為每個使用該變量的線程提供獨立的變量副本, * 所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應(yīng)的副本。 */ private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>(); /** * 設(shè)置數(shù)據(jù)源的變量 */ public static void setDataSourceType(String dsType) { log.info("切換到{}數(shù)據(jù)源", dsType); CONTEXT_HOLDER.set(dsType); } /** * 獲得數(shù)據(jù)源的變量,默認使用主數(shù)據(jù)源 */ public static String getDataSourceType() { return CONTEXT_HOLDER.get() == null ? DataSourceType.MASTER.name() : CONTEXT_HOLDER.get(); } /** * 清空數(shù)據(jù)源變量 */ public static void clearDataSourceType() { CONTEXT_HOLDER.remove(); } }
9、Aop切面類
@Aspect @Order(1) @Component public class DataSourceAspect { @Pointcut("@annotation(com.LYJ.study.DynamicDataSource.annocation.DataSource)" + "|| @within(com.LYJ.study.DynamicDataSource.annocation.DataSource)") public void dsPointCut(){} @Around("dsPointCut()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable{ DataSource dataSource = getDataSource(joinPoint); if (dataSource != null){ DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name()); } try { return joinPoint.proceed(); } finally { // 銷毀數(shù)據(jù)源 在執(zhí)行方法之后 DynamicDataSourceContextHolder.clearDataSourceType(); } } /** * 獲取需要切換的數(shù)據(jù)源 */ public DataSource getDataSource(ProceedingJoinPoint point) { MethodSignature signature = (MethodSignature) point.getSignature(); com.LYJ.study.DynamicDataSource.annocation.DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), com.LYJ.study.DynamicDataSource.annocation.DataSource.class); if (Objects.nonNull(dataSource)) { return dataSource; } return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class); } }
10、在業(yè)務(wù)中使用
@Service @RequiredArgsConstructor @DataSource(value=DataSourceType.MASTER) //@DataSource(value=DataSourceType.SLAVE) public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { private final UserMapper userMapper; @Override @DataSource(value=DataSourceType.MASTER) //@DataSource(value=DataSourceType.SLAVE) public List<User> queryAll() { return userMapper.selectList(null); } }
我們在service、mapper的類和方法上使用都可以。
補充:有很多從數(shù)據(jù)源怎么辦?
我們上面已經(jīng)配置了一個從數(shù)據(jù)源了,接下來我們繼續(xù)配置多個從數(shù)據(jù)源
首先在application-druid.yml文件添加新的數(shù)據(jù)源
在枚舉添加數(shù)據(jù)源名稱
//如果配置多數(shù)據(jù)源,繼續(xù)添加即可 public enum DataSourceType { /** * 主庫 */ MASTER, /** * 從庫 */ SLAVE, /** * 從庫2 */ SLAVE2 }
如何切換數(shù)據(jù)庫?
我們就以O(shè)racle為例
<!--oracle驅(qū)動--> <dependency> <groupId>com.oracle</groupId> <artifactId>ojdbc6</artifactId> <version>11.2.0.3</version> </dependency>
在application-druid.yml添加
slave3: # 從數(shù)據(jù)源開關(guān)/默認關(guān)閉 enabled: true url: jdbc:oracle:thin:@127.0.0.1:1521:oracle username: root password: password
然后刪除指定的mysql驅(qū)動,默認會自動尋找驅(qū)動
添加數(shù)據(jù)源和用法參考上面即可,都是一樣的。
注意:在切換數(shù)據(jù)庫時,因為mysql跟Oracle的sql語法有差別,啟動時可能報錯。
到此這篇關(guān)于SpringBoot項目中如何動態(tài)切換數(shù)據(jù)源、數(shù)據(jù)庫的文章就介紹到這了,更多相關(guān)SpringBoot動態(tài)切換數(shù)據(jù)源內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- springboot中mybatis多數(shù)據(jù)源動態(tài)切換實現(xiàn)
- 詳細聊聊SpringBoot中動態(tài)切換數(shù)據(jù)源的方法
- SpringBoot 自定義+動態(tài)切換數(shù)據(jù)源教程
- Springboot動態(tài)切換數(shù)據(jù)源的具體實現(xiàn)與原理分析
- springboot+dynamicDataSource動態(tài)添加切換數(shù)據(jù)源方式
- springboot連接redis并動態(tài)切換database的實現(xiàn)方法
- SpringBoot多數(shù)據(jù)源配置并通過注解實現(xiàn)動態(tài)切換數(shù)據(jù)源
- springboot使用DynamicDataSource動態(tài)切換數(shù)據(jù)源的實現(xiàn)過程
- 使用SpringBoot動態(tài)切換數(shù)據(jù)源的實現(xiàn)方式
- SpringBoot實現(xiàn)動態(tài)數(shù)據(jù)源切換的方法總結(jié)
- SpringBoot實現(xiàn)動態(tài)端口切換黑魔法
相關(guān)文章
Springboot使用pdfbox提取PDF圖片的代碼示例
PDFBox是一個用于創(chuàng)建和處理PDF文檔的Java庫,它可以使用Java代碼創(chuàng)建、讀取、修改和提取PDF文檔中的內(nèi)容,本文就給大家介紹Springboot如何使用pdfbox提取PDF圖片,感興趣的同學(xué)可以借鑒參考2023-06-06Springboot中實現(xiàn)策略模式+工廠模式的方法
這篇文章主要介紹了Springboot中實現(xiàn)策略模式+工廠模式,具體策略模式和工廠模式的UML我就不給出來了,使用這個這兩個模式主要是防止程序中出現(xiàn)大量的IF ELSE IF ELSE....,接下來咱們直接實現(xiàn)Springboot策略模式工廠模式2022-03-03Java中遍歷數(shù)組使用foreach循環(huán)還是for循環(huán)?
這篇文章主要介紹了Java中遍歷數(shù)組使用foreach循環(huán)還是for循環(huán)?本文著重講解for語句的語法并給出使用實例,同時總結(jié)出盡量使用foreach語句遍歷數(shù)組,需要的朋友可以參考下2015-06-06Springboot如何統(tǒng)一處理Filter異常
這篇文章主要介紹了Springboot如何統(tǒng)一處理Filter異常問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-12-12