1.缘起
源自一个匪夷所思的需求:系统需要频繁采集大量(单表可能超过1亿)外部数据做审核,由于单机mysql自身限制,数据量大了之后查询一次都是问题,因此准备改造成处理一批,归档一批,也分析过分库分表分区方案,这里不在对比,有时间的话再开一篇讨论。同时,可以随时切换到归档库查看历史数据,并且还可以在归档库继续办业务,还可以对归档库归档。
2.基础框架
- springboot 2.6.2
- dynamic-datasource-spring-boot-starter 4.2.0
- mybatis 2.2.0
- spring-cloud-alibaba 2021.1
- druid 1.2.20
其他的就不再啰嗦,需要主要springcloud-alibaba和springboot的版本对应,可以去官网查看。
3.设计思路
- 用户输入归档使用的新库名(推荐默认值:原库_{年月日})
- 备份整个库
- 生成备份记录表(主要记录:原库名、新库名、时间等)
- 归档库仍归档到原服务器上(至关重要),当然,也可以完整的实现数据源配置入库,略复杂,考虑安全性
- 提供【切换数据库】操作
4.目标
系统运行过程中,本身master数据源查的是mos库,需要通过点击【切换数据库】按钮,将master数据源切换为查询mos_20240421库。即仅修改数据库名,其他配置信息依然使用原来的。
5.实现原理
由于我们本来就使用了多数据源,多数据源的原理是系统启动的时候已将数据源初始化完成,将多个数据源存放到DynamicRoutingDataSource中,可以跟进断点,看到最终使用的数据源是DruidDataSource,因此,只需修改DynamicRoutingDataSource中master对应的jdbcurl中的dbname就可以了。
示例
@RequestMapping("change/{db}")
public void changeDataSource(@PathVariable String db) {
String newDatabase = "1".equals(db) ? "mos" : "mos_bak";
Map<String, DynamicRoutingDataSource> dataSourceMap = beanFactory.getBeansOfType(DynamicRoutingDataSource.class);
for (Map.Entry<String, DynamicRoutingDataSource> entry : dataSourceMap.entrySet()) {
DynamicRoutingDataSource dynamicRoutingDataSource = entry.getValue();
Map<String, DataSource> stringDataSourceMap = dynamicRoutingDataSource.getDataSources();
for (Map.Entry<String, DataSource> dataSourceEntry : stringDataSourceMap.entrySet()) {
String datasourceKey = dataSourceEntry.getKey();
ItemDataSource itemDataSource = (ItemDataSource) dataSourceEntry.getValue();
DruidDataSource druidDataSource = (DruidDataSource) itemDataSource.getDataSource();
if ("master".equals(datasourceKey)) {
DruidDataSource newDatasource = druidDataSource.cloneDruidDataSource();
String jdbcUrl = newDatasource.getRawJdbcUrl();
newDatasource.setUrl(replaceDatabase(jdbcUrl, newDatabase));
itemDataSource.setDataSource(newDatasource);
stringDataSourceMap.put("master", itemDataSource);
}
}
}
}
private static String replaceDatabase(String jdbcUrlString, String newDatabase){
String oldDatabaseName = currentDatabaseName(jdbcUrlString);
return jdbcUrlString.replace(oldDatabaseName,newDatabase);
}
private static String currentDatabaseName(String jdbcUrlString){
String[] a = jdbcUrlString.split("\\?");
return a[0].substring(a[0].lastIndexOf("/")+1);
}
@RequestMapping("get")
public AjaxResult currentDataSource(){
Map<String,String> result = new HashMap<>();
Map<String, DynamicRoutingDataSource> dataSourceMap = beanFactory.getBeansOfType(DynamicRoutingDataSource.class);
for (Map.Entry<String, DynamicRoutingDataSource> entry : dataSourceMap.entrySet()) {
DynamicRoutingDataSource dynamicRoutingDataSource = entry.getValue();
Map<String, DataSource> stringDataSourceMap = dynamicRoutingDataSource.getDataSources();
for (Map.Entry<String, DataSource> dataSourceEntry : stringDataSourceMap.entrySet()) {
String datasourceKey = dataSourceEntry.getKey();
ItemDataSource itemDataSource = (ItemDataSource) dataSourceEntry.getValue();
DruidDataSource druidDataSource = (DruidDataSource) itemDataSource.getDataSource();
result.put(datasourceKey,currentDatabaseName(druidDataSource.getRawJdbcUrl()));
}
}
return AjaxResult.success(result);
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = (DefaultListableBeanFactory) beanFactory;
}