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