【解决方式】一种双数据源解决方案
在工作中遇到特殊业务场景,但是不想对业务代码有太多的侵入,在这里记录分享给各位博友,供参考评判,有更好的方式可以评论。
业务开发过程中,需要把一部分数据表的数据拿出来做业务复盘,重新计算收益;所以就提出了几种方案,
(1)使用shardingsphere分库分表。
(2)使用shardingsphere分表。
(3)双数据源配置。
我提出了双数据源配置的方案,在这里简单说明分享一下。
项目采用的是spring-boot框架结构,数据连接池采用druid。
启动类注解加配置如下:
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss #controller返回json的全局时间格式
time-zone: GMT+8
#多环境配置
profiles:
active:
#数据源配置mysql
datasource:
type: com.alibaba.druid.pool.DruidDataSource
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
main:
url: jdbc:mysql://localhost:3306/db1?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&rewriteBatchedStatements=true
replay:
url: jdbc:mysql://localhost:3306/db2?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&rewriteBatchedStatements=true
druid:
max-active: 50
initial-size: 10
min-idle: 5
max-wait: 60000 # 配置超时等待时间
time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最小生存的时间,单位是毫秒
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
filters: stat,wall,log4j
根据application.yml中配置信息去初始化数据源对象。
@Configuration
public class MultiDataSourceConfig {
@Bean(name = "mainDb")
@ConfigurationProperties(prefix = "spring.datasource.main")
public DataSource mainDataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "replayDb")
@ConfigurationProperties(prefix = "spring.datasource.replay")
public DataSource mlReplayDataSource() {
return DruidDataSourceBuilder.create().build();
}
}
注入主数据源和副数据源对象到数据源路由对象中,并与路由中的key对应。实现从map中灵活获取不同数据源。
@Primary
@Component
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {
// 使用TheardLocal主要是为了线程数据隔离
private static ThreadLocal<String> dbName = new ThreadLocal<>();
@Resource
private DataSource mainDb;
@Resource
private DataSource replayDb;
public static void setDbName(String name) {
dbName.set(name);
}
public static void removeDbName() {
dbName.remove();
}
public static String getDbName() {
return dbName.get();
}
@Override
protected Object determineCurrentLookupKey() {
String name = dbName.get();
if (Objects.nonNull(name)) {
log.warn("-------------------> 切换到数据库{} <-------------------", name);
} else {
log.warn("-------------------> 主数据库 <----------------------");
}
return name;
}
@Override
public void afterPropertiesSet() {
Map<Object, Object> targetDataSource = new ConcurrentHashMap<>();
targetDataSource.put("main", mainDb);
// 将第一个数据源设置为默认的数据源。
super.setDefaultTargetDataSource(mainDb);
targetDataSource.put("replay", replayDb);
// 将Map对象赋值给AbstrictRoutingDataSource内部的Map对象中。
super.setTargetDataSources(targetDataSource);
super.afterPropertiesSet();
}
}
进入方法前设置数据库名
DynamicDataSource.setDbName(ds);
方法执行完成去除数据库名
DynamicDataSource.removeDbName();
前端接口在header中添加数据库名参数,拦截器获取后设置数据库。但是存在一个问题,由于副数据库只存在所需要的数据的表,对于一些基本的配置表,或者使用其他的表的数据,切换数据源后在副数据库无法找到,接口会报错,我们之后采用aop来增强所有数据操作方法,使得一些数据表配置在配置文件中,实现灵活切换控制。
public class DynamicDataSourceInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String ds = request.getHeader("ds");
if (StringUtils.isNotBlank(ds)) {
DynamicDataSource.setDbName(ds);
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
DynamicDataSource.removeDbName();
}
}
拦截器注入到webconfig对象拦截器注册器中,其中的PATHS在配置文件中会统一配置,后面会说明。
@Configuration
public class ResourcesConfig implements WebMvcConfigurer {
final String[] excludePaths = {"/static/**","/**/login","/**/doc.html","/swagger-resources/**", "/webjars/**"
, "/v2/**", "/swagger-ui.html/**","/data/crawlerDataReport/**"};
// 使用了security之后不再使用这里的的拦截器验证jwt,而是使用security的filter进行拦截
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new DynamicDataSourceInterceptor()).addPathPatterns(MultiDbEntityProperties.PATHS);
}
业务代码统一采用的mybatis-plus持久层框架统一开发,所以会在切入点拦截mybatis-plus的接口类,具体代码如下
@Aspect
@Component
@Slf4j
public class ResetDataSourceAspect {
@Around(value = "target(service)", argNames = "joinPoint, service")
public Object reset(ProceedingJoinPoint joinPoint, IService<?> service) throws Throwable {
String entityClazzName = service.getEntityClass().getName();
if (MultiDbEntityProperties.ENTITIES.contains(entityClazzName)) {
return joinPoint.proceed();
}
String dbName = DynamicDataSource.getDbName();
DynamicDataSource.removeDbName();
Object proceed = joinPoint.proceed();
DynamicDataSource.setDbName(dbName);
return proceed;
}
}
其中ENTITIES参数与PATHS一样会在配置文件中统一配置。
以上,就实现了控制器层的数据源动态配置,使用AOP解决了副数据库中不存在公共数据表的问题。
新建properties文件放在resource文件夹下
entities=com.jsyn.model.entity.ml.Tables1,\
com.jsyn.model.entity.ml.Table2,\
com.jsyn.model.entity.ml.Table3,\
com.jsyn.model.entity.ml.Table4
paths=/jsyn/ex/trigger/dupDb
public class MultiDbEntityProperties {
private static final Logger log = LoggerFactory.getLogger(MultiDbEntityProperties.class);
private MultiDbEntityProperties() {
}
public static final Set<String> ENTITIES;
public static final List<String> PATHS;
static {
ENTITIES = new HashSet<>();
PATHS = new ArrayList<>();
InputStream inputStream = null;
try {
ClassPathResource classPathResource = new ClassPathResource("multi_db_entity.properties");
inputStream = classPathResource.getInputStream();
Properties properties = new Properties();
properties.load(inputStream);
String excludeEntities = properties.getProperty("entities");
if (StrUtil.isNotBlank(excludeEntities)) {
ENTITIES.addAll(Arrays.asList(excludeEntities.split(",")));
}
String paths = properties.getProperty("paths");
if (StrUtil.isNotBlank(paths)) {
PATHS.addAll(Arrays.asList(paths.split(",")));
}
} catch (Exception e) {
log.error("Load exclude entity properties file failed.");
} finally {
if (Objects.nonNull(inputStream)) {
try {
inputStream.close();
} catch (IOException ignore) {
}
}
}
}
}
………………