相关推荐recommended
springBoot如何动态切换数据源
作者:mmseoamin日期:2024-04-29

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

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

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

1、pom文件


           org.springframework.boot
           spring-boot-starter-web
       
 
       
       
           org.springframework.boot
           spring-boot-starter-aop
       
 
       
           org.springframework.boot
           spring-boot-starter-jdbc
       
 
 
       
           org.projectlombok
           lombok
           true
       
       
           org.springframework.boot
           spring-boot-starter-test
           test
       
       
       
       
           com.alibaba
           druid-spring-boot-starter
           1.2.8
       
       
       
           com.baomidou
           dynamic-datasource-spring-boot-starter
           2.4.2
       
       
           mysql
           mysql-connector-java
           8.0.21
       
       
           com.baomidou
           mybatis-plus-boot-starter
           3.2.0
       
       
       
           com.baomidou
           mybatis-plus-generator
           3.3.2
       
       
           commons-lang
           commons-lang
           2.6
       
 
       
       
           org.apache.velocity
           velocity-engine-core
           2.0
       
 
       
           log4j
           log4j
           1.2.14
       

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 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 defineTargetDataSources;
  
    /**
     * 决定当前线程使用哪个数据源
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceHolder.getDynamicDataSourceKey();
    }
 
}

6、DynamicDataSourceHolder

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

/**
 * 数据源切换处理
 *
 * @author zhangyu
 */
@Slf4j
public class DynamicDataSourceHolder {
    /**
     * 保存动态数据源名称
     */
    private static final ThreadLocal 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 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 getBean(Class clazz){
        return getApplicationContext().getBean(clazz);
    }
     //根据类名和类型获取指定对象
    public static  T getBean(String name,Class clazz){
        return getApplicationContext().getBean(name, clazz);
    }
}

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

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

springBoot如何动态切换数据源,第1张

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