前言:本文主要通过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