前言

本文的素材起源与某次和敌人技术交换,过后敌人就跟我吐槽说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