springBoot-Mybatis-Plus 多数据源切换实现

前言:本文主要通过AbstractRoutingDataSource,实现根据 http 访问携带的标识动态切换数据源;

1 AbstractRoutingDataSource 介绍:

AbstractRoutingDataSource 是 Spring 框架中的一个抽象类,它可以用来实现动态数据源切换。在多数据源场景下,AbstractRoutingDataSource 可以根据不同的请求来动态地选择合适的数据源进行操作,以达到高效利用多个数据源的目的。

AbstractRoutingDataSource 并不是直接连接数据库的数据源,它只是一个路由数据源,它负责根据一定的规则选择一个真正的数据源来执行数据操作。它的作用可以归纳为以下几点:

(1). 实现多数据源动态切换:AbstractRoutingDataSource 可以通过动态的选定数据源,达到多数据源操作的目的。特别在分布式环境中,可以根据业务需求将数据进行分片,然后将不同的分片数据存储在不同的数据库中,这样就能实现数据负载均衡和高可用性。

(2)… 封装数据库连接池和连接的获取逻辑:AbstractRoutingDataSource 通过封装多个数据源连接池的实现细节,屏蔽底层数据源的细节,使得业务代码不需要关心连接的获取和释放,从而简化了业务代码的编写。

(3). 实现数据源的动态切换:AbstractRoutingDataSource 可以通过动态切换数据源,实现数据源的动态切换,从而在不影响系统正常运行的情况下,能够对数据源进行升级、迁移等操作。

综上所述,AbstractRoutingDataSource 的主要作用是实现多数据源的动态切换,这在多个数据库之间实现负载均衡、数据迁移、分片存储等场景下,可以大大提高系统的性能和可用性。

2 springBoot 集成:

2.1 pom.xml 引入jar:

 <dependency>
    <groupId>com.baomidou</groupId>
     <artifactId>mybatis-plus-boot-starter</artifactId>
     <version>3.5.2</version>
 </dependency>
 <dependency>
     <groupId>mysql</groupId>
     <artifactId>mysql-connector-java</artifactId>
     <version>8.0.21</version>
 </dependency>

2.2 数据源解析类:
DataSourceConfig:


import com.example.dynamicdemo.config.DynamicDataSource;
import com.zaxxer.hikari.HikariDataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.bind.BindResult;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.ConfigurationPropertySources;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;

import javax.sql.DataSource;
import java.util.*;

/**
 * @author du_imba
 */
@Configuration
public class DataSourceConfig {

    private static final Logger logger = LoggerFactory.getLogger(DataSourceConfig.class);

    @Autowired
    private Environment environment;
    private static final String SEP = ",";

    @Bean
    public DataSource getDynamicDataSource() {
        DynamicDataSource routingDataSource = new DynamicDataSource();
        List<String> dataSourceKeys = new ArrayList<>();

        Iterable sources = ConfigurationPropertySources.get(environment);
        Binder binder = new Binder(sources);



        BindResult bindResult = binder.bind("datasource.tinyid", Properties.class);
        Properties properties= (Properties) bindResult.get();
        String names = properties.getProperty("names");
        String dataSourceType = properties.getProperty("type");


//        RelaxedPropertyResolver propertyResolver = new RelaxedPropertyResolver(environment, "datasource.tinyid.");
//        String names = propertyResolver.getProperty("names");
//        String dataSourceType = propertyResolver.getProperty("type");

        Map<Object, Object> targetDataSources = new HashMap<>(4);
        routingDataSource.setTargetDataSources(targetDataSources);
        routingDataSource.setDataSourceKeys(dataSourceKeys);
        // 多个数据源
        for (String name : names.split(SEP)) {

            Map<String, Object> dsMap = getSubProperties(name + ".",properties);
            DataSource dataSource = buildDataSource(dataSourceType, dsMap);
            buildDataSourceProperties(dataSource, dsMap);
            targetDataSources.put(name, dataSource);
            dataSourceKeys.add(name);
        }
        // 设置默认数据源
        routingDataSource.setDefaultTargetDataSource(targetDataSources.get("primary"));
        return routingDataSource;
    }

    private Map<String, Object> getSubProperties(String s,Properties properties) {
        Map<String, Object> dsMap = new HashMap<>(1<<2);

        dsMap.put("driver-class-name",properties.get(s+"driver-class-name"));
        dsMap.put("url",properties.get(s+"url"));
        dsMap.put("username",properties.get(s+"username"));
        dsMap.put("password",properties.get(s+"password"));
        return dsMap;
    }

    private void buildDataSourceProperties(DataSource dataSource, Map<String, Object> dsMap) {
        try {
            // 此方法性能差,慎用
            BeanUtils.copyProperties(dataSource, dsMap);
        } catch (Exception e) {
            logger.error("error copy properties", e);
        }
    }

    private HikariDataSource buildDataSource(String dataSourceType, Map<String, Object> dsMap) {
        try {
//            String className = DEFAULT_DATASOURCE_TYPE;
//            if (dataSourceType != null && !"".equals(dataSourceType.trim())) {
//                className = dataSourceType;
//            }
//            Class<? extends DataSource> type = (Class<? extends DataSource>) Class.forName(className);
            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();

            return DataSourceBuilder.create()
                    .driverClassName(driverClassName)
                    .url(url)
                    .username(username)
                    .password(password)
//                    .type(type)
                    .type(HikariDataSource.class)
                    .build();

        } catch (Exception e) {
            logger.error("buildDataSource error", e);
            throw new IllegalStateException(e);
        }
    }


}

DynamicDataSource 路由db:


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

import java.util.List;
import java.util.Random;

/**
 * @author du_imba
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    private  static  final ThreadLocal<String> threadLocal = new ThreadLocal<>();

    private List<String> dataSourceKeys;

    @Override
    protected Object determineCurrentLookupKey() {
//        if(dataSourceKeys.size() == 1) {
//            return dataSourceKeys.get(0);
//        }
//        Random r = new Random();
//        return dataSourceKeys.get(r.nextInt(dataSourceKeys.size()));
        return getDb();
    }

    public List<String> getDataSourceKeys() {
        return dataSourceKeys;
    }

    public void setDataSourceKeys(List<String> dataSourceKeys) {
        this.dataSourceKeys = dataSourceKeys;
    }

    public static void setDb(String db){
        threadLocal.set(db);
    }
    public static String getDb(){
        return threadLocal.get();
    }
    public static void clear() {
        threadLocal.remove();
    }
}

2.3 拦截http 请求,设置本次访问的db:
HttpRequestDynamic:




import lombok.extern.slf4j.Slf4j;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Slf4j
public class HttpRequestDynamic implements HandlerInterceptor {
    final static ThreadLocal<Boolean> threadLocal = new ThreadLocal<>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            // 不是 httpreuqest 请求直接放行
            return true;
        }
        DynamicDataSource.setDb(request.getHeader("db"));
        threadLocal.set(true);

        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 在方法执行完毕或者执行报错后,移除数据源
        if (null != threadLocal.get() && threadLocal.get()) {
            DynamicDataSource.clear();
        }
        threadLocal.remove();
    }
}

WebConfiguration 拦截:


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@Import({HttpRequestDynamic.class})
public class WebConfiguration implements WebMvcConfigurer {
    @Autowired
    private HttpRequestDynamic httpRequestDynamic;

    /**
     * 拦截器配置
     *
     * @param registry 注册类
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(httpRequestDynamic).addPathPatterns("/**")
                .excludePathPatterns(
                        "/file/get/*",
                        "/image/view/*",
                        "/**/error"
                );
    }
}

2.4 application.properties

server.port=9999
server.context-path=/tinyid

batch.size.max=100000

#datasource.tinyid.names=primary
datasource.tinyid.names=primary,secondary

datasource.tinyid.primary.driver-class-name=com.mysql.cj.jdbc.Driver
datasource.tinyid.primary.url=jdbc:mysql://localhost:3406/d1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
datasource.tinyid.primary.username=root
datasource.tinyid.primary.password=ddsoft
#datasource.tinyid.primary.maxActive=10

datasource.tinyid.secondary.driver-class-name=com.mysql.cj.jdbc.Driver
datasource.tinyid.secondary.url=jdbc:mysql://localhost:3406/d2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
datasource.tinyid.secondary.username=root
datasource.tinyid.secondary.password=ddsoft
#datasource.tinyid.secondary.testOnBorrow=false
#datasource.tinyid.secondary.maxActive=10



2.5 请求:header 头放入本次的db
在这里插入图片描述

3 总结:

本文主要通过拦截http 请求,解析本次要访问的db,然后将db 设置到DynamicDataSource的ThreadLocal 中,使得访问数据库时获取到对应的db连接完成操作;

git 地址参考:https://codeup.aliyun.com/61cd21816112fe9819da8d9c/dynamic-demo.git

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值