自动切换数据源别名

93 阅读3分钟

一、前言

由于数据量较大,直接查询数据库无法满足性能要求,因此我们将数据进行预组装,即将检索、排序需要的数据导入至 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();
    }