springBoot如何动态切换数据源

本文介绍了如何在SpringBoot项目中使用AOP和自定义注解实现MySQL主从数据库的动态切换,当从库故障时,能自动切换到主库,确保服务的高可用性。
摘要由CSDN通过智能技术生成

项目背景:最近公司中需要搭建mysql的主从,想着在spring中集成多数据源。mybatisplus提供的有插件用@DS注解就能够实现,但是这种在mysql服务宕机的情况下不能够进行自动切换,于是就想着用aop+自定义注解的方式来实现

项目实现效果:如果公司服务器搭建的是一主多从多个mysql数据源,主服务器用来读。从服务器用来写。此时你在代码层面用注解指定了一个增删改方法到从数据源,但是碰巧此时从数据源失效了,那么就会自动的切换到其它服务器。代码实现如下:

注意:为了节省篇幅,向controller、service层就不展示出来了,只展示相关核心代码。

1、pom文件


<dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
       </dependency>
 
       <!-- aop 切面 -->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-aop</artifactId>
       </dependency>
 
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-jdbc</artifactId>
       </dependency>
 
 
       <dependency>
           <groupId>org.projectlombok</groupId>
           <artifactId>lombok</artifactId>
           <optional>true</optional>
       </dependency>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-test</artifactId>
           <scope>test</scope>
       </dependency>
       <!-- druid -->
       <!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter -->
       <dependency>
           <groupId>com.alibaba</groupId>
           <artifactId>druid-spring-boot-starter</artifactId>
           <version>1.2.8</version>
       </dependency>
       <!--主从配置依赖-->
       <dependency>
           <groupId>com.baomidou</groupId>
           <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
           <version>2.4.2</version>
       </dependency>
       <dependency>
           <groupId>mysql</groupId>
           <artifactId>mysql-connector-java</artifactId>
           <version>8.0.21</version>
       </dependency>
       <dependency>
           <groupId>com.baomidou</groupId>
           <artifactId>mybatis-plus-boot-starter</artifactId>
           <version>3.2.0</version>
       </dependency>
       <!--mybatis-plus生成器-->
       <dependency>
           <groupId>com.baomidou</groupId>
           <artifactId>mybatis-plus-generator</artifactId>
           <version>3.3.2</version>
       </dependency>
       <dependency>
           <groupId>commons-lang</groupId>
           <artifactId>commons-lang</artifactId>
           <version>2.6</version>
       </dependency>
 
       <!-- 模板引擎 -->
       <dependency>
           <groupId>org.apache.velocity</groupId>
           <artifactId>velocity-engine-core</artifactId>
           <version>2.0</version>
       </dependency>
 
       <dependency>
           <groupId>log4j</groupId>
           <artifactId>log4j</artifactId>
           <version>1.2.14</version>
       </dependency>

2、配置文件:application.yml

server:
  port: 8088
 
spring:
  datasource:
    druid:
      type: com.alibaba.druid.pool.DruidDataSource
      master:
        url: jdbc:mysql://192.168.26.4:3306/test01?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
        username: root
        password: root
        driver-class-name: com.mysql.jdbc.Driver
      slave:
        url: jdbc:mysql://192.168.26.8:3306/test01?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
        username: root
        password: root
        driver-class-name: com.mysql.jdbc.Driver

3、数据源名称枚举类CommonConstant:

public class CommonConstant {
    /**
     * 默认数据源标识
     */
    public static final String MASTER = "master";
    /**
     * 从数据源标识
     */
    public static final String SLAVE = "slave";
}

4 数据源解析类DruidConfig:

@Data
@Configuration
public class DruidConfig {
  
    @Bean(name = CommonConstant.MASTER)
    @ConfigurationProperties("spring.datasource.druid.master")
    public DataSource masterDataSource()
    {
        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
        return dataSource;
    }
  
    @Bean(name = CommonConstant.SLAVE)
    @ConfigurationProperties("spring.datasource.druid.slave")
    public DataSource slaveDataSource()
    {
        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
        return dataSource;
    }
  
    @Bean
    @Primary
    public DynamicDataSource dynamicDataSource()
    {
        Map<Object, Object> dataSourceMap = new HashMap<>(2);
        dataSourceMap.put(CommonConstant.MASTER,masterDataSource());
        dataSourceMap.put(CommonConstant.SLAVE,slaveDataSource());
        //设置动态数据源
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
        dynamicDataSource.setTargetDataSources(dataSourceMap);
        //将数据源信息备份在defineTargetDataSources中
        dynamicDataSource.setDefineTargetDataSources(dataSourceMap);
        return dynamicDataSource;
    }
}

5、DynamicDataSource类

编写DynamicDataSource类继承AbstractRoutingDataSource类并重写抽象方法determineCurrentLookupKey以此来决定当前线程使用哪个数据源

/**
 * 动态数据源
 * 调用AddDefineDataSource组件的addDefineDynamicDataSource()方法,获取原来targetdatasources的map,并将新的数据源信息添加到map中,并替换targetdatasources中的map
 * 切换数据源时可以使用@DataSource(value = "数据源名称"),或者DynamicDataSourceContextHolder.setContextKey("数据源名称")
 * @author zhangyu
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class DynamicDataSource extends AbstractRoutingDataSource {
 
    //备份所有数据源信息,
    private Map<Object, Object> defineTargetDataSources;
  
    /**
     * 决定当前线程使用哪个数据源
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceHolder.getDynamicDataSourceKey();
    }
 
}

6、DynamicDataSourceHolder

DynamicDataSourceHolder类主要是设置当前线程的数据源名称,移除数据源名称,以及获取当前数据源的名称,便于动态切换

/**
 * 数据源切换处理
 *
 * @author zhangyu
 */
@Slf4j
public class DynamicDataSourceHolder {
    /**
     * 保存动态数据源名称
     */
    private static final ThreadLocal<String> DYNAMIC_DATASOURCE_KEY = new ThreadLocal<>();
  
    /**
     * 设置/切换数据源,决定当前线程使用哪个数据源
     */
    public static void setDynamicDataSourceKey(String key){
        log.info("数据源切换为:{}",key);
        DYNAMIC_DATASOURCE_KEY.set(key);
    }
  
    /**
     * 获取动态数据源名称,默认使用mater数据源
     */
    public static String getDynamicDataSourceKey(){
        String key = DYNAMIC_DATASOURCE_KEY.get();
        return key == null ? CommonConstant.MASTER : key;
    }
  
    /**
     * 移除当前数据源
     */
    public static void removeDynamicDataSourceKey(){
        log.info("移除数据源:{}",DYNAMIC_DATASOURCE_KEY.get());
        DYNAMIC_DATASOURCE_KEY.remove();
    }
}

7、自定义注解

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource
{
    /**
     * 切换数据源名称
     */
    public String value() default CommonConstant.MASTER;
}

8 aop切面

import com.alibaba.druid.pool.DruidDataSource;
import com.liubujun.config.*;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
 
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.*;
 
@Aspect
@Component
@Slf4j
public class DataSourceAspect {
  
    // 设置DataSource注解的切点表达式
//    @Pointcut("@annotation(com.liubujun.config.aespect.DataSource)")
    @Pointcut("execution(public * com.liubujun.service..*.*(..))")
    public void dynamicDataSourcePointCut(){
  
    }
  
    //环绕通知
    @Around("dynamicDataSourcePointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        DataSource defineAnnotation = getDefineAnnotation(joinPoint);
        String key = "";
        //判断方法上是否有注解,没有注解则默认是走的是主服务器
        if (defineAnnotation == null ) {
            key = CommonConstant.MASTER;
        }else {
            key = defineAnnotation.value();
        }
        //判断数据库是否断开连接
        key = getConnection(key);
        DynamicDataSourceHolder.setDynamicDataSourceKey(key);
        Object proceed = null;
        try {
            proceed =  joinPoint.proceed();
        } finally {
            DynamicDataSourceHolder.removeDynamicDataSourceKey();
        }
        return proceed;
    }
 
    /**
     * 先判断方法的注解,后判断类的注解,以方法的注解为准
     * @param joinPoint
     * @return
     */
    private DataSource getDefineAnnotation(ProceedingJoinPoint joinPoint){
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        DataSource dataSourceAnnotation = methodSignature.getMethod().getAnnotation(DataSource.class);
        if (Objects.nonNull(methodSignature)) {
            return dataSourceAnnotation;
        } else {
            Class<?> dsClass = joinPoint.getTarget().getClass();
            return dsClass.getAnnotation(DataSource.class);
        }
    }
 
    /**
     * 判断数据库是否连接成功
     * @return
     */
    private String getConnection(String target) throws SQLException {
        //将数据源名称添加到list集合,方便后续操作
        List<String> dataSources = new ArrayList<>();
        dataSources.add(CommonConstant.SLAVE);
        dataSources.add(CommonConstant.MASTER);
        //获取装配好的bean对象
        DruidConfig druidConfig = (DruidConfig)SpringUtil.getBean("druidConfig");
        DruidDataSource druidDataSource = new DruidDataSource();
        if (target.equals(CommonConstant.SLAVE)) {
             druidDataSource = (DruidDataSource) druidConfig.slaveDataSource();
        }
        if (target.equals(CommonConstant.MASTER)) {
            druidDataSource = (DruidDataSource) druidConfig.masterDataSource();
        }
        try {
            Connection connection = DriverManager.getConnection(druidDataSource.getUrl(), druidDataSource.getUsername(), druidDataSource.getPassword());
        } catch (SQLException e) {
            dataSources.remove(target);
            // shuffle 打乱顺序
            Collections.shuffle(dataSources);
            String changeTarget = dataSources.get(0);
            getConnection(changeTarget);
            log.info("========================数据源:{}连接异常,切换数据源为:{}===========================",target,changeTarget);
            return changeTarget;
        }
        return target;
    }
  
}

9 获取bean对象工具类

@Component
public class SpringUtil implements ApplicationContextAware {
  
    private static ApplicationContext applicationContext;
  
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if(SpringUtil.applicationContext == null) {
            SpringUtil.applicationContext = applicationContext;
        }
    }
  
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }
     //根据类名获取指定对象
    public static Object getBean(String name){
        return getApplicationContext().getBean(name);
    }
     //根据类型获取指定对象
    public static <T> T getBean(Class<T> clazz){
        return getApplicationContext().getBean(clazz);
    }
     //根据类名和类型获取指定对象
    public static <T> T getBean(String name,Class<T> clazz){
        return getApplicationContext().getBean(name, clazz);
    }
}

以上就是在代码层面动态切换数据源的相关代码,那么如何使用呢?

可以直接在业务service的实现层直接在方法上添加注解指定数据源,如:

我在这个方法上指定的是从数据库,如果此时从数据库发生宕机,那么就会自动切换到主数据库进行操作 

  • 8
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值