乐趣区

关于apollo:聊聊如何利用apollo与druid整合实现数据源动态热切

前言

本文的素材起源与某次和敌人技术交换,过后敌人就跟我吐槽说 apollo 不如 nacos 好用,而且他们还因为 apollo 产生过一次线上事变。

故事的背景大略是如下

前阵子敌人部门的数据库产生宕机,导致业务无奈失常操作,过后敌人他们数据库信息是配置在 apollo 上,敌人的想法是当数据库宕机时,能够通过切换配置在 apollo 上的数据库信息,实现数据源热变更。但当他们数据库产生宕机时,敌人按他的想法操作,发现事件并不像他设想的那样,他们更换数据源后,发现业务服务连贯依然是旧的数据库服务,前面没方法他们只能分割 dba 解决。

后边我听了敌人的形容后,我就问他说,你们过后数据库热切是怎么做的,他的答复是:很简略啊,就把数据源信息配置在 apollo 上,如果要变更数据源,就间接在 apollo 的 portal 上变更一下啊。听了敌人话,我就问而后呢?敌人的答复是:什么而后?就没而后了啊。

通过那次交换,就有了明天的文章,明天咱们就来聊聊 apollo 与 druid 整合实现数据源动静热切

实现外围思路

apollo 的配置变更动静监听 + spring AbstractRoutingDataSource 预留办法 determineCurrentLookupKey 来做数据源切换

在介绍实现外围逻辑之前,咱们来聊一下配置核心

何为配置核心?

配置核心是一种对立治理各种利用配置的根底服务组件。他的外围是对 配置 的对立治理。他治理的领域是配置,至于对配置有依赖的对象,比方数据源,他是不归配置核心来治理。为什么我会独自提这个?是因为敌人仿佛陷入了一个误区,认为在 apollo 上变更了配置,这个配置依赖的数据源也会一起跟着变更

外围代码

1、创立动静数据源,代理原来的 datasource

public class DynamicDataSource extends AbstractRoutingDataSource {

    public static final String DATASOURCE_KEY = "db";

    @Override
    protected Object determineCurrentLookupKey() {return DATASOURCE_KEY;}

    public DataSource getOriginalDetermineTargetDataSource(){return this.determineTargetDataSource();
    }
}
@Configuration
@EnableConfigurationProperties(BackupDataSourceProperties.class)
@ComponentScan(basePackages = "com.github.lybgeek.ds.switchover")
public class DynamicDataSourceAutoConfiguration {


    @Bean
    @ConditionalOnMissingBean
    @Primary
    @ConditionalOnClass(DruidDataSource.class)
    public AbstractDataSourceManger abstractDataSourceManger(DataSourceProperties dataSourceProperties, BackupDataSourceProperties backupDataSourceProperties){return new DruidDataSourceManger(backupDataSourceProperties,dataSourceProperties);
    }

    @Bean("dataSource")
    @Primary
    @ConditionalOnBean(AbstractDataSourceManger.class)
    public DynamicDataSource dynamicDataSource(AbstractDataSourceManger abstractDataSourceManger) {DynamicDataSource source = new DynamicDataSource();
        DataSource dataSource = abstractDataSourceManger.createDataSource(false);
        source.setTargetDataSources(Collections.singletonMap(DATASOURCE_KEY, dataSource));
        return source;
    }

}

这边有个须要留神的点就是 DynamicDataSource 的 bean 名称肯定是须要为 dataSource, 目标是为了让 spring 默认的 datasource 取到的 bean 是 DynamicDataSource

2、监听配置变更,并进行数据源切换

切换数据源

 @ApolloConfigChangeListener(interestedKeyPrefixes = PREFIX)
    public void onChange(ConfigChangeEvent changeEvent) {refresh(changeEvent.changedKeys());
    }

    /**
     *
     * @param changedKeys
     */
    private synchronized void refresh(Set<String> changedKeys) {
        /**
         * rebind configuration beans, e.g. DataSourceProperties
         * @see org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder#onApplicationEvent
         */
        this.applicationContext.publishEvent(new EnvironmentChangeEvent(changedKeys));

        /**
         * BackupDataSourceProperties rebind ,you can also do it in PropertiesRebinderEventListener
         * @see PropertiesRebinderEventListener
         */
        backupDataSourcePropertiesHolder.rebinder();

        abstractDataSourceManger.switchBackupDataSource();}
  @SneakyThrows
    @Override
    public void switchBackupDataSource() {if(backupDataSourceProperties.isForceswitch()){if(backupDataSourceProperties.isForceswitch()){log.info("Start to switch backup datasource :【{}】",backupDataSourceProperties.getBackup().getUrl());
                DataSource dataSource = this.createDataSource(true);
                DynamicDataSource source = applicationContext.getBean(DynamicDataSource.class);
                DataSource originalDetermineTargetDataSource = source.getOriginalDetermineTargetDataSource();
                if(originalDetermineTargetDataSource instanceof DruidDataSource){DruidDataSource druidDataSource = (DruidDataSource)originalDetermineTargetDataSource;
                    ScheduledExecutorService createScheduler = druidDataSource.getCreateScheduler();
                    createScheduler.shutdown();
                    if(!createScheduler.awaitTermination(backupDataSourceProperties.getAwaittermination(), TimeUnit.SECONDS)){log.warn("Druid dataSource【{}】create connection thread force to closed",druidDataSource.getUrl());
                        createScheduler.shutdownNow();}
                }
                // 当检测到数据库地址扭转时,从新设置数据源
                source.setTargetDataSources(Collections.singletonMap(DATASOURCE_KEY, dataSource));
                // 调用该办法刷新 resolvedDataSources,下次获取数据源时将获取到新设置的数据源
                source.afterPropertiesSet();

                log.info("Switch backup datasource :【{}】finished",backupDataSourceProperties.getBackup().getUrl());
            }
        }
    }

3、测试

   @Override
    public void run(ApplicationArguments args) throws Exception {while(true){User user = userService.getById(1L);
            System.err.println(user.getPassword());
            TimeUnit.SECONDS.sleep(1);
        }

    }

未切换前,控制台打印

切换后,控制台打印

总结

以上就是实现 apollo 与 druid 整合实现数据源动静热切的整体思路,然而实现中还存在有一点问题,就是存在老连贯没做解决。尽管我在示例代码中没做解决,但代码外面预留了 getOriginalDetermineTargetDataSource,能够通过 getOriginalDetermineTargetDataSource 来做额定一些操作。

本文的实现形式还能够应用 apollo 在 github 提供的 case 来实现,链接如下

https://github.com/apolloconfig/apollo-use-cases/tree/master/dynamic-datasource

他这个 case 在进行连贯切换后,会对老的数据源进行连贯清理。他外面的用数据源是 HikariDataSource,如果你用 apollo 提供的 case,当你是应用 druid 数据源时,我贴下 druid 的敞开局部源码

以及获取 connection 源码

这边有个留神点就是,当 druid 数据源进行敞开时,如果此时恰好有连贯进来,此时就会报 DataSourceDisableException,而后导致我的项目异样退出

最初说点额定的,之前敌人说 apollo 比 nacos 不好用啥的,我是持保留意见的,其实掂量一个技术的好坏,是要带上场景的,在某些场景,技术的具备的劣势可能反而成了劣势

demo 链接

https://github.com/lyb-geek/springboot-learning/tree/master/springboot-datasource-hot-switchover

退出移动版