微服务实现跨数据中心进行Feign调用

最近在做微服务不停服迁移,为了保证服务在迁移过程中不影响系统的正常使用,还要保证客户无感迁移。

所以在两个机房搭建了Consul注册中心服务,使用官网推荐的模式进行搭建,搭建步骤省略,仅针对应用服务的改造:

原Consul是支持跨数据中心调用的,但是需要通过配置来指定目标服务所在的数据中心,如果是个别服务,推荐使用官方的配置方式,如下配置:

spring:
  cloud:
    consul:
      discovery:
        datacenters:
          service-A: dc1
          service-B: dc2

1、需要在配置中心application,env/data主配置中添加以下内容:

spring:
  cloud:
    consul:
      discovery:
        metadata:
          default-dc: dc1 # 默认的注册中心
        datacenters:
          - dc1
          - dc2

2、添加配置类:

⚠️注意:不同的SpringBoot版本,需要添加不同的配置类:

如果微服务通过spring-cloud-gateway进行路由调用,可以在gateway项目中引入下面2.3.12.RELEASE之后对应的配置,实现网关跨数据中心路由服务。

/**
 * SpringBoot-2.3.12.RELEASE及之前的版本
 */
@Configuration
@ConditionalOnConsulEnabled
@ConditionalOnProperty(name = "spring.cloud.consul.ribbon.enabled", havingValue = "false", matchIfMissing = false)
@RibbonClients(defaultConfiguration = CustomRibbonConsulAutoConfiguration.class)
public class CommonConfig {

}


~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


public class CustomConsulServerList extends ConsulServerList {

    private Logger logger = LoggerFactory.getLogger(CustomConsulServerList.class);

    private final ConsulClient client;
    private final ConsulDiscoveryProperties properties;
    private String serviceId;

    public CustomConsulServerList(ConsulClient client,
        ConsulDiscoveryProperties properties) {
        super(client, properties);
        this.client = client;
        this.properties = properties;
    }

    protected ConsulClient getClient() {
        return this.client;
    }

    protected ConsulDiscoveryProperties getProperties() {
        return this.properties;
    }

    protected String getServiceId() {
        return this.serviceId;
    }

    public void initWithNiwsConfig(IClientConfig clientConfig) {
        this.serviceId = clientConfig.getClientName();
    }

    public List<ConsulServer> getInitialListOfServers() {
        return this.getServers();
    }

    public List<ConsulServer> getUpdatedListOfServers() {
        return this.getServers();
    }

    private List<ConsulServer> getServers() {
        if (this.client == null) {
            return Collections.emptyList();
        } else {
            HealthServicesRequest request = HealthServicesRequest.newBuilder().setTag(this.getTag()).setPassing(this.properties.isQueryPassing()).setQueryParams(this.createQueryParamsForClientRequest()).setToken(this.properties.getAclToken()).build();
            Response<List<HealthService>> response = this.client.getHealthServices(this.serviceId, request);
            // 如果获取不到服务实例,则尝试获取其他数据中心下的服务实例
            if (response == null || CollectionUtils.isEmpty(response.getValue())) {
                Collection<String> values = this.properties.getDatacenters().values();
                Collection<String> values1 = this.properties.getMetadata().values();
                if (CollectionUtils.isNotEmpty(values) && CollectionUtils.isNotEmpty(values1)) {
                    Collection<String> disjunction = CollectionUtils.disjunction(values, values1);
                    if (CollectionUtils.isNotEmpty(disjunction)) {
                        AtomicReference<List<HealthService>> result = new AtomicReference<>();
                        disjunction.forEach(dc -> {
                            try {
                                HealthServicesRequest requests = HealthServicesRequest.newBuilder()
                                    .setTag(this.properties.getDefaultQueryTag())
                                    .setPassing(this.properties.isQueryPassing())
                                    .setQueryParams(
                                        QueryParams.Builder.builder()
                                            .setDatacenter(dc)
                                            .build())
                                    .setToken(this.properties.getAclToken()).build();
                                Response<List<HealthService>> serviceList = client.getHealthServices(serviceId, requests);
                                if (serviceList != null && CollectionUtils.isNotEmpty(serviceList.getValue())) {
                                    synchronized (result) { // 确保线程安全
                                        if (result.get() == null) { // 防止被后续非空结果覆盖
                                            result.set(serviceList.getValue());
                                        }
                                    }
                                }
                            } catch (Exception e) {
                                logger.warn("查询其他数据中心 {} 的健康服务时发生异常", dc, Throwables.getStackTraceAsString(e));
                            }
                        });
                        if (result.get() != null) {
                            return this.transformResponse(result.get());
                        }
                    }
                }
            }
            return response.getValue() != null && !((List)response.getValue()).isEmpty() ? this.transformResponse((List)response.getValue()) : Collections.emptyList();
        }
    }

    protected List<ConsulServer> transformResponse(List<HealthService> healthServices) {
        List<ConsulServer> servers = new ArrayList();

        ConsulServer server;
        for(Iterator var3 = healthServices.iterator(); var3.hasNext(); servers.add(server)) {
            HealthService service = (HealthService)var3.next();
            server = new ConsulServer(service, this.properties.isTagsAsMetadata());
            if (server.getMetadata().containsKey(this.properties.getDefaultZoneMetadataName())) {
                server.setZone((String)server.getMetadata().get(this.properties.getDefaultZoneMetadataName()));
            }
        }

        return servers;
    }

    protected QueryParams createQueryParamsForClientRequest() {
        String datacenter = this.getDatacenter();
        return datacenter != null ? new QueryParams(datacenter, this.properties.getConsistencyMode()) : new QueryParams(this.properties.getConsistencyMode());
    }

    protected String getTag() {
        return this.properties.getQueryTagForService(this.serviceId);
    }

    protected String getDatacenter() {
        String datacenter = this.properties.getDatacenters().get(this.serviceId);
        if (StringUtils.isBlank(datacenter)) {
            datacenter = this.properties.getMetadata().get("default-dc");
        }
        return datacenter;
    }

    public String toString() {
        StringBuilder sb = new StringBuilder("ConsulServerList{");
        sb.append("serviceId='").append(this.serviceId).append('\'');
        sb.append(", tag=").append(this.getTag());
        sb.append('}');
        return sb.toString();
    }
}


~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties
@ConditionalOnConsulEnabled
@ConditionalOnBean(SpringClientFactory.class)
@AutoConfigureAfter(RibbonAutoConfiguration.class)
public class CustomRibbonConsulAutoConfiguration {

    @Autowired
    private ConsulClient client;

    @Value("${ribbon.client.name}")
    private String serviceId = "client";

    public CustomRibbonConsulAutoConfiguration() {
    }

    public CustomRibbonConsulAutoConfiguration(String serviceId) {
        this.serviceId = serviceId;
    }

    @Bean
    @ConditionalOnMissingBean
    public ServerList<?> ribbonServerList(IClientConfig config,
        ConsulDiscoveryProperties properties) {
        ConsulServerList serverList = new CustomConsulServerList(this.client, properties);
        serverList.initWithNiwsConfig(config);
        return serverList;
    }
}
/**
 * SpringBoot-2.3.12.RELEASE之后
 */
@Configuration
public class CrossDCDiscoveryClientConfig {

    /**
     * 创建自定义的注册中心服务
     * @param client
     * @param discoveryProperties
     * @return
     */
    @Bean
    public ConsulReactiveDiscoveryClient consulReactiveDiscoveryClient(
        ConsulClient client, ConsulDiscoveryProperties discoveryProperties) {
        return new CustomConsulReactiveDiscoveryClient(client, discoveryProperties);
    }

}


~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


public class CustomConsulReactiveDiscoveryClient extends ConsulReactiveDiscoveryClient {

    private static final Logger logger = LoggerFactory.getLogger(CustomConsulReactiveDiscoveryClient.class);

    private final ConsulClient client;

    private final ConsulDiscoveryProperties properties;

    public CustomConsulReactiveDiscoveryClient(ConsulClient client,
        ConsulDiscoveryProperties properties) {
        super(client, properties);
        this.client = client;
        this.properties = properties;
    }

    @Override
    public String description() {
        return "Custom Spring Cloud Consul Reactive Discovery Client";
    }

    @Override
    public Flux<ServiceInstance> getInstances(String serviceId) {
        return Flux.defer(() -> {
            List<ServiceInstance> instances = new ArrayList<>();
            for (HealthService healthService : getHealthServices(serviceId)) {
                instances.add(new CustomConsulServiceInstance(healthService, serviceId));
            }
            return Flux.fromIterable(instances);
        }).onErrorResume(exception -> {
            logger.error("Error getting instances from Consul.", exception);
            return Flux.empty();
        }).subscribeOn(Schedulers.boundedElastic());
    }

    private List<HealthService> getHealthServices(String serviceId) {
        //HealthServicesRequest.Builder requestBuilder = HealthServicesRequest.newBuilder()
        //    .setPassing(properties.isQueryPassing()).setQueryParams(QueryParams.DEFAULT)
        //    .setToken(properties.getAclToken());
        //String[] queryTags = properties.getQueryTagsForService(serviceId);
        //if (queryTags != null) {
        //    requestBuilder.setTags(queryTags);
        //}
        //HealthServicesRequest request = requestBuilder.build();

        HealthServicesRequest request = HealthServicesRequest.newBuilder()
            .setTag(this.properties.getDefaultQueryTag())
            .setPassing(this.properties.isQueryPassing())
            .setQueryParams(QueryParams.DEFAULT)
            .setToken(this.properties.getAclToken()).build();

        Response<List<HealthService>> services = client.getHealthServices(serviceId, request);
        // 如果获取不到服务实例,则尝试获取其他数据中心下的服务实例
        if (services == null || CollectionUtils.isEmpty(services.getValue())) {
            Collection<String> values = this.properties.getDatacenters().values();
            Collection<String> values1 = this.properties.getMetadata().values();
            if (CollectionUtils.isNotEmpty(values) && CollectionUtils.isNotEmpty(values1)) {
                Collection<String> disjunction = CollectionUtils.disjunction(values, values1);
                if (CollectionUtils.isNotEmpty(disjunction)) {
                    AtomicReference<List<HealthService>> result = new AtomicReference<>();
                    disjunction.forEach(dc -> {
                        try {
                            HealthServicesRequest requests = HealthServicesRequest.newBuilder()
                                .setTag(this.properties.getDefaultQueryTag())
                                .setPassing(this.properties.isQueryPassing())
                                .setQueryParams(
                                    QueryParams.Builder.builder()
                                        .setDatacenter(dc)
                                        .build())
                                .setToken(this.properties.getAclToken()).build();
                            Response<List<HealthService>> serviceList = client.getHealthServices(serviceId, requests);
                            if (serviceList != null && CollectionUtils.isNotEmpty(serviceList.getValue())) {
                                synchronized (result) { // 确保线程安全
                                    if (result.get() == null) { // 防止被后续非空结果覆盖
                                        result.set(serviceList.getValue());
                                    }
                                }
                            }
                        } catch (Exception e) {
                            logger.warn("查询其他数据中心 {} 的健康服务时发生异常", dc, Throwables.getStackTraceAsString(e));
                        }
                    });
                    if (result.get() != null) {
                        return result.get();
                    }
                }
            }
        }
        return services == null ? Collections.emptyList() : services.getValue();
    }

    @Override
    public Flux<String> getServices() {
        return Flux.defer(() -> {
            CatalogServicesRequest request = CatalogServicesRequest.newBuilder().setToken(properties.getAclToken())
                .setQueryParams(QueryParams.DEFAULT).build();
            Response<Map<String, List<String>>> services = client.getCatalogServices(request);
            return services == null ? Flux.empty() : Flux.fromIterable(services.getValue().keySet());
        }).onErrorResume(exception -> {
            logger.error("Error getting services from Consul.", exception);
            return Flux.empty();
        }).subscribeOn(Schedulers.boundedElastic());
    }

    @Override
    public int getOrder() {
        return properties.getOrder();
    }

}


~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


public class CustomConsulServiceInstance extends DefaultServiceInstance {

    private HealthService healthService;

    public CustomConsulServiceInstance(HealthService healthService, String serviceId) {
        this(healthService.getService().getId(), serviceId, findHost(healthService),
            getPort(healthService.getService()), getSecure(healthService), getMetadata(healthService),
            healthService.getService().getTags());
        this.healthService = healthService;
    }

    public CustomConsulServiceInstance(String instanceId, String serviceId, String host, int port, boolean secure,
        Map<String, String> metadata, List<String> tags) {
        super(instanceId, serviceId, host, port, secure, metadata);
    }

    public CustomConsulServiceInstance(String instanceId, String serviceId, String host, int port, boolean secure) {
        super(instanceId, serviceId, host, port, secure);
    }

    //public CustomConsulServiceInstance() {
    //}

    private static Map<String, String> getMetadata(HealthService healthService) {
        Map<String, String> metadata = healthService.getService().getMeta();
        if (metadata == null) {
            metadata = new LinkedHashMap<>();
        }
        return metadata;
    }

    private static int getPort(HealthService.Service service) {
        Integer port = service.getPort();
        // Services without registered port receive '0' from consul 1.9 and null from
        // consul 1.10
        if (port == null) {
            return 0;
        }
        return port;
    }

    private static boolean getSecure(HealthService healthService) {
        boolean secure = false;
        Map<String, String> metadata = getMetadata(healthService);
        // getMetadata() above returns an empty Map if meta is null
        if (metadata.containsKey("secure")) {
            secure = Boolean.parseBoolean(metadata.get("secure"));
        }
        return secure;
    }

    public HealthService getHealthService() {
        return this.healthService;
    }

    public void setHealthService(HealthService healthService) {
        this.healthService = healthService;
    }

    public List<String> getTags() {
        if (healthService != null) {
            return healthService.getService().getTags();
        }
        return Collections.emptyList();
    }

    @Override
    public String toString() {
        return new ToStringCreator(this).append("instanceId", getInstanceId()).append("serviceId", getServiceId())
            .append("host", getHost()).append("port", getPort()).append("secure", isSecure())
            .append("metadata", getMetadata()).append("uri", getUri()).append("healthService", healthService)
            .toString();

    }
}

3、修改启动JVM参数:

  添加--spring.cloud.consul.ribbon.enabled=false

 

至此,完美实现跨数据中心Feign调用。

posted @ 2025-07-15 18:44  Commissar-Xia  阅读(33)  评论(0)    收藏  举报