一、前言
由于数据量较大,直接查询数据库无法满足性能要求,因此我们将数据进行预组装,即将检索、排序需要的数据导入至 es 中,使用 mongo 作为数据仓库来提供详情数据。在这样的场景下,数据导入完成后总是需要切换 es 索引别名与 mongo 的别名(自实现),为了防止开发小伙伴忘记提供切换脚本或者运维忘记操作,因此我们希望可以自动切换数据源的别名,从而解放开发小伙伴们的双手,实现运维自动化。
二、任务拆解
如想实现该需求,大致分为两步:
- 如何在服务重启时做到自定义实现;
- 如何实现切换别名
- 如何切换 es 别名;
- 如何切换 mongo 别名;
- 能否批量操作;
- 切换别名是否需要考虑原子性。
那么,步骤一自然延伸至如何在spring容器启动时实现自定义功能?
可以利用 @PostConstruct 注解实现容器启动时自定义功能。
如何切换 es 别名呢,可利用 es 的 _aliases 操作实现多索引的别名切换。
如何切换 mongo 别名呢,此为自定义实现(通过 AOP 拦截查询,将别名替换成实际的集合名),即批量修改 mongo 集合中数据内容,利用 mongo 的 BulkOperations 实现批量更新。
原子性,自然想到 spring 的事务,利用@Transactional注解,实现 mongo 及 es 插入失败时回滚。
so,接下来开始实现吧。
三、设计与实现
3.1.配置文件提取
由于消费者服务实现数据的预组装需要知道往哪个对应的 es 和 mongo 中进行导入,而查询服务需要知道真实的数据源名称和别名是什么,自然就涉及到配置的公共提取。
由于我们使用的是 Nacos 作为配置管理及注册中心,此时只需通过 Nacos 的 shared-configs 实现配置的公共监听,因此,新增common-source-name.yml实现配置公共化管理。
此时问题来了,消费者服务中需要引入对应配置,而查询服务只需要知道对应模块的数据源名称与别名即可,如何在查询服务提取到公共内容从而切换呢?让开发者每加一个模块,new 一个对应模块的公共对象,然后手动往list中放置? 此时利用 Map 结构即可解决。
@Component
@ConfigurationProperties(prefix = "xx.common")
@Data
@RefreshScope
public class NacosConfig {
private CommonSourceNacosConfig commonSource;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CommonSourceNacosConfig {
private Map<String, AliasConfig> detail;
public List<AliasRequest> buildAliasRequestList() {
return detail.values().stream()
.filter(aliasConfig -> StringUtils.isNoneBlank(aliasConfig.getEsIndexName(), aliasConfig.getEsIndexAlias()
, aliasConfig.getMongoAlias(), aliasConfig.getMongoName()))
.map(aliasConfig -> new AliasRequest(aliasConfig.getMongoAlias(), aliasConfig.getMongoName()
, aliasConfig.getEsIndexAlias(), aliasConfig.getEsIndexName()))
.collect(Collectors.toList());
}
}
3.2.服务启动监听
/**
* 刷新数据源别名,例如ES、Mongo
*
*/
@Configuration
@Slf4j
public class RefreshAliasListener {
@Resource
private AliasServiceApi aliasServiceApi;
@Resource
private NacosConfig nacosConfig;
@PostConstruct
public void afterPropertiesSet() {
log.info("refresh alias start...");
//1.公共配置中获取数据源名称及对应es、mongo别名
CommonSourceNacosConfig commonSource = nacosConfig.getCommonSource();
if (Objects.isNull(commonSource)) {
return;
}
List<AliasRequest> aliasRequestList = commonSource.buildAliasRequestList();
if (CollectionUtils.isEmpty(aliasRequestList)) {
return;
}
//2.调用systemManager服务进行更新
try {
aliasServiceApi.listUpsert(aliasRequestList);
} catch (Exception e) {
log.error("refresh alias error", e);
}
log.info("refresh alias end...");
}
}
3.3.刷新别名
@Slf4j
@Service
public class AliasServiceImpl implements AliasService {
@Resource
private MongoTemplate mongoTemplate;
@Resource
private NacosConfigManager nacosConfigManager;
@Resource
private ElasticsearchService elasticsearchService;
@Value("${spring.data.mongodb.collection.mongoAlias:ep_mongo_alias}")
private String collectionName;
public static final String DATA_ID_MONGO_ALIAS = "mongo-alias-timestamp.txt";
@Override
@Transactional(rollbackFor = RuntimeException.class)
public void listUpsert(List<AliasRequest> aliasList) {
//切换 mongo 别名
BulkOperations bulkOperations = mongoTemplate.bulkOps(BulkOperations.BulkMode.UNORDERED, collectionName);
List<Pair<Query, Update>> upsertList = aliasList.stream().map(aliasRequest -> {
Query query = Query.query(Criteria.where("_id").is(aliasRequest.getMongoAlias()));
Update update = Update.update("collectionName", aliasRequest.getMongoName());
return Pair.of(query, update);
}).collect(Collectors.toList());
bulkOperations.upsert(upsertList);
BulkWriteResult writeResult = bulkOperations.execute();
//若插入失败抛出异常,回滚
if (!writeResult.wasAcknowledged()) {
log.error("Failed to send data to {} ,reason:{}", collectionName, writeResult);
throw new SystemManageException(SystemManageExceptionEnum.ALIAS_UPDATE_ERROR);
}
//切换 es 别名,若失败则抛出异常
try {
elasticsearchService.listUpdateAlias(aliasList);
ConfigService configService = nacosConfigManager.getConfigService();
String mongoAlias = aliasList.stream().map(AliasRequest::getMongoAlias).collect(Collectors.joining(StrPool.COMMA));
// 通知本地缓存清除
String cacheKey = String.format("%s:%d", mongoAlias, System.currentTimeMillis());
configService.publishConfig(DATA_ID_MONGO_ALIAS, GROUP_ID, cacheKey);
} catch (IOException | NacosException e) {
log.error("refresh es alias error", e);
throw new SystemManageException(SystemManageExceptionEnum.ALIAS_UPDATE_ERROR);
}
}
...
@Override
public Boolean listUpdateAlias(List<AliasRequest> aliasList) throws IOException {
Set<String> esAliasSet = aliasList.stream().map(AliasRequest::getEsIndexAlias).collect(Collectors.toSet());
Map<String, Set<AliasMetadata>> indexNameMap = getIndexByAliasName(esAliasSet.toArray(new String[]{}));
IndicesAliasesRequest indicesAliasesRequest = new IndicesAliasesRequest();
aliasList.forEach(aliasRequest -> {
IndicesAliasesRequest.AliasActions addIndexAction = new IndicesAliasesRequest
.AliasActions(IndicesAliasesRequest.AliasActions.Type.ADD).index(aliasRequest.getEsIndexName())
.alias(aliasRequest.getEsIndexAlias());
if (!MapUtil.isEmpty(indexNameMap)) {
indexNameMap.entrySet().stream().filter(entry -> entry.getValue().stream().anyMatch(aliasMetadata ->
StringUtils.equals(aliasMetadata.getAlias(), aliasRequest.getEsIndexAlias()))).findFirst().ifPresent(entry -> {
IndicesAliasesRequest.AliasActions removeAction = new IndicesAliasesRequest
.AliasActions(IndicesAliasesRequest.AliasActions.Type.REMOVE).index(entry.getKey()).alias(aliasRequest.getEsIndexAlias());
indicesAliasesRequest.addAliasAction(removeAction);
});
}
indicesAliasesRequest.addAliasAction(addIndexAction);
});
AcknowledgedResponse indicesAliasesResponse = restHighLevelClient.indices().updateAliases(indicesAliasesRequest, RequestOptions.DEFAULT);
if (!indicesAliasesResponse.isAcknowledged()) {
throw new SystemManageException(SystemManageExceptionEnum.ALIAS_UPDATE_ERROR);
}
return true;
}
@Override
public Map<String, Set<AliasMetadata>> getIndexByAliasName(String... aliasName) throws IOException {
GetAliasesRequest request = new GetAliasesRequest(aliasName);
GetAliasesResponse getAliasesResponse = restHighLevelClient.indices().getAlias(request, RequestOptions.DEFAULT);
return getAliasesResponse.getAliases();
}