springBoot-Mybatis-Plus 多数据源切换实现
作者:mmseoamin日期:2023-12-14

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

1 AbstractRoutingDataSource 介绍:

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

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

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

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

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

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

2 springBoot 集成:

2.1 pom.xml 引入jar:

 
    com.baomidou
     mybatis-plus-boot-starter
     3.5.2
 
 
     mysql
     mysql-connector-java
     8.0.21
 

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 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 targetDataSources = new HashMap<>(4);
        routingDataSource.setTargetDataSources(targetDataSources);
        routingDataSource.setDataSourceKeys(dataSourceKeys);
        // 多个数据源
        for (String name : names.split(SEP)) {
            Map 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 getSubProperties(String s,Properties properties) {
        Map 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 dsMap) {
        try {
            // 此方法性能差,慎用
            BeanUtils.copyProperties(dataSource, dsMap);
        } catch (Exception e) {
            logger.error("error copy properties", e);
        }
    }
    private HikariDataSource buildDataSource(String dataSourceType, Map dsMap) {
        try {
//            String className = DEFAULT_DATASOURCE_TYPE;
//            if (dataSourceType != null && !"".equals(dataSourceType.trim())) {
//                className = dataSourceType;
//            }
//            Class type = (Class) 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 threadLocal = new ThreadLocal<>();
    private List 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 getDataSourceKeys() {
        return dataSourceKeys;
    }
    public void setDataSourceKeys(List 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 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

springBoot-Mybatis-Plus 多数据源切换实现,在这里插入图片描述,第1张

3 总结:

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

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