微服务实现跨数据中心进行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调用。

浙公网安备 33010602011771号