【解决方式】一种双数据源解决方案

【解决方式】一种双数据源解决方案

说明

​ 在工作中遇到特殊业务场景,但是不想对业务代码有太多的侵入,在这里记录分享给各位博友,供参考评判,有更好的方式可以评论。

问题背景

​ 业务开发过程中,需要把一部分数据表的数据拿出来做业务复盘,重新计算收益;所以就提出了几种方案,

​ (1)使用shardingsphere分库分表。

​ (2)使用shardingsphere分表。

​ (3)双数据源配置。

​ 我提出了双数据源配置的方案,在这里简单说明分享一下。

​ 项目采用的是spring-boot框架结构,数据连接池采用druid。

具体步骤
1.取消spring数据源自动配置

​ 启动类注解加配置如下:

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
2.配置application.yml数据源
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
3.多数据源连接初始化

​ 根据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();
    }
}
4.动态数据源连接路由配置

​ 注入主数据源和副数据源对象到数据源路由对象中,并与路由中的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();
    }
}
5.简单使用方法

​ 进入方法前设置数据库名

DynamicDataSource.setDbName(ds);

​ 方法执行完成去除数据库名

DynamicDataSource.removeDbName();
6.使用拦截器

​ 前端接口在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);
    }
7.使用AOP增强数据库Sevice

​ 业务代码统一采用的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解决了副数据库中不存在公共数据表的问题。

8.统一配置

​ 新建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) {

                }
            }
        }

    }
}

………………

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值