Spring Cloud 服务注册发现详解
一、知识概述
服务注册与发现是微服务架构的核心基础设施,它解决了服务实例动态变化时的地址管理问题。Spring Cloud 提供了多种服务注册中心的支持,包括 Eureka、Nacos、Consul 等。
服务注册发现的核心概念:
- 服务注册:服务启动时向注册中心注册自己的地址
- 服务发现:消费者从注册中心获取服务提供者的地址
- 健康检查:注册中心定期检查服务实例的健康状态
- 服务续约:服务实例定期向注册中心发送心跳
理解服务注册发现的原理,是构建微服务架构的基础。
二、知识点详细讲解
2.1 服务注册发现架构
服务提供者 注册中心 服务消费者
│ │ │
│──── 注册 ────────────>│ │
│ │ │
│<─── 心跳 ────────────>│ │
│ │ │
│ │<──── 发现 ────────────│
│ │ │
│ │───── 地址列表 ───────>│
│ │ │
│<──────────────────── 直接调用 ───────────────>│
2.2 注册中心对比
| 特性 | Eureka | Nacos | Consul |
|---|---|---|---|
| CAP | AP | AP/CP | CP |
| 健康检查 | 心跳 | 心跳/HTTP/TCP | HTTP/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;
}
}
五、总结与最佳实践
注册中心选择
- Eureka:简单易用,适合 Spring Cloud 生态
- Nacos:功能全面,支持配置中心
- Consul:强一致性,支持多数据中心
最佳实践
- 服务命名:使用有意义的名称
- 健康检查:配置合理的健康检查
- 优雅下线:服务停止前先下线
- 元数据:存储版本、权重等信息
服务注册发现是微服务架构的基础设施,选择合适的注册中心,理解其工作原理,能够构建出高可用的微服务系统。