← 返回首页
Java学习#Java

21.SpringCloud与Nacos服务注册与发现

2026-10-22·1 次浏览
21.SpringCloud与Nacos服务注册与发现

Spring Cloud 与 Nacos:微服务注册与服务发现

一个项目刚开始时,所有功能放在同一个 Spring Boot 应用里很方便。随着业务变大,用户、订单、商品等功能会由不同小组维护,发布一个小改动却要重新构建并发布整个应用,问题就会逐渐显现。微服务提供了一种拆分方式:把边界清楚的业务拆成可以独立运行、独立发布的服务,再让这些服务通过网络接口合作。

这篇文章沿着一个很小的例子学习 Spring Cloud 服务发现:把 provider 作为服务提供者,把 consumer 作为服务发现的使用者,让两个应用都注册到 Nacos。先确认 provider 注册成功,再让 consumer 按服务名查询它,最后启动两个 provider 实例,观察一个服务名为什么会对应多个地址。

先把服务注册和发现跑通,才能知道服务名如何对应实例地址。因此,下面先介绍微服务和 Spring Cloud,再一步步配置 Nacos、provider 和 consumer。

1. 微服务到底解决什么问题

1.1 从一个大应用到多个独立服务

假设一个网上书店最初只有一个 Spring Boot 项目,里面同时包含用户、图书、订单和支付代码。这个应用可以叫作单体应用。所有功能共享一个进程、一个发布包,调用其他模块时通常是 Java 方法调用。

当业务继续增长,可以按业务边界拆出几个服务:

服务 负责的事情 可能提供的接口
user-service 用户资料和登录 /users/{id}
book-service 图书信息和库存 /books/{id}
order-service 创建和查询订单 /orders
payment-service 支付状态 /payments/{id}

这些服务分别启动在不同进程中,通过 HTTP 或消息进行通信。每个服务仍然可以是一个普通的 Spring Boot 应用,只是它不再把所有业务都放在同一个工程里。

因此,“微服务”不是把一个项目机械地切成很多文件夹,也不是服务越多越好。一个服务应该围绕相对完整的业务能力组织,能够独立开发、测试和发布。服务之间通过明确的接口交互,而不是直接访问对方的数据库表或依赖对方的内部类。

1.2 为什么要拆分

拆分通常是为了应对下面几类实际问题。

第一,不同功能的变化速度不同。商品搜索可能每天调整,支付模块却需要更严格的审核。如果它们是独立服务,商品服务可以单独发布,不必让支付模块一起重新上线。

第二,不同功能的访问量不同。商品详情可能每秒被访问很多次,后台报表访问很少。可以只增加商品服务的实例数量,把更多机器资源给访问量高的部分。

第三,故障影响范围更容易控制。推荐服务暂时不可用时,订单服务仍可以完成下单,只是页面少显示一块推荐内容。这里的“更容易控制”不代表微服务自动消除故障;网络超时、数据一致性和服务依赖反而需要额外设计。

第四,团队可以按业务边界协作。团队之间只要先约定接口,就可以分别开发服务。不过,服务拆分也会增加部署、监控、日志、网络调用和版本管理的工作量。一个小型后台如果没有这些需求,继续使用单体应用往往更简单。

1.3 本文例子的服务关系

本文不直接做完整电商系统,只使用一个 provider 和一个 consumer 观察服务发现过程:

flowchart LR C[consumer\n端口 8282] -->|查询 provider 的实例| N[Nacos\n注册中心] P1[provider\n端口 8082] -->|注册服务名 provider| N P2[provider\n端口 8083] -->|注册同一个服务名| N

provider 启动时向 Nacos 报告“我叫 provider,地址和端口是……”。consumer 按服务名查询 provider 的实例,Nacos 返回地址和端口等信息。本文先学习这段注册与发现过程。

这里先分清两件事:Nacos 负责保存和返回服务地址,不会替 provider 执行业务方法。consumer 能查到实例,表示服务发现成功;具体如何访问这个实例,是另一环节。

2. Spring Cloud、Spring Cloud Alibaba 和 Spring Boot

2.1 三者分别负责什么

Spring Boot 负责创建一个可运行的应用,例如启动内置 Web 服务器、读取 application.yml 和创建 Spring Bean。provider 和 consumer 都是 Spring Boot 应用。

Spring Cloud 是围绕分布式系统提供的一组 Spring 项目,解决服务发现、配置管理、负载均衡、熔断和网关等常见问题。本文只使用服务发现能力,通过统一的 DiscoveryClient 接口查询实例。

Spring Cloud Alibaba 是 Spring Cloud 生态中的一组 Alibaba 实现,里面包括 Nacos 服务发现与配置、Sentinel 流量控制、Seata 分布式事务等。本文只使用其中的 Nacos 服务发现模块。

可以把三者的关系理解为:

  • Spring Boot:把一个服务运行起来。
  • Spring Cloud:提供分布式应用常用的组件和统一接口。
  • Spring Cloud Alibaba:为其中一些能力提供 Nacos 等具体实现。

2.2 版本必须成套选择

Spring Cloud、Spring Cloud Alibaba 和 Spring Boot 不能随意各选一个版本。BOM(Bill of Materials)会统一管理一组依赖的版本,先根据官方兼容关系选择整套版本,再写代码。

下面的示例采用 Java 17、Spring Boot 4.0.7、Spring Cloud 2025.1.2 和 Spring Cloud Alibaba 2025.1.0.0。这个组合中的 Spring Cloud Alibaba BOM 管理 Nacos 客户端 3.1.1。若项目使用其他版本,应以对应版本的官方兼容表为准,不要只把某一个版本号替换掉。

版本说明可以先记住两点:

  1. spring-cloud.version 和 spring-cloud-alibaba.version 放在 BOM 中统一管理,普通依赖通常不再单独写版本。
  2. Spring Boot 的版本要和 Spring Cloud 发行列相匹配。遇到 Spring Boot ... is not compatible with this Spring Cloud release train,先检查版本组合,不要先改业务代码。

例如,Spring Cloud 2025.1.x 对应的是 Spring Boot 4.0.x 这一代,不能因为 Maven 能下载到 Spring Boot 4.1.1,就认为两个版本可以直接配合。依赖能够下载、代码能够编译,与应用能通过兼容性检查正常启动,是三件不同的事。

2.3 一个服务的 Maven 依赖

下面是 provider 的完整 pom.xml。consumer 的依赖基本相同,只需要把 artifactId 改成 consumer 自己的名称。MySQL 和 Lombok 不是“服务发现”必须依赖;这里保留它们,是因为实际业务服务通常还会访问数据库,示例中可以按需要删除。

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>4.0.7</version>
        <relativePath/>
    </parent>

    <groupId>com.yigalaxy</groupId>
    <artifactId>provider</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>provider</name>

    <properties>
        <java.version>17</java.version>
        <spring-cloud.version>2025.1.2</spring-cloud.version>
        <spring-cloud-alibaba.version>2025.1.0.0</spring-cloud-alibaba.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <!-- Spring Cloud 的版本由 BOM 统一管理。 -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!-- Nacos、Sentinel 等 Alibaba 组件的版本由这个 BOM 管理。 -->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <!-- 让服务可以提供 HTTP 接口。 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webmvc</artifactId>
        </dependency>
        <!-- 自动把当前应用注册到 Nacos,并提供 DiscoveryClient。 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!-- 只有这个服务需要访问 MySQL 时才保留。 -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

如果 provider 和 consumer 放在同一个 Maven 多模块项目中,可以把父工程的 dependencyManagement 提取出来,两个子模块只保留各自需要的 starter。学习阶段先把两个独立工程跑通,再考虑多模块整理,排错会更直接。

3. 注册中心:先认识 Nacos

3.1 注册中心是什么

在没有注册中心时,consumer 想调用 provider,可能只能把 http://192.168.1.20:8082 写死在配置里。provider 换机器、改端口或增加实例后,consumer 的地址就需要手动修改。

注册中心保存一张动态目录,至少包含下面这些信息:

  • 服务名,例如 provider。
  • 实例地址,例如 192.168.1.20。
  • 端口,例如 8082。
  • 实例是否健康、所属分组和元数据。

provider 启动后注册自己,Nacos 持续跟踪实例的在线状态。本文的 Nacos 3.x 客户端通过 gRPC 长连接维持临时实例的连接状态;旧版本资料中也经常会看到“发送心跳”的描述。consumer 按服务名查询实例列表,这样,调用方依赖的是服务名,而不是某一台机器的固定地址。

注册中心的职责是“登记和查询服务实例”,它不等同于网关,也不等同于数据库:

  • 网关通常负责统一入口、路由、鉴权和限流。
  • 注册中心负责服务地址和实例状态。
  • 数据库负责业务数据持久化。

Nacos 同时具备服务发现和配置管理能力,本文先只使用服务发现,避免把两个概念混在一起。

3.2 用 Docker 启动一个本地 Nacos

先确认 Docker 已经启动。学习服务发现时,只启动 Nacos 就够了,在一个空目录创建 compose.yaml:

yaml 复制代码
services:
  nacos:
    image: nacos/nacos-server:v3.1.1
    container_name: study-nacos
    restart: unless-stopped
    environment:
      MODE: standalone
      # 仅用于本地学习。生产环境应开启认证并管理好密钥。
      NACOS_AUTH_ENABLE: "false"
      # 开启认证时,令牌密钥至少使用足够长度的随机值。
      NACOS_AUTH_TOKEN: "VGhpc0lzTXlDdXN0b21TZWNyZXRLZXkwMTIzNDU2Nzg="
      NACOS_AUTH_IDENTITY_KEY: studyIdentityKey
      NACOS_AUTH_IDENTITY_VALUE: studyIdentityValue
      TZ: Asia/Shanghai
    ports:
      # Nacos 客户端 HTTP 接口。
      - "8848:8848"
      # Nacos 3.x 控制台端口。
      - "8080:8080"
      # Nacos 客户端 gRPC 端口,不能只映射 8848。
      - "9848:9848"
    volumes:
      - nacos_data:/home/nacos/data

volumes:
  nacos_data:

启动并观察日志:

powershell 复制代码
docker compose up -d nacos
docker compose logs -f nacos

看到类似 Nacos started successfully 后,再打开 http://127.0.0.1:8080/index.html。控制台能打开,说明 Web 控制台端口可用;服务能否注册,还要看应用日志和控制台的服务列表。

这里有三个容易混淆的端口:8848 是客户端访问 Nacos API 的主要端口,9848 是部分客户端使用的 gRPC 端口,8080 是 Nacos 3.x 控制台端口。应用配置里的 server-addr 写 127.0.0.1:8848,不是写控制台的 8080。

Nacos 3.x 的 Docker 镜像还会使用认证相关环境变量。示例为了本地学习关闭了认证;部署到真实环境时应开启认证、设置管理员账号和随机密钥,并限制 Nacos 端口只对需要的网络开放。

3.3 项目同时需要 MySQL 和 Redis 时怎样配置

实际项目可能同时需要 Spring Boot 应用、MySQL、Redis 和 Nacos。它们可以放在同一份 Compose 配置中统一启动:MySQL 存业务数据,Redis 做缓存,Nacos 管理服务地址。下面这份配置替换前面的 Nacos 单服务配置使用;两个 Nacos 容器使用相同的宿主机端口,不能同时启动。

yaml 复制代码
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: springcloud-study-app
    restart: unless-stopped
    depends_on:
      - nacos
    environment:
      # 容器之间通过服务名访问,不写宿主机的 127.0.0.1。
      SPRING_CLOUD_NACOS_DISCOVERY_SERVER_ADDR: nacos:8848
      SERVER_PORT: 8080
      TZ: Asia/Shanghai
    ports:
      # 宿主机 8081 -> 应用容器 8080。
      - "8081:8080"
    networks:
      - springcloud-network

  mysql:
    image: mysql:8.4
    container_name: springcloud-study-mysql
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: "root"
      MYSQL_DATABASE: springcloud001
      TZ: Asia/Shanghai
    ports:
      # 宿主机连接使用 3307,容器之间使用 mysql:3306。
      - "3307:3306"
    volumes:
      - mysql_data:/var/lib/mysql
    networks:
      - springcloud-network

  redis:
    image: redis:8
    container_name: springcloud-study-redis
    restart: unless-stopped
    command: ["redis-server", "--appendonly", "yes", "--requirepass", "ChangeMe_Redis_123!"]
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    networks:
      - springcloud-network

  nacos:
    image: nacos/nacos-server:v3.1.1
    container_name: springcloud-study-nacos
    restart: unless-stopped
    environment:
      MODE: standalone
      NACOS_AUTH_ENABLE: "false"
      NACOS_AUTH_TOKEN: "VGhpc0lzTXlDdXN0b21TZWNyZXRLZXkwMTIzNDU2Nzg="
      NACOS_AUTH_IDENTITY_KEY: studyIdentityKey
      NACOS_AUTH_IDENTITY_VALUE: studyIdentityValue
      TZ: Asia/Shanghai
    ports:
      - "8848:8848"
      - "9848:9848"
      - "8080:8080"
    volumes:
      - nacos_data:/home/nacos/data
    networks:
      - springcloud-network

volumes:
  mysql_data:
  redis_data:
  nacos_data:

networks:
  springcloud-network:
    driver: bridge

这份文件中的 app 使用当前目录的 Dockerfile 构建镜像。以稍后创建的 provider 为例,把 Compose 配置放在 provider 工程根目录,先运行 mvn clean package -DskipTests,再在同一目录准备下面的 Dockerfile:

dockerfile 复制代码
FROM eclipse-temurin:21-jre
WORKDIR /app
# 把 Maven 打包好的 provider 放进镜像。
COPY target/provider-0.0.1-SNAPSHOT.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]

镜像里的应用仍然使用 JAR 中的 application.yml,而 Compose 的环境变量会把 Nacos 地址覆盖为 nacos:8848、应用端口覆盖为 8080。应用和 Nacos 都加入 springcloud-network,所以可以通过服务名互相访问。

学习注册中心时可以先执行 docker compose up -d nacos。等 provider 打包完并准备好 Dockerfile,再执行 docker compose up -d --build app。MySQL 和 Redis 只在对应业务需要时启动;上面的 Compose 只是把它们准备在环境中,并不会自动让 Java 代码使用数据库或缓存。

depends_on 只控制启动顺序,不保证 Nacos 已经完成初始化。应用启动时如果 Nacos 还没有准备好,可能第一次注册失败;可以查看应用日志,必要时重新启动应用,生产环境则应增加健康检查和重试策略。

4. 创建 provider 并注册到 Nacos

4.1 provider 的启动类

先创建一个普通 Spring Boot 应用。启动类本身不需要手动调用 Nacos,Nacos Discovery starter 会在应用启动过程中读取配置并完成注册。本文的版本不必添加 @EnableDiscoveryClient;有些项目会显式添加这个注解,用来表达“启用服务发现能力”。

java 复制代码
package com.yigalaxy.provider;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

4.2 provider 的配置

在 provider 的 application.yml 中写服务名和 Nacos 地址:

yaml 复制代码
spring:
  application:
    # 这是注册中心中的服务名,consumer 查询时必须使用同一个值。
    name: provider
  cloud:
    nacos:
      discovery:
        # 应用运行在宿主机时访问宿主机映射出来的 8848。
        server-addr: 127.0.0.1:8848
        # 默认分组即可;多个环境可以用不同 group 或 namespace 隔离。
        group: DEFAULT_GROUP

server:
  port: 8082

如果 provider 也运行在前面的 Docker Compose 网络中,server-addr 改成 nacos:8848。不要把容器内的 nacos 主机名写在宿主机直接运行的 Java 应用里,因为这个名字只在 Docker 网络中可解析。

可以把环境差异放进 Spring profile,而不是每次修改同一个文件:

yaml 复制代码
# application-docker.yml
spring:
  cloud:
    nacos:
      discovery:
        server-addr: nacos:8848

启动时选择 profile:

powershell 复制代码
mvn spring-boot:run "-Dspring-boot.run.profiles=docker"

4.3 写一个最简单的 provider 接口

先用一个返回端口的接口确认“到底调用了哪一个实例”。这里使用 @Value 读取当前应用的端口;它不负责服务发现,只是帮助我们观察结果。

java 复制代码
package com.yigalaxy.provider.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ProviderController {
    @Value("${server.port}")
    private String port;

    @GetMapping("/index")
    public String index() {
        // 访问不同实例时返回不同端口,方便观察实例切换。
        return "当前的服务端口是:" + port + ",服务名是 provider";
    }
}

在 provider 工程根目录打开终端,启动应用:

powershell 复制代码
mvn spring-boot:run

保持这个终端运行,再单独访问 http://localhost:8082/index,预期返回:

text 复制代码
当前的服务端口是:8082,服务名是 provider

这一步只证明 provider 自己能工作,还没有证明它已经注册到 Nacos。继续看启动日志和 Nacos 控制台的“服务管理”页面,服务名应为 provider,实例端口应为 8082。

4.4 怎样判断注册成功

按下面顺序检查,比只看“应用启动成功”更可靠:

  1. provider 日志没有连接 Nacos 失败、超时或认证错误。
  2. 浏览器打开 Nacos 控制台,在服务列表中找到 provider。
  3. provider 的实例列表中出现正确的 IP 和 8082 端口。
  4. 关闭 provider 后等待一段时间,实例状态会变化或从临时实例列表中消失。

Nacos 中显示的是实例地址。这个地址必须是 consumer 能访问的地址:如果 provider 在容器内注册了只能在容器网络访问的 IP,而 consumer 在宿主机运行,consumer 即使查到了实例也可能连接失败。Docker 场景要特别检查网络和实例 IP 的可达性。

5. 创建 consumer,先做服务发现

5.1 consumer 的工程和配置

consumer 使用和 provider 相同的 Spring Cloud 与 Nacos 依赖,artifactId 改为 consumer,并新增自己的启动类:

java 复制代码
package com.yigalaxy.consumer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

consumer 的配置使用不同的端口,但 Nacos 地址相同:

yaml 复制代码
spring:
  application:
    name: consumer
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        group: DEFAULT_GROUP

server:
  port: 8282

spring.application.name 决定 consumer 在 Nacos 中显示的服务名;server.port 只是这个应用自己的 HTTP 端口。provider 用 8082,consumer 用 8282,不能因为服务名不同就省略端口配置。

5.2 DiscoveryClient 做了什么

DiscoveryClient 是 Spring Cloud 提供的服务发现接口,用来按服务名查询实例。引入 Nacos starter 后,Spring 会创建使用 Nacos 的实现对象,再通过构造器传给 Controller,所以这里不需要自己 new DiscoveryClient。

查询结果中的 ServiceInstance 也是 Spring Cloud 提供的接口,表示一个正在运行的服务实例,包含 IP、端口和服务名等信息。它不只用于 Nacos;换成其他注册中心时,业务代码仍可以用这套接口读取实例信息。

下面的接口查询名为 provider 的实例:

java 复制代码
package com.yigalaxy.consumer.controller;

import java.util.List;

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

@RestController
public class ConsumerController {
    private final DiscoveryClient discoveryClient;

    public ConsumerController(DiscoveryClient discoveryClient) {
        this.discoveryClient = discoveryClient;
    }

    @GetMapping("/list")
    public List<ServiceInstance> list() {
        List<ServiceInstance> instances = discoveryClient.getInstances("provider");
        if (instances.isEmpty()) {
            throw new ResponseStatusException(
                    HttpStatus.SERVICE_UNAVAILABLE,
                    "暂时没有可用的 provider 实例");
        }
        return instances;
    }
}

保持 provider 运行,在 consumer 工程根目录另开一个终端,执行 mvn spring-boot:run。consumer 启动后也会注册到 Nacos;它本身同样是一个可被发现的服务。需要查询 consumer 时,provider 也可以用 getInstances("consumer") 读取它的实例信息。

现在访问 http://localhost:8282/list。如果 provider 已注册,会看到一个 JSON 数组。数组中的每一项就是一个 ServiceInstance,常见字段包括 host、port、serviceId、uri、metadata 和 secure。

返回结果可能类似下面这样,具体 IP、实例编号和元数据会根据运行环境变化:

json 复制代码
[
  {
    "serviceId": "provider",
    "host": "192.168.215.1",
    "instanceId": "192.168.215.1#8082##DEFAULT_GROUP@@provider",
    "port": 8082,
    "scheme": "http",
    "uri": "http://192.168.215.1:8082",
    "secure": false,
    "metadata": {
      "nacos.instanceId": "192.168.215.1#8082##DEFAULT_GROUP@@provider",
      "nacos.cluster": "DEFAULT",
      "nacos.weight": "1.0",
      "nacos.ephemeral": "true",
      "nacos.healthy": "true",
      "preserved.register.source": "SPRING_CLOUD"
    }
  }
]

读这个结果时,先抓住几个字段:

  • serviceId 是服务名,同一个服务的多个实例都叫 provider。
  • host 和 port 是这个实例的地址,uri 把协议、地址和端口合在一起。
  • instanceId 区分不同实例;示例里的实例编号包含 IP、端口和分组信息。
  • scheme 和 secure 表示访问协议;这里使用 HTTP,所以 secure 是 false。
  • metadata 是补充信息,例如集群名、权重、是否为临时实例。不同客户端版本返回的元数据可能不同,不需要照着手写这些值。

访问 /list 返回实例信息,表示服务发现成功。它不包含 provider 的业务数据,也不会执行 provider 中的 Controller。

5.3 为什么返回集合而不是一个对象

如果只想观察一个实例,可以在确认集合非空后用 instances.get(0) 取第一项。不过,同一个服务可能有多个实例,所以 getInstances 返回的是 List<ServiceInstance>,这里直接返回集合,便于稍后观察两个实例。

多个实例可能来自:

  • 同一个服务在多台机器上部署。
  • 同一个 jar 启动多次,但使用不同端口。
  • 高访问量下增加副本,把请求分散到多个进程。

getInstances 返回的是完整的实例集合。本文将它直接展示出来,用来观察一个服务名下登记了哪些实例。

6. 启动多个 provider 实例

6.1 同一服务名配置不同端口

现在 consumer 已经能查到一个 provider 实例。实际部署中,访问量增加时可以启动同一服务的多个进程,让 Nacos 记录每一个实例。这里的重点是:两个进程使用相同的服务名,但监听不同端口。

先编译 provider:

powershell 复制代码
mvn clean package -DskipTests

第一个实例使用 application.yml 中的 8082 端口启动。第二个实例不需要复制代码,只要覆盖端口:

powershell 复制代码
mvn spring-boot:run "-Dspring-boot.run.arguments=--server.port=8083"

或者对已经打包好的 JAR 启动第二个进程:

powershell 复制代码
java -jar target/provider-0.0.1-SNAPSHOT.jar --server.port=8083

两个进程的 spring.application.name 都是 provider,端口分别为 8082 和 8083。服务名相同,Nacos 才会把它们放在同一个服务下;如果第二个进程使用 provider-8083,Nacos 会把它当成另一个服务。

6.2 在 Nacos 和 consumer 中观察实例

再次访问 http://localhost:8282/list,返回数组中应有两个实例,端口分别为 8082 和 8083。Nacos 控制台的 provider 服务详情中也应显示两个实例。

分别访问两个 provider 自己的接口:

text 复制代码
http://localhost:8082/index
http://localhost:8083/index

它们会返回不同端口,但使用的是同一份代码。这说明同一个服务可以由多个独立运行的实例组成。

Nacos 负责记录并提供这些实例信息。consumer 怎样从多个实例中选择一个、怎样访问它,属于后续服务调用环节,这篇先不展开。

6.3 实例下线后观察状态

关闭 8083 实例,再观察 Nacos 控制台和 /list。正常退出时,客户端会注销实例;进程突然中断时,Nacos 需要先检测连接断开,再更新实例状态。

consumer 的服务发现客户端也可能短暂缓存旧信息,因此各处看到状态更新的时间可能略有差异。可以等片刻再次查看 /list,确认返回列表中只剩 8082。

实际项目还需要关注 provider 的健康检查、优雅停机、实例注册 IP 是否可达,以及不同环境是否应该使用不同的 namespace 或 group。

7. Docker 环境里最容易出错的地址

7.1 localhost 取决于请求从哪里发出

127.0.0.1 和 localhost 代表“当前网络命名空间的本机”。如果 Java 应用在 Windows 宿主机运行,127.0.0.1:8848 指向宿主机映射的 Nacos 端口;如果 Java 应用在 Docker 容器中运行,127.0.0.1:8848 指向应用自己所在的容器,不是 Nacos 容器。

因此有两种常见写法:

应用位置 Nacos 地址
provider/consumer 在宿主机直接运行 127.0.0.1:8848
provider/consumer 在 Compose 网络中运行 nacos:8848

同样,Compose 网络里的应用连接 MySQL 应使用 mysql:3306、连接 Redis 应使用 redis:6379;宿主机程序才使用映射端口 3307 和 6379。

7.2 宿主机端口和容器端口不是一回事

"8081:8080" 的左边是宿主机端口,右边是容器端口。用户从宿主机访问应用用 localhost:8081,应用容器内部仍然监听 8080。

Nacos 的 "8080:8080" 是控制台映射,和上面应用的容器端口不是同一个网络命名空间,因此在不同容器中可以都使用 8080;但如果把两个端口都映射到宿主机的 8080,就会发生冲突。

7.3 服务注册的 IP 必须能被 consumer 访问

provider 注册时会把自己的地址写入 Nacos。若 provider 在容器里注册了 172.x.x.x,consumer 在宿主机上运行时可能无法访问这个容器地址;如果 consumer 和 provider 在同一个 Compose 网络中,这个地址通常才可达。

Nacos 返回的 host 和 port 应当是 consumer 所在网络能够访问的地址。若 provider 注册了容器内不可达的 IP,consumer 虽能在 /list 中看到它,后续访问仍会失败。

8. 常见问题排查

8.1 依赖无法解析或启动时报版本错误

先检查三件事:

  1. Spring Boot、Spring Cloud 和 Spring Cloud Alibaba 是否来自同一兼容组合。
  2. 两个 BOM 是否都放在 dependencyManagement,且没有重复引入冲突版本。
  3. spring-cloud-starter-alibaba-nacos-discovery 的版本是否由 Alibaba BOM 管理,而不是手动写了另一个版本。

mvn dependency:tree 可以查看最终解析出的版本。不要只看 pom.xml 里写了什么,要看 Maven 最终使用了什么。

8.2 Nacos 控制台打不开

检查容器状态和端口:

powershell 复制代码
docker ps
docker compose logs nacos
Test-NetConnection 127.0.0.1 -Port 8848
Test-NetConnection 127.0.0.1 -Port 8080

控制台通常访问 8080,Java 客户端配置 8848;把两者写反会出现“浏览器能打开但应用连不上”或“客户端能连接但不知道控制台地址”的情况。

8.3 Nacos 服务列表没有 provider

检查 provider 的:

  • spring.application.name 是否确实是 provider。
  • spring.cloud.nacos.discovery.server-addr 是否指向可访问的 8848。
  • provider 使用的是宿主机配置还是 Docker 配置。
  • Nacos 是否完成启动,日志中是否出现连接或认证错误。
  • 应用端口是否被其他进程占用。

8.4 consumer 找不到实例

discoveryClient.getInstances("provider") 中的字符串必须和 provider 的 spring.application.name 完全一致,大小写和空格都不能随意改。group 和 namespace 不一致时,服务也可能彼此看不见。

如果 /list 返回 503,先看 Nacos 控制台和 consumer、provider 的服务名配置;如果 /list 有实例,再确认返回的地址是否符合当前部署网络。

8.5 分清服务发现与业务访问

discoveryClient.getInstances("provider") 返回的是服务实例信息,例如 IP 和端口。它不会读取 provider 的业务数据,也不会执行 provider 中的 Controller。

因此,访问 /list 得到两个 ServiceInstance,只能说明 Nacos 注册发现流程可用。要获取 provider 的业务结果,还需要后续的服务访问步骤。把这两个环节分开,排查时就能判断问题发生在“找不到实例”,还是“实例地址有了但后续访问失败”。

9. 从这个例子继续扩展

本文只使用了 Nacos 的服务发现能力。服务实例登记后,微服务项目还可以按需要加入其他组件,它们分别解决不同问题:

需求 常见组件或做法 解决的问题
统一入口和路由 Spring Cloud Gateway 外部请求如何进入多个服务
集中配置 Nacos Config 等 配置修改和环境管理
限流和熔断 Sentinel、CircuitBreaker 依赖服务变慢或故障时保护系统
分布式事务 Seata 等 多服务写入时处理事务一致性

不要一开始就把所有组件全部加入。先让 provider 注册、consumer 发现并看到实例信息,再根据项目需求逐步增加其他能力。

10. 一次完整验证的顺序

可以按下面顺序复现全文例子:

  1. 启动 Docker 中的 Nacos,确认 8848、9848 和 8080 的用途。
  2. 创建 provider,配置服务名 provider 和端口 8082。
  3. 访问 provider 的 /index,先确认它自己能返回结果。
  4. 到 Nacos 控制台确认 provider 已注册。
  5. 创建 consumer,配置服务名 consumer 和端口 8282。
  6. 访问 /list,确认 consumer 能取得 ServiceInstance。
  7. 再启动一个 8083 provider,访问 /list,确认结果中出现两个实例。
  8. 分别访问 8082 和 8083 上的 /index,观察两个 provider 实例返回的端口。
  9. 关闭其中一个实例,等待 Nacos 更新实例状态,再访问 /list。

这条顺序把问题分成几层:先验证服务自身,再验证注册,然后验证发现和多实例。任何一步失败,都可以根据它所在的层次缩小排查范围。

11. 官方资料

── 完 ──

评论

还没有评论,来说点什么
发表评论
0 / 1000