springBoot-Mybatis-Plus multi-data source switching implementation

Preface: This article mainly uses AbstractRoutingDataSource to dynamically switch data sources according to the identifier carried in http access;

1 Introduction to AbstractRoutingDataSource:

AbstractRoutingDataSource is an abstract class in the Spring framework, which can be used to implement dynamic data source switching. In the multi-data source scenario, AbstractRoutingDataSource can dynamically select the appropriate data source for operation according to different requests, so as to achieve the purpose of efficiently utilizing multiple data sources.

AbstractRoutingDataSource is not a data source directly connected to the database, it is just a routing data source, which is responsible for selecting a real data source to perform data operations according to certain rules. Its functions can be summarized as follows:

(1). Realize dynamic switching of multiple data sources: AbstractRoutingDataSource can achieve the purpose of multi-data source operation by dynamically selecting data sources. Especially in a distributed environment, data can be fragmented according to business needs, and then different fragmented data can be stored in different databases, so that data load balancing and high availability can be achieved.

(2)… Encapsulate the database connection pool and connection acquisition logic: AbstractRoutingDataSource encapsulates the implementation details of multiple data source connection pools and shields the details of the underlying data source, so that business code does not need to care about connection acquisition and release, thus simplifying the business Code writing.

(3). Realize dynamic switching of data sources: AbstractRoutingDataSource can realize dynamic switching of data sources by dynamically switching data sources, so that the data source can be upgraded, migrated, etc. without affecting the normal operation of the system.

To sum up, the main function of AbstractRoutingDataSource is to realize the dynamic switching of multiple data sources, which can greatly improve the performance and availability of the system in scenarios such as load balancing, data migration, and fragmented storage among multiple databases.

2 springBoot integration:

2.1 pom.xml import 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 Data source parsing class:
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);
        // multiple data sources
        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);
        }
        // Set the default data source
        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 {<!-- -->
            // This method has poor performance, use with caution
            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 & amp; & amp; !"".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 routing 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 Intercept http requests and set the db for this visit:
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)) {<!-- -->
            // It is not a httpreuqest request to be released directly
            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 {<!-- -->
        // After the method is executed or an error is reported, remove the data source
        if (null != threadLocal.get() & amp; & amp; threadLocal.get()) {<!-- -->
            DynamicDataSource.clear();
        }
        threadLocal. remove();
    }
}

WebConfiguration interception:

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;

    /**
     * Interceptor configuration
     *
     * @param registry registration class
     */
    @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+8
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+8
datasource.tinyid.secondary.username=root
datasource.tinyid.secondary.password=ddsoft
#datasource.tinyid.secondary.testOnBorrow=false
#datasource.tinyid.secondary.maxActive=10



2.5 Request: put the header into this db

3 Summary:

This article mainly intercepts the http request, analyzes the db to be accessed this time, and then sets the db to the ThreadLocal of DynamicDataSource, so that the corresponding db connection can be obtained when accessing the database to complete the operation;

Git address reference: https://codeup.aliyun.com/61cd21816112fe9819da8d9c/dynamic-demo.git