阅读 343
Springboot项目实现Mysql多数据源切换

Springboot项目实现Mysql多数据源切换

一、 前言

通常项目Mysql会有多个数据库(即数据源),某些特殊业务我们需要同时操作不同的数据源。

二、分析AbstractRoutingDataSource抽象类源码

关注import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource以下变量

@Nullable
private Map<Object, Object> targetDataSources; // 目标数据源
@Nullable
private Object defaultTargetDataSource; // 默认目标数据源
@Nullable
private Map<Object, DataSource> resolvedDataSources; // 解析的数据源
@Nullable
private DataSource resolvedDefaultDataSource; // 解析的默认数据源
复制代码

这两组变量是相互对应的,在熟悉多实例数据源切换代码的不难发现,当有多个数据源的时候,一定要指定一个作为默认的数据源;

在这里也同理,当同时初始化多个数据源的时候,需要显式的调用setDefaultTargetDataSource方法指定一个作为默认数据源;

我们需要关注的是Map<Object, Object> targetDataSourcesMap<Object, DataSource> resolvedDataSources

targetDataSources是暴露给外部程序用来赋值的,而resolvedDataSources是程序内部执行时的依据,因此会有一个赋值的操作;

根据这段源码可以看出,每次执行时,都会遍历targetDataSources内的所有元素并赋值给resolvedDataSources;这样如果我们在外部程序新增一个新的数据源,都会添加到内部使用,从而实现数据源的动态加载

继承该抽象类的时候,必须实现一个抽象方法:protected abstract Object determineCurrentLookupKey(),该方法用于指定到底需要使用哪一个数据源。


二、实现多数据源切换和动态数据源加载

A - 配置文件信息

application.yml文件

server:
  port: 18080
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      # 主数据源
      master-db:
        driverClassName: com.mysql.jdbc.Driver
        url: jdbc:mysql://192.168.223.129:13306/test_master_db?characterEncoding=utf-8
        username: josen
        password: josen
      # 从数据源
      slave-db:
        driverClassName: com.mysql.jdbc.Driver
        url: jdbc:mysql://192.168.223.129:13306/test_slave_db?characterEncoding=utf-8
        username: josen
        password: josen
mybatis:
  mapper-locations: classpath:mapper/*.xml
logging:
  path: ./logs/mydemo20201105.log
  level:
    com.josen.mydemo20201105: debug
复制代码

maven 依赖

<dependencies>
    <!-- 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.1.10</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.3</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>
复制代码

B - 编码实现

1、创建一个DynamicDataSource类,继承AbstractRoutingDataSource抽象类,实现determineCurrentLookupKey方法,通过该方法指定当前使用哪个数据源;

2、 在DynamicDataSource类中通过ThreadLocal维护一个全局数据源名称,后续通过修改该名称实现动态切换;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.sql.DataSource;
import java.util.Map;

/**
 * @ClassName DynamicDataSource
 * @Description 设置动态数据源
 * @Author Josen
 * @Date 2020/11/5 14:28
 **/
public class DynamicDataSource extends AbstractRoutingDataSource {
    // 通过ThreadLocal维护一个全局唯一的map来实现数据源的动态切换
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }

    /**
     * 指定使用哪一个数据源
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return getDataSource();
    }

    public static void setDataSource(String dataSource) {
        contextHolder.set(dataSource);
    }

    public static String getDataSource() {
        return contextHolder.get();
    }

    public static void clearDataSource() {
        contextHolder.remove();
    }
}

复制代码

3、创建DynamicDataSourceConfig类,引入application.yaml配置的多个数据源信息,构建多个数据源;

import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * @ClassName DynamicDataSourceConfig
 * @Description 引入动态数据源,构建数据源
 * @Author Josen
 * @Date 2020/11/5 14:23
 **/
@Configuration
@Component
public class DynamicDataSourceConfig {
    /**
     * 读取application配置,构建master-db数据源
     */
    @Bean
    @ConfigurationProperties("spring.datasource.druid.master-db")
    public DataSource myMasterDataSource(){
        return DruidDataSourceBuilder.create().build();
    }

    /**
     * 读取application配置,构建slave-db数据源
     */
    @Bean
    @ConfigurationProperties("spring.datasource.druid.slave-db")
    public DataSource  mySlaveDataSource(){
        return DruidDataSourceBuilder.create().build();
    }
    /**
     * 读取application配置,创建动态数据源
     */
    @Bean
    @Primary
    public DynamicDataSource dataSource(DataSource myMasterDataSource, DataSource mySlaveDataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("master-db",myMasterDataSource);
        targetDataSources.put("slave-db", mySlaveDataSource);
        // myMasterDataSource=默认数据源
        // targetDataSources=目标数据源(多个)
        return new DynamicDataSource(myMasterDataSource, targetDataSources);
    }
}
复制代码

4、 创建MyDataSource自定义注解,后续AOP通过该注解作为切入点,通过获取使用该注解存入不同的值动态切换指定的数据源

import java.lang.annotation.*;
/**
 * 自定义数据源注解
 * 在需要切换数据源的Service层方法上添加此注解,指定数据源名称
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyDataSource {
    String name() default "";
}
复制代码

5、 创建DataSourceAspectAOP切面类,以MyDataSource注解为切入点作拓展。在执行被@MyDataSource注解的方法时,获取该注解传入的name,并切换到指定数据源执行,执行完成后切换回默认数据源;

import com.josen.mydemo20201105.annotation.MyDataSource;
import com.josen.mydemo20201105.datasource.DynamicDataSource;
import lombok.extern.slf4j.Slf4j;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.lang.reflect.Method;

/**
 * @ClassName DataSourceAspect
 * @Description Aop切面类配置
 * @Author Josen
 * @Date 2020/11/5 14:35
 **/
@Aspect
@Component
@Slf4j
public class DataSourceAspect {
    private static final Logger logger = LoggerFactory.getLogger(DataSourceAspect.class);

    /**
     * 设置切入点
     * 只有调用@MyDataSource注解的方法才会触发around
     */
    @Pointcut("@annotation(com.josen.mydemo20201105.annotation.MyDataSource)")
    public void dataSourcePointCut() {
    }

    /**
     * 截取使用MyDataSource注解的方法,切换指定数据源
     * 环绕切面:是(前置&后置&返回&异常)通知的结合体,更像是动态代理的整个过程
     * @param point
     */
    @Around("dataSourcePointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        logger.info("execute DataSourceAspect around=========>"+method.getName());
        // 1. 获取自定义注解MyDataSource,查看是否配置指定数据源名称
        MyDataSource dataSource = method.getAnnotation(MyDataSource.class);
        if(dataSource == null){
            // 1.1 使用默认数据源
            DynamicDataSource.setDataSource("master-db");
        }else {
            // 1.2 使用指定名称数据源
            DynamicDataSource.setDataSource(dataSource.name());
            logger.info("使用指定名称数据源=========>"+dataSource.name());
        }
        try {
            return point.proceed();
        } finally {
            // 后置处理 - 恢复默认数据源
            DynamicDataSource.clearDataSource();
        }
    }
}
复制代码

6、 配置启动类

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;

@SpringBootApplication(exclude= {DataSourceAutoConfiguration.class}) // 不加载默认数据源配置
@MapperScan(basePackages = "com.josen.mydemo.mapper")
public class MydemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(MydemoApplication.class, args);
    }
}
复制代码

7、 到这里基本上已经完成,剩下的就是测试是否正确切换数据源了;

Mapper层

@Repository
public interface PermissionMapper {
    List<Permission> findAll();
}
复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.josen.mydemo.mapper.PermissionMapper">
    <select id="findAll" resultType="com.josen.mydemo20201105.pojo.Permission">
        select * from t_permission;
    </select>
</mapper>
复制代码

Service层

@Service
public class PermissionService {
    @Autowired
    private PermissionMapper permissionMapper;
	// 切换从库数据源
    @MyDataSource(name = "slave-db")
    public List<Permission> findSlaveAll(){
        return permissionMapper.findAll();
    }
	// 默认数据源
    public List<Permission> findAll(){
        return permissionMapper.findAll();
    }
}
复制代码

Controller层

@RestController
@RequestMapping("/permission")
public class PermissionController {
    @Autowired
    private PermissionService permissionService;

    // 测试获取默认master-db数据
    @GetMapping("/master")
    public List<Permission> handlerFindAll() {
        List<Permission> list = permissionService.findAll();
        return list;
    }

    // 测试获取指定slave-db数据
    @GetMapping("/slave")
    public List<Permission> handlerFindAll2() {
        List<Permission> list = permissionService.findSlaveAll();
        return list;
    }
}
复制代码

C - 测试数据源切换

Mysql数据

接口返回数据

Demo源码地址:gitee.com/taco-gigigi…

文章分类
后端
文章标签