SpringBoot項目中如何動態(tài)切換數(shù)據(jù)源、數(shù)據(jù)庫
前言
本文參考若依源碼,介紹了如何在SpringBoot項目中使用AOP和自定義注解實現(xiàn)MySQL主從數(shù)據(jù)庫的動態(tài)切換,當從庫故障時,能自動切換到主庫,確保服務(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)移和高可用性:當主數(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: druidapplication-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: 9000003、數(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();
}
/**
* 獲取當前的環(huán)境配置,無配置返回null
*
* @return 當前的環(huán)境配置
*/
public static String[] getActiveProfiles()
{
return applicationContext.getEnvironment().getActiveProfiles();
}
/**
* 獲取當前的環(huán)境配置,當有多個環(huán)境配置時,只獲取第一個
*
* @return 當前的環(huán)境配置
*/
public static String getActiveProfile()
{
final String[] activeProfiles = getActiveProfiles();
return StringUtils.isNotEmpty(Arrays.toString(activeProfiles)) ? activeProfiles[0] : null;
}
/**
* 獲取配置文件中的值
*
* @param key 配置文件的key
* @return 當前的配置文件的值
*
*/
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項目中動態(tài)切換數(shù)據(jù)源和數(shù)據(jù)庫的詳細步驟
- SpringBoot實現(xiàn)數(shù)據(jù)源動態(tài)切換的最佳姿勢
- SpringBoot實現(xiàn)動態(tài)數(shù)據(jù)源切換的項目實踐
- SpringBoot實現(xiàn)動態(tài)數(shù)據(jù)源切換的方法總結(jié)
- 使用SpringBoot動態(tài)切換數(shù)據(jù)源的實現(xiàn)方式
- Springboot實現(xiàn)多數(shù)據(jù)源切換詳情
- SpringBoot多數(shù)據(jù)源切換實現(xiàn)代碼(Mybaitis)
- SpringBoot實現(xiàn)動態(tài)切換數(shù)據(jù)源的示例代碼
相關(guān)文章
Spring框架中ImportBeanDefinitionRegistrar的應(yīng)用詳解
這篇文章主要介紹了Spring框架中ImportBeanDefinitionRegistrar的應(yīng)用詳解,如果實現(xiàn)了ImportSelector接口,在配置類中被@Import加入到Spring容器中以后,Spring容器就會把ImportSelector接口方法返回的字符串數(shù)組中的類new出來對象然后放到工廠中去,需要的朋友可以參考下2024-01-01
Jedis零基礎(chǔ)入門及操作Redis中的數(shù)據(jù)結(jié)構(gòu)詳解
Jedis 的 API 方法跟 Redis 的命令基本上完全一致,熟悉 Redis 的操作命令,自然就很容易使用 Jedis,因此官方也推薦 Java 使用 Jedis 來連接和操作 Redis2022-09-09
基于Redis實現(xiàn)分布式應(yīng)用限流的方法
本篇文章主要介紹了基于 Redis 實現(xiàn)分布式應(yīng)用限流的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-12-12

