Spring Boot 微服务注册与发现详解
为什么需要服务注册与发现
在微服务架构中,服务实例动态变化:
- 服务扩容/缩容:实例数量不断变化
- 故障转移:故障实例需要自动剔除
- 动态路由:客户端需要知道可用服务地址
- 负载均衡:在多个实例间分发请求
主流注册中心对比
| 特性 | Eureka | Nacos | Consul | Zookeeper |
|---|---|---|---|---|
| 开发公司 | Netflix | 阿里巴巴 | HashiCorp | Apache |
| 一致性协议 | AP | AP/CP | CP | CP |
| 健康检查 | 客户端心跳 | TCP/HTTP/MySQL | TCP/HTTP/gRPC | 临时节点 |
| 多数据中心 | 支持 | 支持 | 支持 | 不支持 |
| Spring Cloud | 原生支持 | 支持 | 支持 | 支持 |
| 配置中心 | 不支持 | 支持 | 支持 | 不支持 |
实现方式一:Eureka
1. Eureka Server 配置
添加依赖
xml<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency>
启动类
java@SpringBootApplication @EnableEurekaServer public class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); } }
配置文件
yamlserver: port: 8761 spring: application: name: eureka-server 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: 5000
2. Eureka Client 配置
添加依赖
xml<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
配置文件
yamlserver: port: 8081 spring: application: name: user-service eureka: client: service-url: defaultZone: http://localhost:8761/eureka/ instance: # 使用 IP 地址注册 prefer-ip-address: true # 实例 ID instance-id: ${spring.application.name}:${server.port} # 心跳间隔 lease-renewal-interval-in-seconds: 5 # 服务过期时间 lease-expiration-duration-in-seconds: 10
3. Eureka 高可用集群
yaml# eureka-server-1.yml server: port: 8761 spring: application: name: eureka-server eureka: instance: hostname: eureka1 client: service-url: defaultZone: http://eureka2:8762/eureka/,http://eureka3:8763/eureka/ # eureka-server-2.yml server: port: 8762 eureka: instance: hostname: eureka2 client: service-url: defaultZone: http://eureka1:8761/eureka/,http://eureka3:8763/eureka/ # eureka-server-3.yml server: port: 8763 eureka: instance: hostname: eureka3 client: service-url: defaultZone: http://eureka1:8761/eureka/,http://eureka2:8762/eureka/
实现方式二:Nacos(推荐)
1. Nacos Server 部署
bash# 下载并启动 Nacos curl -O https://github.com/alibaba/nacos/releases/download/2.2.3/nacos-server-2.2.3.tar.gz tar -xzf nacos-server-2.2.3.tar.gz cd nacos/bin # 单机模式启动 sh startup.sh -m standalone # 访问控制台 # http://localhost:8848/nacos # 默认账号密码:nacos/nacos
2. Nacos Client 配置
添加依赖
xml<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
配置文件
yamlserver: port: 8081 spring: application: name: user-service cloud: nacos: discovery: # Nacos 服务器地址 server-addr: localhost:8848 # 命名空间(环境隔离) namespace: dev # 分组 group: DEFAULT_GROUP # 元数据 metadata: version: v1 region: beijing # 权重 weight: 1 # 临时实例(false 为持久实例) ephemeral: true # 心跳间隔 heart-beat-interval: 5000 # 心跳超时 heart-beat-timeout: 15000
3. Nacos 服务发现
java@RestController @RequestMapping("/discovery") @RequiredArgsConstructor public class DiscoveryController { private final DiscoveryClient discoveryClient; private final LoadBalancerClient loadBalancerClient; /** * 获取所有服务实例 */ @GetMapping("/services") public List<String> getServices() { return discoveryClient.getServices(); } /** * 获取指定服务的实例 */ @GetMapping("/instances/{serviceName}") public List<ServiceInstance> getInstances(@PathVariable String serviceName) { return discoveryClient.getInstances(serviceName); } /** * 使用负载均衡选择实例 */ @GetMapping("/choose/{serviceName}") public ServiceInstance choose(@PathVariable String serviceName) { return loadBalancerClient.choose(serviceName); } }
实现方式三:Consul
1. Consul Server 部署
bash# 下载 Consul # https://www.consul.io/downloads # 开发模式启动 consul agent -dev # 生产模式(集群) consul agent -server -bootstrap-expect=3 -data-dir=/var/consul \ -bind=192.168.1.1 -client=0.0.0.0 -ui -retry-join="provider=aws ..."
2. Consul Client 配置
添加依赖
xml<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-consul-discovery</artifactId> </dependency>
配置文件
yamlserver: port: 8081 spring: application: name: user-service cloud: consul: host: localhost port: 8500 discovery: # 注册服务 register: true # 服务名称 service-name: ${spring.application.name} # 健康检查路径 health-check-path: /actuator/health # 健康检查间隔 health-check-interval: 10s # 实例 ID instance-id: ${spring.application.name}:${random.value} # 标签 tags: version=1.0,profile=dev # 元数据 metadata: version: v1 region: beijing
服务间调用
1. 使用 RestTemplate + @LoadBalanced
java@Configuration public class RestTemplateConfig { @Bean @LoadBalanced // 开启负载均衡 public RestTemplate restTemplate() { return new RestTemplate(); } } @Service @RequiredArgsConstructor public class UserService { private final RestTemplate restTemplate; public Order getOrderByUserId(Long userId) { // 使用服务名调用,而非具体 IP String url = "http://order-service/orders/user/" + userId; return restTemplate.getForObject(url, Order.class); } }
2. 使用 OpenFeign(推荐)
添加依赖
xml<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
启用 Feign
java@SpringBootApplication @EnableFeignClients public class UserServiceApplication { public static void main(String[] args) { SpringApplication.run(UserServiceApplication.class, args); } }
定义 Feign 客户端
java@FeignClient( name = "order-service", fallback = OrderClientFallback.class, configuration = FeignConfig.class ) public interface OrderClient { @GetMapping("/orders/{id}") Order getOrderById(@PathVariable("id") Long id); @GetMapping("/orders/user/{userId}") List<Order> getOrdersByUserId(@PathVariable("userId") Long userId); @PostMapping("/orders") Order createOrder(@RequestBody Order order); } @Component @Slf4j public class OrderClientFallback implements OrderClient { @Override public Order getOrderById(Long id) { log.warn("Order service is down, returning fallback for order: {}", id); Order order = new Order(); order.setId(id); order.setStatus("UNKNOWN"); return order; } @Override public List<Order> getOrdersByUserId(Long userId) { return Collections.emptyList(); } @Override public Order createOrder(Order order) { throw new RuntimeException("Order service is unavailable"); } }
Feign 配置
java@Configuration public class FeignConfig { @Bean public Retryer feignRetryer() { // 重试策略:初始间隔 100ms,最大间隔 1s,最多重试 3 次 return new Retryer.Default(100, 1000, 3); } @Bean public ErrorDecoder feignErrorDecoder() { return new CustomErrorDecoder(); } @Bean public RequestInterceptor feignRequestInterceptor() { return requestTemplate -> { // 添加请求头 requestTemplate.header("X-Request-Id", UUID.randomUUID().toString()); // 传递认证信息 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attributes != null) { HttpServletRequest request = attributes.getRequest(); String token = request.getHeader("Authorization"); if (StringUtils.hasText(token)) { requestTemplate.header("Authorization", token); } } }; } }
负载均衡
1. Ribbon(已弃用,Spring Cloud 2020+ 使用 Spring Cloud LoadBalancer)
yaml# 自定义负载均衡策略 user-service: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 其他策略:RoundRobinRule, WeightedResponseTimeRule, BestAvailableRule
2. Spring Cloud LoadBalancer
java@Configuration public class LoadBalancerConfig { @Bean public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer( Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) { String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME); return new ReactorServiceInstanceLoadBalancer() { private Random random = new Random(); @Override public Mono<Response<ServiceInstance>> choose(Request request) { ServiceInstanceListSupplier supplier = loadBalancerClientFactory .getLazyProvider(name, ServiceInstanceListSupplier.class) .getIfAvailable(); return supplier.get().next().map(instances -> { if (instances.isEmpty()) { return new EmptyResponse(); } // 随机选择 int index = random.nextInt(instances.size()); return new DefaultResponse(instances.get(index)); }); } }; } }
服务网关(Gateway)
xml<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency>
yamlserver: port: 8080 spring: application: name: api-gateway cloud: gateway: discovery: locator: enabled: true # 自动从注册中心发现服务 lower-case-service-id: true routes: # 用户服务路由 - id: user-service uri: lb://user-service predicates: - Path=/api/users/** filters: - StripPrefix=1 - name: Retry args: retries: 3 statuses: BAD_GATEWAY # 订单服务路由 - id: order-service uri: lb://order-service predicates: - Path=/api/orders/** filters: - StripPrefix=1 - name: CircuitBreaker args: name: orderServiceCircuitBreaker fallbackUri: forward:/fallback/order # 全局过滤器 default-filters: - AddRequestHeader=X-Request-From, Gateway - AddResponseHeader=X-Response-From, Gateway
配置中心集成(Nacos Config)
xml<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>
yaml# bootstrap.yml spring: application: name: user-service profiles: active: dev cloud: nacos: config: server-addr: localhost:8848 file-extension: yaml namespace: dev group: DEFAULT_GROUP # 共享配置 shared-configs: - data-id: common.yaml group: DEFAULT_GROUP refresh: true # 扩展配置 extension-configs: - data-id: redis.yaml group: DEFAULT_GROUP refresh: true
java@RestController @RefreshScope // 配置自动刷新 public class ConfigController { @Value("${user.name:default}") private String userName; @Value("${user.age:0}") private Integer userAge; @GetMapping("/config") public Map<String, Object> getConfig() { Map<String, Object> config = new HashMap<>(); config.put("userName", userName); config.put("userAge", userAge); return config; } }
健康检查与监控
yamlmanagement: endpoints: web: exposure: include: health,info,metrics,prometheus endpoint: health: show-details: always health: circuitbreakers: enabled: true
java@Component public class CustomHealthIndicator implements HealthIndicator { @Override public Health health() { int errorCode = check(); if (errorCode != 0) { return Health.down() .withDetail("Error Code", errorCode) .build(); } return Health.up().build(); } private int check() { // 自定义健康检查逻辑 return 0; } }
最佳实践
1. 服务命名规范
yamlspring: application: # 使用小写,用横线分隔 name: order-service
2. 版本管理
yaml# 通过元数据区分版本 spring: cloud: nacos: discovery: metadata: version: v2 region: beijing
3. 优雅下线
java@Component public class GracefulShutdown implements ApplicationListener<ContextClosedEvent> { @Autowired private NacosRegistration registration; @Override public void onApplicationEvent(ContextClosedEvent event) { // 从注册中心注销 registration.deregister(); // 等待正在处理的请求完成 try { Thread.sleep(5000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }
总结
| 注册中心 | 推荐场景 | 优势 | 劣势 |
|---|---|---|---|
| Eureka | Spring Cloud 传统项目 | 成熟稳定 | 停止维护 |
| Nacos | 新项目的首选 | 功能全面、国产支持 | 学习成本 |
| Consul | 多语言环境 | 云原生、功能丰富 | 资源占用 |
| Zookeeper | 已有 ZK 集群 | 成熟可靠 | 功能单一 |
选型建议:
- 新项目:Nacos(功能全面,社区活跃)
- 云原生:Consul
- 遗留项目:Eureka