聊聊如何利用apollo與druid整合實現資料來源動態熱切

linyb極客之路發表於2023-02-01

前言

本文的素材來源與某次和朋友技術交流,當時朋友就跟我吐槽說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

相關文章