美文网首页
Springboot环境中多个DataSource基于自定义注解

Springboot环境中多个DataSource基于自定义注解

作者: 冬天里的懒喵 | 来源:发表于2021-09-18 20:16 被阅读0次

前面配置了mysql数据库的主从复制模式,在数据库上实现了master-slave配置,通过这种方式可以实现一主一从,或者一主多从,从而提升系统的高可用。
这是数据库层面的实现。在数据库实现了主从模式之后,我们需要考率的问题就是,在我们的应用代码中,如何将不同的数据库操作按需要分配到不同的数据库去执行。

1.需要的依赖

    implementation 'org.springframework.boot:spring-boot-starter'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-aop'
    implementation 'mysql:mysql-connector-java:8.0.25'
    implementation 'com.zaxxer:HikariCP:4.0.3'
    

2.yml配置

在application.yml文件中,数据源相关配置如下:

# 自定义的动态数据源配置
custom:
  datasource:
    - key: master
      type: com.zaxxer.hikari.HikariDataSource
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://192.168.161.114:3306/gts?useSSL=false&autoReconnect=true&characterEncoding=UTF-8&serverTimezone=UTC
      username: gts
      password: mysql
      default: true
    - key: slave1
      type: com.zaxxer.hikari.HikariDataSource
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://192.168.161.115:3306/gts?useSSL=false&autoReconnect=true&characterEncoding=UTF-8&serverTimezone=UTC
      username: gts
      password: mysql
    - key: slave2
      type: com.zaxxer.hikari.HikariDataSource
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://192.168.161.114:3306/gts?useSSL=false&autoReconnect=true&characterEncoding=UTF-8&serverTimezone=UTC
      username: gts
      password: mysql 

在custom.datasource,定义了一组数据源配置。master配置到主库,slave1喝slave2分贝表示多组从库。本文用主库来表示从库。

3.动态数据源配置

数据源定义:

package com.dhb.gts.javacourse.week7.dynamic;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceType();
    }
}

动态数据源切面配置:

package com.dhb.gts.javacourse.week7.dynamic;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Aspect
@Order(-1)// 保证该AOP在@Transactional之前执行
@Component
@Slf4j
public class DynamicDataSourceAspect {
    @Before("@annotation(ds)")
    public void changeDataSource(JoinPoint point, TargetDataSource ds) throws Throwable {
        String dsId = ds.name();
        if (!DynamicDataSourceContextHolder.containsDataSource(dsId)) {
            log.error("数据源[{}]不存在,使用默认数据源 > {}", ds.name(), point.getSignature());
        }else {
            log.info("Use DataSource : {} > {}", dsId, point.getSignature());
            DynamicDataSourceContextHolder.setDataSourceType(dsId);
        }
    }
    
    @After("@annotation(ds)")
    public void restoreDataSource(JoinPoint point, TargetDataSource ds) {
        log.debug("Revert DataSource : {} > {}", ds.name(), point.getSignature());
        DynamicDataSourceContextHolder.clearDataSourceType();
    }
}

数据源切换处理

package com.dhb.gts.javacourse.week7.dynamic;

import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;

@Slf4j
public class DynamicDataSourceContextHolder {

    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    public static List<String> dataSourceIds = new ArrayList<>();

    public static void setDataSourceType(String dataSourceType) {
        contextHolder.set(dataSourceType);
        log.info("dataSourceType set is {}",dataSourceType);
    }

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

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

    /**
     * 判断指定DataSrouce当前是否存在
     */
    public static boolean containsDataSource(String dataSourceId){
        return dataSourceIds.contains(dataSourceId);
    }
}

数据源注册类

package com.dhb.gts.javacourse.week7.dynamic;

import com.google.common.base.Strings;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.boot.context.properties.source.ConfigurationPropertyNameAliases;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotationMetadata;

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

@Slf4j
public class DynamicDataSourceRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware {

    // 如配置文件中未指定数据源类型,使用该默认值
    private static final Object DATASOURCE_TYPE_DEFAULT = "com.zaxxer.hikari.HikariDataSource";
    /**
     * 数据源参数配置别名
     */
    private final static ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases();

    static {
        //由于部分数据源配置不同,所以在此处添加别名,避免切换数据源出现某些参数无法注入的情况
        aliases.addAliases("url", "jdbc-url");
        aliases.addAliases("username", "user");
    }

    /**
     * 参数绑定工具
     */
    private Binder binder;
    /**
     * 配置上下文(也可以理解为配置文件的获取工具)
     */
    private Environment env;
    // 默认数据源
    private DataSource defaultDataSource;
    /**
     * 自定义数据源
     */
    private Map<String, DataSource> customDataSources = new HashMap<>();

    public DataSource buildDataSource(Map<String, Object> dsMap) {
        try {
            Object type = dsMap.get("type");
            if (type == null) {
                // 默认DataSource
                type = DATASOURCE_TYPE_DEFAULT;
            }
            Class<? extends DataSource> dataSourceType;
            dataSourceType = (Class<? extends DataSource>) Class.forName((String) type);
            String driverClassName = dsMap.get("driver-class-name").toString();
            String url = dsMap.get("url").toString();
            String username = dsMap.get("username").toString();
            String password = dsMap.get("password").toString();
            DataSourceBuilder factory = DataSourceBuilder.create().driverClassName(driverClassName).url(url)
                    .username(username).password(password).type(dataSourceType);
            return factory.build();
        } catch (Throwable e) {
            log.error("buildDataSource failed!", e);
        }
        return null;
    }

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
        // 将主数据源添加到更多数据源中
        targetDataSources.put("dataSource", defaultDataSource);
        DynamicDataSourceContextHolder.dataSourceIds.add("dataSource");
        // 添加更多数据源
        targetDataSources.putAll(customDataSources);
        // 创建DynamicDataSource
        DynamicDataSourceContextHolder.dataSourceIds.addAll(customDataSources.keySet());
        //bean定义类
        GenericBeanDefinition define = new GenericBeanDefinition();
        //设置bean的类型,此处DynamicDataSource是继承AbstractRoutingDataSource的实现类
        define.setBeanClass(DynamicDataSource.class);
        //需要注入的参数,类似spring配置文件中的<property/>
        MutablePropertyValues mpv = define.getPropertyValues();
        //添加默认数据源,避免key不存在的情况没有数据源可用
        mpv.add("defaultTargetDataSource", defaultDataSource);
        //添加其他数据源
        mpv.add("targetDataSources", targetDataSources);
        //将该bean注册为datasource,不使用spring-boot自动生成的datasource
        registry.registerBeanDefinition("datasource", define);
        log.info("Dynamic DataSource Registry success !");
    }

    @Override
    public void setEnvironment(Environment environment) {
        this.env = environment;
        //绑定配置器
        binder = Binder.get(env);
        initCustomDataSources();
    }


    private void initCustomDataSources() {
        // 读取配置文件获取更多数据源,也可以通过defaultDataSource读取数据库获取更多数据源
        List<Map> configs = binder.bind("custom.datasource", Bindable.listOf(Map.class)).get();
        String dsPrefix;
        DataSource custom;
        for (Map config : configs) {
            String key = config.get("key").toString();
            if (!Strings.isNullOrEmpty(key)) {
                dsPrefix = key;
            } else {
                dsPrefix = "default";
            }
            custom = buildDataSource(config);
            customDataSources.put(dsPrefix, custom);
            dataBinder(custom, config);
            //如果 default标识为true,则将其设置为defaultDataSource
            if (null != config.get("default") && "true".equals(config.get("default").toString())) {
                defaultDataSource = custom;
            }
        }
        //如果default数据源没有,将master设置为default的数据源。
        if (null == defaultDataSource) {
            defaultDataSource = customDataSources.get("master");
        }
    }

    private void dataBinder(DataSource dataSource, Map properties) {
        ConfigurationPropertySource source = new MapConfigurationPropertySource(properties);
        Binder binderEx = new Binder(source.withAliases(aliases));
        //将参数绑定到对象
        binderEx.bind(ConfigurationPropertyName.EMPTY, Bindable.ofInstance(dataSource));
    }

}

定义一个注解,在使用数据源的时候通过注解进行配置:

package com.dhb.gts.javacourse.week7.dynamic;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// 自定义一个注解,在方法上使用,用于指定使用哪个数据源
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
    String name();
}

4.使用说明

4.1 启动类配置

需要import定义的DynamicDataSourceRegister。
@Import({DynamicDataSourceRegister.class})
另外需要开启切面。
@EnableAspectJAutoProxy(proxyTargetClass=true)

package com.dhb.gts.javacourse.week7;

import com.dhb.gts.javacourse.week7.dynamic.DynamicDataSourceRegister;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.Import;

@SpringBootApplication(scanBasePackages  = {"com.dhb.gts.javacourse.fluent.dao","com.dhb.gts.javacourse.week7"} )
@MapperScan(basePackages = {"com.dhb.gts.javacourse.fluent.mapper"})
@Import({DynamicDataSourceRegister.class})
@EnableAspectJAutoProxy(proxyTargetClass=true)
public class Starter {

    public static void main(String[] args) {
        SpringApplication.run(Starter.class, args);
    }
}

4.2 Service层注解的使用

现在将自定义的注解,配置到Service层即可使用:

    @Async
    @TargetDataSource(name = "master")
    public ListenableFuture<OrderSummaryEntity> asyncQueryOrderById(int order_id){
        OrderSummaryEntity entity = orderSummaryDao.selectById(order_id);
        return new AsyncResult<>(entity);
    }

    @TargetDataSource(name = "slave1")
    public OrderSummaryEntity queryOrderById(int order_id){
        return orderSummaryDao.selectById(order_id);
    }

通过Controller进行调用:

    @RequestMapping("/asyncQueryByKey")
    public String asyncQueryByKey(String key) {
        Stopwatch stopwatch = Stopwatch.createStarted();
        Integer orde_id = Integer.parseInt(key);
        OrderSummaryEntity entity = null;
        try {
            entity = orderService.asyncQueryOrderById(orde_id).get();
        }catch (Exception e) {
            e.printStackTrace();
        }
        stopwatch.stop();
        log.info("通过key查询,走索引耗时:" + stopwatch);
        return JSON.toJSONString(entity);
    }
    
        @RequestMapping("/queryByKey")
    public String queryByKey(String key) {
        Stopwatch stopwatch = Stopwatch.createStarted();
        Integer orde_id = Integer.parseInt(key);
        OrderSummaryEntity entity = orderService.queryOrderById(orde_id);
        stopwatch.stop();
        log.info("通过key查询,走索引耗时:" + stopwatch);
        return JSON.toJSONString(entity);
    }
asyncQueryByKey使用master请求 queryByKey 通过slave数据源查询

查询日志:

2021-09-18 19:13:03.406  INFO 18368 --- [async-service-1] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2021-09-18 19:13:03.504  INFO 18368 --- [async-service-1] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2021-09-18 19:13:03.560  INFO 18368 --- [async-service-1] c.d.g.j.week7.service.OrderService       : 通过线程池插入完成,共耗时:196.5 ms
2021-09-18 19:13:39.022  INFO 18368 --- [async-service-2] c.d.g.j.w.d.DynamicDataSourceAspect      : Use DataSource : master > ListenableFuture com.dhb.gts.javacourse.week7.service.OrderService.asyncQueryOrderById(int)
2021-09-18 19:13:39.022  INFO 18368 --- [async-service-2] d.g.j.w.d.DynamicDataSourceContextHolder : dataSourceType set is master
2021-09-18 19:13:39.031  INFO 18368 --- [nio-8084-exec-4] c.d.g.j.w.controller.OrderController     : 通过key查询,走索引耗时:14.56 ms
2021-09-18 19:15:17.534  INFO 18368 --- [nio-8084-exec-5] c.d.g.j.w.d.DynamicDataSourceAspect      : Use DataSource : slave1 > OrderSummaryEntity com.dhb.gts.javacourse.week7.service.OrderService.queryOrderById(int)
2021-09-18 19:15:17.535  INFO 18368 --- [nio-8084-exec-5] d.g.j.w.d.DynamicDataSourceContextHolder : dataSourceType set is slave1
2021-09-18 19:15:17.535  INFO 18368 --- [nio-8084-exec-5] com.zaxxer.hikari.HikariDataSource       : HikariPool-2 - Starting...
2021-09-18 19:15:17.553  INFO 18368 --- [nio-8084-exec-5] com.zaxxer.hikari.HikariDataSource       : HikariPool-2 - Start completed.
2021-09-18 19:15:17.556  INFO 18368 --- [nio-8084-exec-5] c.d.g.j.w.controller.OrderController     : 通过key查询,走索引耗时:21.28 ms

相关文章

网友评论

      本文标题:Springboot环境中多个DataSource基于自定义注解

      本文链接:https://www.haomeiwen.com/subject/ezuygltx.html