头图

Spring Cloud中MyBatis-Plus动态数据源刷新问题

一、问题场景描述

在使用MyBatis-Plus的DynamicRoutingDataSource时遇到的问题,当我在配置中心动态增加或者删除了一个数据源,他并不会自动同步最新的数据源,导致我用DynamicDataSourceContextHolder.push(ds)方法的时候拿不到刚添加的数据源

二、问题产生的原因

在Spring Cloud中刷新Bean,官方提供了@RefreshScope注解用于Bean的刷新,然而DynamicDataSourceProperties并没有该注解,而且dynamic-datasource-spring-boot-starter并没有实现获取刷新后的配置重新加入DynamicRoutingDataSource

三、源码分析

dynamic-datasource-spring-boot-starter的核心自动配置类为DynamicDataSourceAutoConfiguration,在该自动配置类中会初始化DynamicRoutingDataSource Bean对象,该对象实现了InitializingBean方法:

    @Override
    public void afterPropertiesSet() throws Exception {
        // 检查开启了配置但没有相关依赖
        checkEnv();
        // 添加并分组数据源
        Map<String, DataSource> dataSources = new HashMap<>(16);
        for (DynamicDataSourceProvider provider : providers) {
            dataSources.putAll(provider.loadDataSources());
        }
        for (Map.Entry<String, DataSource> dsItem : dataSources.entrySet()) {
            addDataSource(dsItem.getKey(), dsItem.getValue());
        }
        // 检测默认数据源是否设置
        if (groupDataSources.containsKey(primary)) {
            log.info("dynamic-datasource initial loaded [{}] datasource,primary group datasource named [{}]", dataSources.size(), primary);
        } else if (dataSourceMap.containsKey(primary)) {
            log.info("dynamic-datasource initial loaded [{}] datasource,primary datasource named [{}]", dataSources.size(), primary);
        } else {
            log.warn("dynamic-datasource initial loaded [{}] datasource,Please add your primary datasource or check your configuration", dataSources.size());
        }
    }

可以看到在此处调用了addDataSource方法

    public synchronized void addDataSource(String ds, DataSource dataSource) {
        DataSource oldDataSource = dataSourceMap.put(ds, dataSource);
        // 新数据源添加到分组
        this.addGroupDataSource(ds, dataSource);
        // 关闭老的数据源
        if (oldDataSource != null) {
            closeDataSource(ds, oldDataSource);
        }
        log.info("dynamic-datasource - add a datasource named [{}] success", ds);
    }
    /**
     * 新数据源添加到分组
     *
     * @param ds         新数据源的名字
     * @param dataSource 新数据源
     */
    private void addGroupDataSource(String ds, DataSource dataSource) {
        if (ds.contains(UNDERLINE)) {
            String group = ds.split(UNDERLINE)[0];
            GroupDataSource groupDataSource = groupDataSources.get(group);
            if (groupDataSource == null) {
                try {
                    groupDataSource = new GroupDataSource(group, strategy.getDeclaredConstructor().newInstance());
                    groupDataSources.put(group, groupDataSource);
                } catch (Exception e) {
                    throw new RuntimeException("dynamic-datasource - add the datasource named " + ds + " error", e);
                }
            }
            groupDataSource.addDatasource(ds, dataSource);
        }
    }

除此之外,别的地方再也找不到调用该方法的源码了,至此可以得出结论,DynamicRoutingDataSource只有在程序刚启动的时候会读取配置中心的配置并加入到数据源Map中,此后新加入的数据源,或者删掉某一个数据源并不会生效

四、解决思路

既然官方没有实现自动刷新,那自己构造一个能刷新的配置不就行了吗?代码如下:

@Data
@RefreshScope
@ConfigurationProperties(DynamicDataSourceProperties.PREFIX)
public class RefreshableDynamicDataSourceProperties implements DisposableBean {

    /**
     * 每一个数据源
     */
    private Map<String, DataSourceProperty> datasource = new LinkedHashMap<>();

    @Override
    public void destroy() throws Exception {
        datasource = new LinkedHashMap<>();
    }
}

注:此处一定要实现DisposableBean接口,并重置数据源,否则在配置中心删除数据源时没法正常删除,这是由于Spring源码中判断Bean的属性如果是复合类型如Map做的操作是putAll()操作,也就是说不会删除原来配置。

上面只是完成了第一步,下面还要将咱们自己的配置在配置刷新的时候加入到DynamicRoutingDataSource中,那么此时需要一个监听器去监听配置刷新的事件,代码如下:

@Order(0)
@Configuration
@RequiredArgsConstructor
@ConditionalOnClass(DynamicDataSourceAutoConfiguration.class)
@EnableConfigurationProperties(RefreshableDynamicDataSourceProperties.class)
@AutoConfigureAfter({DataSourceAutoConfiguration.class, DynamicDataSourceAutoConfiguration.class})
public class DynamicDataSourceConfig implements ApplicationListener<RefreshScopeRefreshedEvent> {
    private final RefreshableDynamicDataSourceProperties properties;
    private final DataSource dataSource;
    private final DefaultDataSourceCreator creator;

    @Override
    public void onApplicationEvent(RefreshScopeRefreshedEvent event) {
        //获取最新的数据源
        Map<String, DataSourceProperty> datasource = properties.getDatasource();
        //获取原来的数据源
        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
        //移除当前数据源中不存在的数据源
        Set<String> keys = datasource.keySet();
        //判断是否存在(不存在即删除)
        ds.getDataSources().entrySet().removeIf(next -> !keys.contains(next.getKey()));
        //添加新的数据源
        datasource.forEach((key, value) -> {
            ds.addDataSource(key, creator.createDataSource(value));
        });
    }
}

此时就实现了不重启工程的情况下动态应用配置中心数据源了。

五、总结

官方没有实现自动刷新可能有自己的考量吧,这里我主要记录一下自己的解决思路和方案,毕竟技术是随着需求去应用的,而不是为了技术而技术。

对技术抱有热情,对工作保持严谨!

7 声望
3 粉丝
0 条评论
推荐阅读
SpringBoot微信扫码登录(小程序版)
一、需求描述用户在PC端用微信扫描二维码实现后台登录图示:二、实现原理此处采用socket实现,当然也可以通过轮询去监测微信扫码状态三、实现步骤1. 微信公众平台小程序后台申请跳转链接开发管理-&gt;开发设置-&g...

Pursuer丶阅读 757

封面图
刨根问底 Redis, 面试过程真好使
充满寒气的互联网如何在面试中脱颖而出,平时积累很重要,八股文更不能少!下面带来的这篇 Redis 问答希望能够在你的 offer 上增添一把🔥。

菜农曰17阅读 973

封面图
PHP转Go实践:xjson解析神器「开源工具集」
我和劲仔都是PHP转Go,身边越来越多做PHP的朋友也逐渐在用Go进行重构,重构过程中,会发现php的json解析操作(系列化与反序列化)是真的香,弱类型语言的各种隐式类型转换,很大程度的减低了程序的复杂度。

王中阳Go10阅读 2k评论 3

封面图
万字详解,吃透 MongoDB!
MongoDB 是一个基于 分布式文件存储 的开源 NoSQL 数据库系统,由 C++ 编写的。MongoDB 提供了 面向文档 的存储方式,操作起来比较简单和容易,支持“无模式”的数据建模,可以存储比较复杂的数据类型,是一款非常...

JavaGuide5阅读 847

封面图
计算机网络连环炮40问
本文已经收录到Github仓库,该仓库包含计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享等核心知识点,欢迎star~

程序员大彬8阅读 1.1k

与RabbitMQ有关的一些知识
工作中用过一段时间的Kafka,不过主要还是RabbitMQ用的多一些。今天主要来讲讲与RabbitMQ相关的一些知识。一些基本概念,以及实际使用场景及一些注意事项。

lpe2348阅读 1.9k

封面图
spring boot 锁
由于当前的项目中由于多线程操作同一个实体,会出现数据覆盖的问题,后保存的实体把先保存的实体的数据给覆盖了。于是查找了锁的实现的几种方式。但写到最后发现,其实自己可以写sql 更新需要更新的字段即可,这...

weiewiyi3阅读 9.2k

对技术抱有热情,对工作保持严谨!

7 声望
3 粉丝
宣传栏