乐闻世界logo
搜索文章和话题

Spring Boot 中如何实现微服务架构的服务注册与发现?

3月6日 21:58

Spring Boot 微服务注册与发现详解

为什么需要服务注册与发现

在微服务架构中,服务实例动态变化:

  • 服务扩容/缩容:实例数量不断变化
  • 故障转移:故障实例需要自动剔除
  • 动态路由:客户端需要知道可用服务地址
  • 负载均衡:在多个实例间分发请求

主流注册中心对比

特性EurekaNacosConsulZookeeper
开发公司Netflix阿里巴巴HashiCorpApache
一致性协议APAP/CPCPCP
健康检查客户端心跳TCP/HTTP/MySQLTCP/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); } }

配置文件

yaml
server: 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>

配置文件

yaml
server: 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>

配置文件

yaml
server: 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>

配置文件

yaml
server: 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>
yaml
server: 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; } }

健康检查与监控

yaml
management: 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. 服务命名规范

yaml
spring: 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(); } } }

总结

注册中心推荐场景优势劣势
EurekaSpring Cloud 传统项目成熟稳定停止维护
Nacos新项目的首选功能全面、国产支持学习成本
Consul多语言环境云原生、功能丰富资源占用
Zookeeper已有 ZK 集群成熟可靠功能单一

选型建议:

  • 新项目:Nacos(功能全面,社区活跃)
  • 云原生:Consul
  • 遗留项目:Eureka
标签:Spring Boot