24-Spring Cloud 服务注册发现详解

5 阅读5分钟

Spring Cloud 服务注册发现详解

一、知识概述

服务注册与发现是微服务架构的核心基础设施,它解决了服务实例动态变化时的地址管理问题。Spring Cloud 提供了多种服务注册中心的支持,包括 Eureka、Nacos、Consul 等。

服务注册发现的核心概念:

  • 服务注册:服务启动时向注册中心注册自己的地址
  • 服务发现:消费者从注册中心获取服务提供者的地址
  • 健康检查:注册中心定期检查服务实例的健康状态
  • 服务续约:服务实例定期向注册中心发送心跳

理解服务注册发现的原理,是构建微服务架构的基础。

二、知识点详细讲解

2.1 服务注册发现架构

服务提供者                注册中心                服务消费者
    │                       │                       │
    │──── 注册 ────────────>│                       │
    │                       │                       │
    │<─── 心跳 ────────────>│                       │
    │                       │                       │
    │                       │<──── 发现 ────────────│
    │                       │                       │
    │                       │───── 地址列表 ───────>│
    │                       │                       │
    │<──────────────────── 直接调用 ───────────────>│

2.2 注册中心对比

特性EurekaNacosConsul
CAPAPAP/CPCP
健康检查心跳心跳/HTTP/TCPHTTP/TCP
配置中心
多数据中心
一致性最终一致可选强一致

2.3 Eureka 核心概念

Eureka Server
  • 服务注册中心
  • 维护服务注册表
  • 提供服务发现接口
Eureka Client
  • 服务提供者:注册服务、发送心跳
  • 服务消费者:获取服务列表、调用服务
核心配置
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
    register-with-eureka: true  # 是否注册自己
    fetch-registry: true        # 是否获取注册表
  instance:
    hostname: localhost
    lease-renewal-interval-in-seconds: 30  # 心跳间隔
    lease-expiration-duration-in-seconds: 90  # 过期时间

2.4 Nacos 核心概念

命名空间(Namespace)
  • 环境隔离(开发、测试、生产)
  • 租户隔离
分组(Group)
  • 服务分组
  • 默认为 DEFAULT_GROUP
服务(Service)
  • 服务名称
  • 服务实例列表
实例(Instance)
  • IP:Port
  • 权重
  • 健康状态
  • 元数据

2.5 服务注册流程

1. 服务启动
    ↓
2. 读取配置(注册中心地址、服务名称)
    ↓
3. 向注册中心发送注册请求
    ↓
4. 注册中心保存服务实例信息
    ↓
5. 开始发送心跳
    ↓
6. 注册中心定期检查实例健康

2.6 服务发现流程

1. 服务启动
    ↓
2. 连接注册中心
    ↓
3. 订阅服务变更
    ↓
4. 获取服务实例列表
    ↓
5. 本地缓存服务列表
    ↓
6. 定期更新缓存
    ↓
7. 使用负载均衡选择实例

三、代码示例

3.1 Eureka Server 搭建

<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}
# application.yml
server:
  port: 8761

eureka:
  instance:
    hostname: localhost
  client:
    register-with-eureka: false  # 不注册自己
    fetch-registry: false        # 不获取注册表
    service-url:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
  server:
    enable-self-preservation: false  # 关闭自我保护(开发环境)
    eviction-interval-timer-in-ms: 60000  # 清理间隔

spring:
  application:
    name: eureka-server

3.2 Eureka Client 服务注册

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.web.bind.annotation.*;

@SpringBootApplication
@EnableEurekaClient
@RestController
public class ProviderApplication {
    
    public static void main(String[] args) {
        SpringApplication.run(ProviderApplication.class, args);
    }
    
    @GetMapping("/hello")
    public String hello() {
        return "Hello from Provider!";
    }
}
# application.yml
server:
  port: 8081

spring:
  application:
    name: service-provider

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  instance:
    prefer-ip-address: true
    instance-id: ${spring.application.name}:${server.port}
    lease-renewal-interval-in-seconds: 30
    lease-expiration-duration-in-seconds: 90

3.3 服务消费者

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
@EnableDiscoveryClient
@RestController
public class ConsumerApplication {
    
    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class, args);
    }
    
    @Bean
    @LoadBalanced  // 启用负载均衡
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
    
    @Autowired
    private RestTemplate restTemplate;
    
    @Autowired
    private DiscoveryClient discoveryClient;
    
    // 方式1:使用 RestTemplate + 负载均衡
    @GetMapping("/consume")
    public String consume() {
        return restTemplate.getForObject(
            "http://service-provider/hello", 
            String.class
        );
    }
    
    // 方式2:手动发现服务
    @GetMapping("/manual")
    public String manualDiscovery() {
        List<ServiceInstance> instances = discoveryClient.getInstances("service-provider");
        
        if (instances.isEmpty()) {
            return "No instances available";
        }
        
        ServiceInstance instance = instances.get(0);
        String url = instance.getUri().toString() + "/hello";
        
        return restTemplate.getForObject(url, String.class);
    }
    
    // 查看所有服务
    @GetMapping("/services")
    public List<String> services() {
        return discoveryClient.getServices();
    }
}
# application.yml
server:
  port: 8082

spring:
  application:
    name: service-consumer

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/

3.4 Nacos 服务注册发现

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
# application.yml
server:
  port: 8081

spring:
  application:
    name: service-provider
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        namespace: dev
        group: DEFAULT_GROUP
        service: ${spring.application.name}
        weight: 1
        metadata:
          version: 1.0.0
          region: cn-east
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class NacosProviderApplication {
    public static void main(String[] args) {
        SpringApplication.run(NacosProviderApplication.class, args);
    }
}

3.5 Nacos 配置中心

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
# bootstrap.yml
spring:
  application:
    name: service-provider
  cloud:
    nacos:
      config:
        server-addr: localhost:8848
        namespace: dev
        group: DEFAULT_GROUP
        file-extension: yaml
        refresh-enabled: true  # 动态刷新
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.*;

@RestController
@RefreshScope  // 支持动态刷新
public class ConfigController {
    
    @Value("${app.message:default}")
    private String message;
    
    @GetMapping("/config")
    public String getConfig() {
        return "Config message: " + message;
    }
}

3.6 健康检查

import org.springframework.boot.actuate.health.*;
import org.springframework.stereotype.Component;

@Component
public class CustomHealthIndicator implements HealthIndicator {
    
    @Override
    public Health health() {
        // 自定义健康检查逻辑
        boolean healthy = checkHealth();
        
        if (healthy) {
            return Health.up()
                .withDetail("service", "available")
                .build();
        } else {
            return Health.down()
                .withDetail("service", "unavailable")
                .build();
        }
    }
    
    private boolean checkHealth() {
        // 检查数据库、缓存等
        return true;
    }
}

3.7 服务元数据

import org.springframework.cloud.client.serviceregistry.Registration;
import org.springframework.web.bind.annotation.*;

@RestController
public class ServiceMetadataController {
    
    @Autowired
    private Registration registration;
    
    @Autowired
    private DiscoveryClient discoveryClient;
    
    @GetMapping("/metadata")
    public Map<String, Object> getMetadata() {
        Map<String, Object> info = new HashMap<>();
        info.put("serviceId", registration.getServiceId());
        info.put("host", registration.getHost());
        info.put("port", registration.getPort());
        info.put("uri", registration.getUri());
        info.put("metadata", registration.getMetadata());
        return info;
    }
    
    @GetMapping("/instances/{serviceName}")
    public List<Map<String, Object>> getInstances(@PathVariable String serviceName) {
        List<ServiceInstance> instances = discoveryClient.getInstances(serviceName);
        
        return instances.stream().map(instance -> {
            Map<String, Object> info = new HashMap<>();
            info.put("serviceId", instance.getServiceId());
            info.put("host", instance.getHost());
            info.put("port", instance.getPort());
            info.put("uri", instance.getUri());
            info.put("metadata", instance.getMetadata());
            info.put("secure", instance.isSecure());
            return info;
        }).collect(Collectors.toList());
    }
}

四、实战应用场景

4.1 多注册中心配置

spring:
  application:
    name: multi-registry-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        namespace: prod
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:8761/eureka/

4.2 服务下线优雅处理

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.serviceregistry.Registration;
import org.springframework.cloud.client.serviceregistry.ServiceRegistry;
import org.springframework.stereotype.Component;

@Component
public class GracefulShutdown {
    
    @Autowired
    private ServiceRegistry<Registration> serviceRegistry;
    
    @Autowired
    private Registration registration;
    
    public void deregister() {
        // 从注册中心下线
        serviceRegistry.deregister(registration);
        System.out.println("服务已下线");
    }
}

4.3 服务实例选择策略

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

@Component
public class LoadBalanceStrategy {
    
    @Autowired
    private DiscoveryClient discoveryClient;
    
    private final Map<String, AtomicInteger> counters = new ConcurrentHashMap<>();
    
    // 轮询
    public ServiceInstance roundRobin(String serviceName) {
        List<ServiceInstance> instances = discoveryClient.getInstances(serviceName);
        if (instances.isEmpty()) return null;
        
        AtomicInteger counter = counters.computeIfAbsent(serviceName, k -> new AtomicInteger(0));
        int index = Math.abs(counter.getAndIncrement() % instances.size());
        return instances.get(index);
    }
    
    // 随机
    public ServiceInstance random(String serviceName) {
        List<ServiceInstance> instances = discoveryClient.getInstances(serviceName);
        if (instances.isEmpty()) return null;
        
        int index = new Random().nextInt(instances.size());
        return instances.get(index);
    }
    
    // 加权随机
    public ServiceInstance weightedRandom(String serviceName) {
        List<ServiceInstance> instances = discoveryClient.getInstances(serviceName);
        if (instances.isEmpty()) return null;
        
        // 计算总权重
        int totalWeight = instances.stream()
            .mapToInt(this::getWeight)
            .sum();
        
        int random = new Random().nextInt(totalWeight);
        int currentWeight = 0;
        
        for (ServiceInstance instance : instances) {
            currentWeight += getWeight(instance);
            if (random < currentWeight) {
                return instance;
            }
        }
        
        return instances.get(0);
    }
    
    // 一致性哈希
    public ServiceInstance consistentHash(String serviceName, String key) {
        List<ServiceInstance> instances = discoveryClient.getInstances(serviceName);
        if (instances.isEmpty()) return null;
        
        int hash = Math.abs(key.hashCode());
        int index = hash % instances.size();
        return instances.get(index);
    }
    
    private int getWeight(ServiceInstance instance) {
        Map<String, String> metadata = instance.getMetadata();
        if (metadata != null && metadata.containsKey("weight")) {
            return Integer.parseInt(metadata.get("weight"));
        }
        return 1;
    }
}

五、总结与最佳实践

注册中心选择

  1. Eureka:简单易用,适合 Spring Cloud 生态
  2. Nacos:功能全面,支持配置中心
  3. Consul:强一致性,支持多数据中心

最佳实践

  1. 服务命名:使用有意义的名称
  2. 健康检查:配置合理的健康检查
  3. 优雅下线:服务停止前先下线
  4. 元数据:存储版本、权重等信息

服务注册发现是微服务架构的基础设施,选择合适的注册中心,理解其工作原理,能够构建出高可用的微服务系统。