从零搭建 Spring Boot + Nacos + MySQL + OpenFeign + Docker Compose 微服务项目

从零搭建 Spring Boot + Nacos + MySQL + OpenFeign + Docker Compose 微服务项目

本文以一个"用户服务 + 订单服务"的完整示例为主线。完成后,可以通过浏览器访问两个后端服务,订单服务还能通过 Nacos 发现用户服务,并使用 OpenFeign 调用用户接口。
所有微服务启动时,向 Nacos 注册自身,同时从 Nacos 拉取配置并监听变更;客户端请求打到 Gateway 网关,网关从 Nacos 拿到后端服务实例,负载均衡转发;服务之间使用 Feign 远程调用,每次调用都会被 Sentinel 做流量和熔断保护

1. 最终要搭建什么

整个项目包含 4 个容器:

容器 作用 宿主机访问端口 容器内部端口
mysql 保存 Nacos、用户和订单数据 3307 3306
nacos 服务注册中心 8848、9848 8848、9848
user-service 查询用户数据 8081 8081
order-service 查询订单,并调用用户服务 8082 8082

调用关系如下:

text 复制代码
浏览器
  ├─ http://localhost:8081/user/list
  │                  │
  │                  └─ user-service ──> MySQL 的 spring-cloud-user 数据库
  │
  └─ http://localhost:8082/tbOrder/101
                     │
                     ├─ order-service ──> MySQL 的 spring-cloud-order 数据库
                     │
                     └─ OpenFeign ──> Nacos 查询 user-service ──> user-service

这里最重要的概念是:

  • 每个微服务都是一个独立的 Spring Boot 应用。
  • 每个微服务有自己的名称、端口和数据库。
  • 服务启动后把自己的地址注册到 Nacos。
  • 订单服务调用用户服务时,不写死用户服务的 IP,而是通过服务名 user-service 查找。
  • Docker Compose 负责一次性创建网络、MySQL、Nacos 和两个后端容器。

2. 准备开发环境

需要安装以下软件:

  1. JDK 17
  2. Maven 3.9 或兼容版本
  3. Docker Desktop
  4. IntelliJ IDEA(推荐,但不是必须)

安装完成后,在 PowerShell 中检查:

powershell 复制代码
java -version
mvn -version
docker version
docker compose version

预期结果:

  • Java 显示 17。
  • Maven 能正常显示版本,并且使用 Java 17。
  • Docker 客户端和服务端都能显示信息。
  • docker compose 命令存在。

如果 docker version 只能显示 Client,不能显示 Server,请先启动 Docker Desktop,等待左下角显示 Docker Engine 已运行。


3. 创建项目目录

创建父工程目录:

text 复制代码
demo01_parent/
├─ pom.xml
├─ docker-compose.yml
├─ init/
│  ├─ 01-user.sql
│  ├─ 02-order.sql
│  └─ nacos.sql
├─ demo01_user/
│  ├─ pom.xml
│  ├─ Dockerfile
│  └─ src/
└─ demo01_order/
   ├─ pom.xml
   ├─ Dockerfile
   └─ src/

说明:

  • demo01_parent 是 Maven 父工程,同时也是 Compose 项目根目录。
  • demo01_user 是用户微服务。
  • demo01_order 是订单微服务。
  • init 保存 MySQL 第一次启动时自动执行的 SQL。
  • 每个后端目录都有自己的 Dockerfile。

4. 创建 Maven 父工程

在项目根目录创建 pom.xml:

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>3.3.5</version>
        <relativePath/>
    </parent>

    <groupId>org.example.c</groupId>
    <artifactId>demo01_parent</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>pom</packaging>

    <modules>
        <module>demo01_user</module>
        <module>demo01_order</module>
    </modules>

    <properties>
        <java.version>17</java.version>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <spring-cloud.version>2023.0.3</spring-cloud.version>
        <spring-cloud-alibaba.version>2023.0.1.2</spring-cloud-alibaba.version>
        <mysql.version>8.0.33</mysql.version>
        <mybatis-plus.version>3.5.7</mybatis-plus.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <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>

            <dependency>
                <groupId>com.mysql</groupId>
                <artifactId>mysql-connector-j</artifactId>
                <version>${mysql.version}</version>
            </dependency>

            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
                <version>${mybatis-plus.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

关键说明:

  • <packaging>pom</packaging> 表示父工程本身不生成可运行 JAR,主要负责管理子模块。
  • <modules> 告诉 Maven 一次构建两个子工程。
  • dependencyManagement 只管理依赖版本,不会自动把依赖加入子工程。
  • Spring Boot、Spring Cloud 和 Spring Cloud Alibaba 的版本必须互相兼容,不能随意拼接不同年代的版本。

5. 创建用户服务

5.1 用户服务目录

text 复制代码
demo01_user/
├─ pom.xml
├─ Dockerfile
└─ src/main/
   ├─ java/org/example/c/demo01_user/
   │  ├─ Demo01UserApplication.java
   │  ├─ controller/TbUserController.java
   │  ├─ entity/TbUser.java
   │  ├─ mapper/TbUserMapper.java
   │  ├─ service/TbUserService.java
   │  └─ service/impl/TbUserServiceImpl.java
   └─ resources/
      ├─ application.yml
      └─ application-compose.yml

5.2 用户服务 pom.xml

创建 demo01_user/pom.xml:

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.example.c</groupId>
        <artifactId>demo01_parent</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <relativePath>../pom.xml</relativePath>
    </parent>

    <artifactId>demo01_user</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </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>

依赖作用:

  • spring-boot-starter-web:提供 Spring MVC、Tomcat 和 REST 接口能力。
  • mysql-connector-j:让 Java 可以连接 MySQL。
  • lombok:使用 @Data 自动生成 getter、setter 等代码。
  • mybatis-plus-spring-boot3-starter:简化数据库 CRUD。
  • spring-cloud-starter-alibaba-nacos-discovery:把服务注册到 Nacos,并发现其他服务。
  • spring-boot-starter-test:提供测试支持。
  • spring-boot-maven-plugin:把程序打包成可以用 java -jar 启动的 JAR。

5.3 用户服务启动类

创建 Demo01UserApplication.java:

java 复制代码
package org.example.c.demo01_user;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

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

说明:

  • @SpringBootApplication 是 Spring Boot 主入口。
  • @EnableDiscoveryClient 表示启用服务注册与发现。引入 Nacos discovery 依赖后,应用启动时会自动注册。

5.4 用户实体类

创建 entity/TbUser.java:

java 复制代码
package org.example.c.demo01_user.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

@Data
@TableName("tb_user")
public class TbUser {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String username;
    private String address;
}

说明:

  • @TableName("tb_user") 指定对应数据库表。
  • @TableId(type = IdType.AUTO) 表示主键由 MySQL 自增。
  • MyBatis Plus 默认会把 Java 的 username 映射到不区分大小写的 MySQL 字段 userName。

5.5 Mapper、Service 和实现类

创建 mapper/TbUserMapper.java:

java 复制代码
package org.example.c.demo01_user.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.example.c.demo01_user.entity.TbUser;

@Mapper
public interface TbUserMapper extends BaseMapper<TbUser> {
}

创建 service/TbUserService.java:

java 复制代码
package org.example.c.demo01_user.service;

import com.baomidou.mybatisplus.extension.service.IService;
import org.example.c.demo01_user.entity.TbUser;

public interface TbUserService extends IService<TbUser> {
}

创建 service/impl/TbUserServiceImpl.java:

java 复制代码
package org.example.c.demo01_user.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.example.c.demo01_user.entity.TbUser;
import org.example.c.demo01_user.mapper.TbUserMapper;
import org.example.c.demo01_user.service.TbUserService;
import org.springframework.stereotype.Service;

@Service
public class TbUserServiceImpl
        extends ServiceImpl<TbUserMapper, TbUser>
        implements TbUserService {
}

说明:继承 MyBatis Plus 提供的接口和实现后,无需手写 SQL,就能使用 list()、getById()、save() 等常用方法。

5.6 用户接口

创建 controller/TbUserController.java:

java 复制代码
package org.example.c.demo01_user.controller;

import org.example.c.demo01_user.entity.TbUser;
import org.example.c.demo01_user.service.TbUserService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/user")
public class TbUserController {
    private final TbUserService tbUserService;

    public TbUserController(TbUserService tbUserService) {
        this.tbUserService = tbUserService;
    }

    @GetMapping("/list")
    public List<TbUser> list() {
        return tbUserService.list();
    }

    @GetMapping("/{id}")
    public TbUser getOne(@PathVariable Long id) {
        return tbUserService.getById(id);
    }
}

完成后提供两个接口:

  • GET /user/list:查询所有用户。
  • GET /user/{id}:按照用户 ID 查询。

6. 创建订单服务

订单服务的 Mapper、Service 和实体类与用户服务结构相同,只是操作 tb_order 表。这里重点说明它和用户服务不同的部分。

6.1 订单服务 pom.xml

创建 demo01_order/pom.xml,基础依赖与用户服务相同,并额外加入 OpenFeign 和 LoadBalancer:

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.example.c</groupId>
        <artifactId>demo01_parent</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <relativePath>../pom.xml</relativePath>
    </parent>

    <artifactId>demo01_order</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </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>

额外依赖的作用:

  • spring-cloud-starter-openfeign:通过 Java 接口声明远程 HTTP 调用。
  • spring-cloud-starter-loadbalancer:根据 Nacos 返回的服务实例选择一个实例;以后增加多个用户服务实例时也能负载均衡。

6.2 订单服务启动类

java 复制代码
package org.example.c.demo01_order;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

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

@EnableFeignClients 会扫描带有 @FeignClient 的接口并生成调用实现。

6.3 订单实体、Mapper 和 Service

实体类的核心结构:

java 复制代码
package org.example.c.demo01_order.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

@Data
@TableName("tb_order")
public class TbOrder {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long userId;
    private String name;
    private Long price;
    private Integer num;
}

Mapper:

java 复制代码
@Mapper
public interface TbOrderMapper extends BaseMapper<TbOrder> {
}

Service:

java 复制代码
public interface TbOrderService extends IService<TbOrder> {
}

实现类:

java 复制代码
@Service
public class TbOrderServiceImpl
        extends ServiceImpl<TbOrderMapper, TbOrder>
        implements TbOrderService {
}

实际创建文件时,需要补齐对应的 package 和 import,写法与用户服务一致。

6.4 创建 OpenFeign 客户端

创建 feignclient/UserFeignClient.java:

java 复制代码
package org.example.c.demo01_order.feignclient;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient("user-service")
public interface UserFeignClient {
    @GetMapping("/user/{id}")
    String getOne(@PathVariable("id") Long id);
}

说明:

  • user-service 必须和用户服务的 spring.application.name 完全一致。
  • 这里没有写 IP 和端口。
  • OpenFeign 会先向 Nacos 查询 user-service 的可用实例,再发送 HTTP 请求。
  • 推荐在 @PathVariable 中明确写参数名,避免某些编译配置下无法推断参数名。

6.5 创建订单接口

java 复制代码
package org.example.c.demo01_order.controller;

import org.example.c.demo01_order.entity.TbOrder;
import org.example.c.demo01_order.feignclient.UserFeignClient;
import org.example.c.demo01_order.service.TbOrderService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/tbOrder")
public class TbOrderController {
    private final TbOrderService tbOrderService;
    private final UserFeignClient userFeignClient;

    public TbOrderController(TbOrderService tbOrderService,
                             UserFeignClient userFeignClient) {
        this.tbOrderService = tbOrderService;
        this.userFeignClient = userFeignClient;
    }

    @GetMapping("/list")
    public List<TbOrder> list() {
        return tbOrderService.list();
    }

    @GetMapping("/{id}")
    public String getOne(@PathVariable Long id) {
        TbOrder order = tbOrderService.getById(id);
        String user = userFeignClient.getOne(order.getUserId());
        return order + System.lineSeparator() + user;
    }
}

访问 /tbOrder/101 时将发生以下过程:

  1. 浏览器请求订单服务。
  2. 订单服务从订单数据库查询订单 101。
  3. 从订单中取得 userId。
  4. OpenFeign 通过 Nacos 找到用户服务。
  5. 调用用户服务的 /user/{id}。
  6. 订单服务组合订单和用户结果后返回浏览器。

7. 配置本地环境和 Compose 环境

同一个程序有两种运行位置:

  • 在 IDEA 中运行:程序在宿主机上。
  • 在 Docker 中运行:程序在 Compose 网络的容器中。

这两种环境访问 MySQL 和 Nacos 的地址不同,所以需要区分配置。

7.1 用户服务 application.yml

demo01_user/src/main/resources/application.yml:

yaml 复制代码
server:
  port: 8081

spring:
  application:
    name: user-service
  datasource:
    url: jdbc:mysql://localhost:3307/spring-cloud-user?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: 123
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848

这是本地默认配置。在 IDEA 中直接运行时,Java 程序通过宿主机映射端口访问容器。

7.2 用户服务 application-compose.yml

demo01_user/src/main/resources/application-compose.yml:

yaml 复制代码
spring:
  datasource:
    url: jdbc:mysql://mysql:3306/spring-cloud-user?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
  cloud:
    nacos:
      discovery:
        server-addr: nacos:8848

7.3 订单服务 application.yml

demo01_order/src/main/resources/application.yml:

yaml 复制代码
server:
  port: 8082

spring:
  application:
    name: order-service
  datasource:
    url: jdbc:mysql://localhost:3307/spring-cloud-order?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: 123
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848

7.4 订单服务 application-compose.yml

demo01_order/src/main/resources/application-compose.yml:

yaml 复制代码
spring:
  datasource:
    url: jdbc:mysql://mysql:3306/spring-cloud-order?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
  cloud:
    nacos:
      discovery:
        server-addr: nacos:8848

7.5 为什么地址不同

text 复制代码
宿主机访问 MySQL:localhost:3307
容器访问 MySQL:mysql:3306

宿主机访问 Nacos:localhost:8848
容器访问 Nacos:nacos:8848

Compose 会为项目创建一个内部网络,并为每个服务提供 DNS 名称。因此,在容器内部:

  • mysql 会解析到 MySQL 容器。
  • nacos 会解析到 Nacos 容器。
  • user-service 会解析到用户服务容器。

不能在容器中使用 localhost 访问其他容器,因为容器中的 localhost 只代表当前容器自己。

application-compose.yml 只有在激活 compose profile 时才加载。Compose 中通过下面的环境变量激活:

yaml 复制代码
environment:
  SPRING_PROFILES_ACTIVE: compose

Spring Boot 会先读取 application.yml,再用 application-compose.yml 中的同名配置覆盖它。


8. 准备 MySQL 初始化脚本

8.1 用户数据库

创建 init/01-user.sql:

sql 复制代码
CREATE DATABASE IF NOT EXISTS `spring-cloud-user`
    DEFAULT CHARACTER SET utf8mb4
    COLLATE utf8mb4_unicode_ci;

USE `spring-cloud-user`;

DROP TABLE IF EXISTS `tb_user`;

CREATE TABLE `tb_user` (
    `id` BIGINT NOT NULL AUTO_INCREMENT,
    `userName` VARCHAR(100) DEFAULT NULL COMMENT '用户名',
    `address` VARCHAR(255) DEFAULT NULL COMMENT '地址',
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_user_name` (`userName`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO `tb_user` (`id`, `userName`, `address`) VALUES
    (1, '柳岩', '湖南省衡阳市'),
    (2, '粒子', '陕西省西安市'),
    (3, '华沉鱼', '湖北省十堰市');

8.2 订单数据库

创建 init/02-order.sql:

sql 复制代码
CREATE DATABASE IF NOT EXISTS `spring-cloud-order`
    DEFAULT CHARACTER SET utf8mb4
    COLLATE utf8mb4_unicode_ci;

USE `spring-cloud-order`;

DROP TABLE IF EXISTS `tb_order`;

CREATE TABLE `tb_order` (
    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '订单 ID',
    `user_id` BIGINT NOT NULL COMMENT '用户 ID',
    `name` VARCHAR(100) DEFAULT NULL COMMENT '商品名称',
    `price` BIGINT NOT NULL COMMENT '商品价格,单位为分',
    `num` INT DEFAULT 0 COMMENT '商品数量',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO `tb_order` (`id`, `user_id`, `name`, `price`, `num`) VALUES
    (101, 1, 'Apple iPhone', 699900, 1),
    (102, 2, '电动车', 209900, 1),
    (103, 3, '运动鞋', 43900, 1);

8.3 Nacos 数据库脚本

Nacos 使用 MySQL 保存配置、命名空间等数据。需要把与所用 Nacos 版本对应的 MySQL 初始化脚本保存为:

text 复制代码
init/nacos.sql

本项目使用 Nacos v2.3.2,应使用对应版本的 mysql-schema.sql 内容。Compose 中的 MYSQL_DATABASE: spring-cloud 会先创建 spring-cloud 数据库,随后 nacos.sql 会在这个数据库中创建 Nacos 所需表。

重要规则:/docker-entrypoint-initdb.d 中的脚本只会在 MySQL 数据目录第一次创建时执行。

如果 MySQL 数据卷已经存在,后来新增或修改 SQL,简单重启容器不会重新执行它。开发环境需要完全重新初始化时,可以使用:

powershell 复制代码
docker compose down -v
docker compose up -d

警告:down -v 会删除 Compose 数据卷,其中的 MySQL 数据会丢失。只有确认数据可以删除时才能执行。


9. 为两个后端编写 Dockerfile

9.1 用户服务 Dockerfile

创建 demo01_user/Dockerfile:

dockerfile 复制代码
FROM eclipse-temurin:17-jre

COPY target/demo01_user-0.0.1-SNAPSHOT.jar /app.jar

EXPOSE 8081

ENTRYPOINT ["java", "-jar", "/app.jar"]

9.2 订单服务 Dockerfile

创建 demo01_order/Dockerfile:

dockerfile 复制代码
FROM eclipse-temurin:17-jre

COPY target/demo01_order-0.0.1-SNAPSHOT.jar /app.jar

EXPOSE 8082

ENTRYPOINT ["java", "-jar", "/app.jar"]

每一行的作用:

  • FROM:以包含 Java 17 运行环境的镜像为基础。
  • COPY:把 Maven 生成的 JAR 复制进镜像。
  • EXPOSE:声明应用监听的端口,主要用于说明;真正映射端口仍由 Compose 的 ports 决定。
  • ENTRYPOINT:容器启动时执行 java -jar /app.jar。

注意:Dockerfile 不会自动执行 Maven 打包。如果 target 中没有 JAR,镜像构建会失败;如果 JAR 是旧的,镜像里运行的也会是旧代码。


10. 编写 Docker Compose

在父工程根目录创建 docker-compose.yml:

yaml 复制代码
services:
  mysql:
    container_name: mysql
    image: mysql:8.0.30
    command: --skip-character-set-client-handshake
    ports:
      - "3307:3306"
    volumes:
      - mysql_data:/var/lib/mysql
      - ./init:/docker-entrypoint-initdb.d
    environment:
      MYSQL_ROOT_PASSWORD: 123
      MYSQL_DATABASE: spring-cloud
    restart: always

  nacos:
    container_name: nacos
    image: nacos/nacos-server:v2.3.2
    ports:
      - "8848:8848"
      - "9848:9848"
    environment:
      MODE: standalone
      SPRING_DATASOURCE_PLATFORM: mysql
      MYSQL_SERVICE_HOST: mysql
      MYSQL_SERVICE_PORT: 3306
      MYSQL_SERVICE_DB_NAME: spring-cloud
      MYSQL_SERVICE_USER: root
      MYSQL_SERVICE_PASSWORD: 123
      NACOS_AUTH_ENABLE: false
      MYSQL_SERVICE_DB_PARAM: characterEncoding=utf8&connectTimeout=10000&socketTimeout=30000&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true
    depends_on:
      - mysql
    restart: always

  user-service:
    container_name: user-service
    build: ./demo01_user
    ports:
      - "8081:8081"
    environment:
      SPRING_PROFILES_ACTIVE: compose
    depends_on:
      - mysql
      - nacos
    restart: always

  order-service:
    container_name: order-service
    build: ./demo01_order
    ports:
      - "8082:8082"
    environment:
      SPRING_PROFILES_ACTIVE: compose
    depends_on:
      - mysql
      - nacos
    restart: always

volumes:
  mysql_data: {}

10.1 ports 的含义

格式是:

text 复制代码
宿主机端口:容器端口

例如:

yaml 复制代码
ports:
  - "3307:3306"

表示:

  • 在 Windows 上访问 MySQL 使用 localhost:3307。
  • 其他容器访问 MySQL 使用 mysql:3306。

10.2 volumes 的含义

yaml 复制代码
- mysql_data:/var/lib/mysql

把数据库数据放进 Docker 命名卷。即使容器被重新创建,数据仍然保留。

yaml 复制代码
- ./init:/docker-entrypoint-initdb.d

把项目中的 init 目录挂载到 MySQL 初始化脚本目录。

10.3 depends_on 的限制

depends_on 只保证容器启动顺序,不保证 MySQL 和 Nacos 已经完全可以接受请求。

本项目给服务设置了 restart: always。如果后端第一次启动时 Nacos 尚未准备好,后端退出后会自动重启,直到连接成功。

更严格的生产配置应给 MySQL 和 Nacos增加 healthcheck,再使用健康状态作为启动条件。学习阶段可以先使用当前配置。

10.4 为什么不需要手动声明 network

Compose 会自动创建一个默认网络,并把当前文件中的所有服务加入该网络,因此容器可以直接用服务名互相访问。


11. 第一次构建和启动

所有命令都在父工程根目录执行。

第一步:打包 Java 项目

powershell 复制代码
mvn clean package -DskipTests

作用:

  • clean:删除之前的 target,避免旧产物干扰。
  • package:编译并生成两个可运行 JAR。
  • -DskipTests:跳过测试执行,适合首次验证环境;正式交付前不应长期跳过测试。

成功后应存在:

text 复制代码
demo01_user/target/demo01_user-0.0.1-SNAPSHOT.jar
demo01_order/target/demo01_order-0.0.1-SNAPSHOT.jar

第二步:检查 Compose 配置

powershell 复制代码
docker compose config

作用:解析 Compose 文件,可以提前发现 YAML 缩进、字段拼写和变量错误。

第三步:构建镜像并启动

powershell 复制代码
docker compose up -d --build

作用:

  • up:创建并启动服务。
  • -d:后台运行。
  • --build:根据最新 JAR 重新构建两个后端镜像。

第一次启动需要下载 MySQL、Nacos 和 Java 基础镜像,耗时会更长。

第四步:查看容器状态

powershell 复制代码
docker compose ps -a

正常结果中,4 个服务都应该是 Up。如果后端显示 Restarting 或 Exited,说明应用启动失败,必须查看日志。

第五步:查看日志

powershell 复制代码
docker compose logs -f user-service order-service

看到类似下面的内容表示启动成功:

text 复制代码
Started Demo01UserApplication
Started Demo01OrderApplication
register finished

按 Ctrl + C 只会退出日志查看,不会停止后台容器。


12. 验证是否搭建成功

12.1 验证 Nacos

浏览器访问:

text 复制代码
http://localhost:8848/nacos/

进入"服务管理 → 服务列表",应该能看到:

text 复制代码
user-service
order-service

12.2 验证用户服务

text 复制代码
http://localhost:8081/user/list
http://localhost:8081/user/1

应该返回用户数据。

12.3 验证订单服务

text 复制代码
http://localhost:8082/tbOrder/list
http://localhost:8082/tbOrder/101

第二个地址不仅查询订单,还会通过 OpenFeign 调用用户服务。如果同时返回订单和用户内容,说明以下链路全部正常:

text 复制代码
浏览器 → 订单服务 → 订单数据库
                   → Nacos → 用户服务 → 用户数据库

注意:访问 http://localhost:8081/ 或 http://localhost:8082/ 可能返回 404,这是正常的,因为示例没有定义根路径 /。


13. 修改代码后的标准操作流程

只修改 Java 代码或 application*.yml 后:

powershell 复制代码
mvn package -DskipTests
docker compose up -d --build
docker compose ps

原因:

  1. 源代码和配置文件需要先进入新的 JAR。
  2. Dockerfile 再把新 JAR 放进新镜像。
  3. Compose 使用新镜像重新创建后端容器。

只执行 docker compose restart 不会重新打包,也不会重新构建镜像,所以代码修改不会生效。

如果只修改 docker-compose.yml:

powershell 复制代码
docker compose up -d

如果修改 Dockerfile、JAR 或构建上下文中的内容:

powershell 复制代码
docker compose up -d --build

14. 常用维护命令

查看服务状态:

powershell 复制代码
docker compose ps -a

查看全部日志:

powershell 复制代码
docker compose logs -f

只查看最近 100 行:

powershell 复制代码
docker compose logs --tail=100 user-service

重启某个服务:

powershell 复制代码
docker compose restart user-service

停止并删除容器,但保留数据库数据:

powershell 复制代码
docker compose down

再次启动:

powershell 复制代码
docker compose up -d

停止并删除容器及数据卷:

powershell 复制代码
docker compose down -v

最后一个命令会删除数据库数据,只用于确认可以清空数据的开发环境。


15. 常见故障排查

15.1 浏览器显示"无法访问此网站"

先执行:

powershell 复制代码
docker compose ps -a

检查后端是否为 Up。如果是 Restarting,继续执行:

powershell 复制代码
docker compose logs --tail=200 user-service
docker compose logs --tail=200 order-service

浏览器访问失败通常只是最终现象,真正原因在应用启动日志中。

15.2 UnknownHostException: host.docker.internal

容器中的配置不应该绕到宿主机访问同一个 Compose 中的服务。应使用:

yaml 复制代码
mysql:3306
nacos:8848

并确认 Compose 激活了:

yaml 复制代码
SPRING_PROFILES_ACTIVE: compose

15.3 Connection refused

常见原因:

  • MySQL 或 Nacos 尚未启动完成。
  • 端口写错。
  • 容器内部错误使用了宿主机映射端口。

容器内部必须使用 MySQL 的 3306,不能使用宿主机的 3307。

15.4 Unknown database

说明数据库没有创建。检查:

  • init 是否正确挂载。
  • SQL 文件是否存在。
  • MySQL 数据卷是不是在添加 SQL 之前就已经创建。

如果允许清空数据,可执行:

powershell 复制代码
docker compose down -v
docker compose up -d

15.5 修改配置后容器仍使用旧配置

最常见原因是只修改了源码,没有重新生成 JAR 和镜像。执行:

powershell 复制代码
mvn package -DskipTests
docker compose up -d --build

15.6 Nacos 中看不到服务

检查:

  1. 后端是否正常运行。
  2. spring.application.name 是否存在。
  3. server-addr 在 Compose 环境是否为 nacos:8848。
  4. Nacos 是否已正常启动。
  5. 日志中是否出现 register finished。

15.7 Feign 报"找不到 user-service 实例"

检查:

  • 用户服务和订单服务是否连接到同一个 Nacos。
  • @FeignClient("user-service") 是否和用户服务名称完全一致。
  • Nacos 服务列表中的用户服务是否健康。
  • 订单服务是否加入了 OpenFeign 和 LoadBalancer 依赖。
  • 启动类是否有 @EnableFeignClients。

15.8 访问根路径返回 404

这是接口路径不正确,不代表服务没启动。当前接口是:

text 复制代码
/user/list
/user/{id}
/tbOrder/list
/tbOrder/{id}

15.9 MySQL 中文乱码

建议同时做到:

  • 数据库和表使用 utf8mb4。
  • JDBC URL 带 useUnicode=true&characterEncoding=utf8。
  • SQL 文件保存为 UTF-8。
  • 不要使用编辑器错误地以 ANSI 或 GBK 保存 SQL。

15.10 端口被占用

如果 8081、8082、8848、9848 或 3307 已被其他程序占用,可以修改 ports 左侧的宿主机端口。例如:

yaml 复制代码
ports:
  - "18081:8081"

此时浏览器访问 http://localhost:18081,但容器内部端口和 Spring Boot 的 server.port 仍然是 8081。


16. 本地运行与全容器运行的区别

方案 A:MySQL、Nacos 在 Docker,后端在 IDEA

先启动基础设施:

powershell 复制代码
docker compose up -d mysql nacos

然后分别在 IDEA 中运行两个启动类。此时使用默认 application.yml:

text 复制代码
MySQL:localhost:3307
Nacos:localhost:8848

方案 B:所有服务都在 Docker

powershell 复制代码
mvn package -DskipTests
docker compose up -d --build

此时 Compose 设置 SPRING_PROFILES_ACTIVE=compose,应用读取 application-compose.yml:

text 复制代码
MySQL:mysql:3306
Nacos:nacos:8848

不要同时在 IDEA 和 Docker 中启动相同端口的后端,否则会发生端口冲突。


17. 从零搭建检查清单

按顺序逐项确认:

  • 已安装 JDK 17、Maven 和 Docker Desktop。
  • Docker Desktop 已启动。
  • 父工程 pom.xml 使用 pom 打包,并声明两个模块。
  • 父工程统一管理 Spring Cloud、Nacos、MySQL 和 MyBatis Plus 版本。
  • 用户服务端口为 8081,服务名为 user-service。
  • 订单服务端口为 8082,服务名为 order-service。
  • 两个服务都加入 Nacos discovery 依赖。
  • 订单服务加入 OpenFeign 和 LoadBalancer 依赖。
  • 订单启动类加入 @EnableFeignClients。
  • Feign 服务名与 spring.application.name 完全一致。
  • 本地配置使用 localhost:3307 和 localhost:8848。
  • Compose 配置使用 mysql:3306 和 nacos:8848。
  • Compose 激活 SPRING_PROFILES_ACTIVE=compose。
  • init 目录中包含用户、订单和 Nacos SQL。
  • 两个 Dockerfile 中的 JAR 名称与 Maven 产物一致。
  • 已运行 mvn clean package -DskipTests。
  • 已运行 docker compose config。
  • 已运行 docker compose up -d --build。
  • docker compose ps -a 显示 4 个容器都是 Up。
  • Nacos 中能看到两个服务。
  • 用户列表接口返回 200。
  • 订单详情接口可以同时获得订单和用户信息。

18. 推荐的学习顺序

为了真正理解这套项目,建议按下面顺序练习:

  1. 先只用 Spring Boot 写一个返回字符串的接口。
  2. 加入 MySQL 和 MyBatis Plus,让接口能查询数据库。
  3. 复制出第二个独立 Spring Boot 服务。
  4. 启动 Nacos,让两个服务完成注册。
  5. 在订单服务中加入 OpenFeign,通过服务名调用用户服务。
  6. 分别为两个后端构建 Docker 镜像。
  7. 用 Compose 统一启动 MySQL、Nacos 和两个后端。
  8. 理解本地地址与容器服务名的区别。
  9. 最后再增加网关、配置中心、熔断限流和认证等能力。

完成本文后,你已经搭建的是一个最小但完整的微服务闭环:独立服务、独立数据、注册发现、服务调用、容器化和统一编排都已具备。


第二部分:加入服务集群、Gateway 与 Sentinel

前面的项目已经具备 MySQL、Nacos、用户服务、订单服务、OpenFeign 和 Docker Compose。本部分继续将它升级为:

text 复制代码
浏览器
   │
   ▼
Gateway:localhost:8222
   ├─ /tbUser/**  ──> user-service 集群
   │                     ├─ user-1:8081
   │                     └─ user-2:8083
   │
   └─ /tbOrder/** ──> order-service:8082
                              │
                              └─ OpenFeign ──> user-service 集群

19. 升级后的服务和端口

Compose 服务 应用名称 宿主机端口 容器端口 作用
mysql MySQL 3307 3306 保存业务数据和 Nacos 数据
nacos Nacos 8848、9848 8848、9848 服务注册与发现
sentinel Sentinel Dashboard 8858 8858 流量监控和规则管理
user-1 user-service 8081 8081 用户服务实例 1
user-2 user-service 8083 8083 用户服务实例 2
order-service order-service 8082 8082 订单服务
gateway-service gateway-service 8222 8222 系统统一入口

这里的"集群"是业务服务集群:两个用户服务实例使用同一个服务名 user-service。当前 Nacos 仍使用:

yaml 复制代码
MODE: standalone

所以 Nacos 本身不是集群。真正的 Nacos 集群需要部署多个 Nacos 节点。

20. 创建 Gateway 模块

20.1 把 Gateway 加入父工程

在父工程 pom.xml 中加入模块:

xml 复制代码
<modules>
    <module>demo01_user</module>
    <module>demo01_order</module>
    <module>demo01_gateway</module>
</modules>

目录结构:

text 复制代码
demo01_gateway/
├─ pom.xml
├─ Dockerfile
└─ src/main/
   ├─ java/org/example/c/demo01_gateway/
   │  └─ Demo01GatewayApplication.java
   └─ resources/
      ├─ application.yml
      └─ application-compose.yml

20.2 Gateway 依赖

demo01_gateway/pom.xml 的关键依赖:

xml 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>

    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-loadbalancer</artifactId>
    </dependency>

    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

各依赖的作用:

  • spring-cloud-starter-gateway:提供路由、谓词和过滤器能力。
  • nacos-discovery:让 Gateway 注册到 Nacos,并查询后端实例。
  • loadbalancer:处理 lb://服务名 并选择一个实例。
  • sentinel:监控 Gateway 流量并执行限流规则。

Gateway 使用 WebFlux 响应式模型,不要再加入 spring-boot-starter-web,否则可能产生 MVC 与 WebFlux 冲突。Gateway 不直接调用 Feign 接口,通常也不需要 OpenFeign 依赖。

构建插件:

xml 复制代码
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

20.3 Gateway 启动类

java 复制代码
package org.example.c.demo01_gateway;

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

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

当前版本引入 Nacos Discovery starter 后,会自动启用服务注册与发现,因此 @EnableDiscoveryClient 可以省略。

21. 配置 Gateway 路由

21.1 本地配置

demo01_gateway/src/main/resources/application.yml:

yaml 复制代码
server:
  port: 8222

spring:
  application:
    name: gateway-service
  cloud:
    gateway:
      routes:
        - id: user-service-route
          uri: lb://user-service
          predicates:
            - Path=/tbUser/**

        - id: order-service-route
          uri: lb://order-service
          predicates:
            - Path=/tbOrder/**
    nacos:
      discovery:
        server-addr: localhost:8848
    sentinel:
      transport:
        dashboard: localhost:8858
        port: 8719

核心配置解释:

  • id 是路由的唯一名称。
  • Path 决定什么请求匹配这条路由。
  • lb://user-service 表示从 Nacos 获取 user-service 的实例,并进行负载均衡。
  • Gateway 默认保留原始路径,因此 /tbUser/list 转发后仍是 /tbUser/list。
  • Gateway 的 Path 必须与后端 Controller 的 @RequestMapping 一致。

例如用户 Controller 为:

java 复制代码
@RequestMapping("/tbUser")

那么 Gateway 应匹配:

yaml 复制代码
- Path=/tbUser/**

可以配置自动路由:

yaml 复制代码
spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true

但自动路由会形成带服务名的访问路径,控制范围也更宽。初学和正式项目推荐优先使用显式 routes。

21.2 Compose 专用配置

demo01_gateway/src/main/resources/application-compose.yml:

yaml 复制代码
spring:
  cloud:
    nacos:
      discovery:
        server-addr: nacos:8848
    sentinel:
      transport:
        dashboard: sentinel:8858
        port: 8719

本地运行使用 localhost,容器运行使用 Compose 服务名:

text 复制代码
本地:localhost:8848、localhost:8858
容器:nacos:8848、sentinel:8858

22. 创建两个用户服务实例

22.1 让应用端口可覆盖

用户服务 application-compose.yml:

yaml 复制代码
server:
  port: ${SERVER_PORT:8081}

spring:
  datasource:
    url: jdbc:mysql://mysql:3306/spring-cloud-user?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
  cloud:
    nacos:
      discovery:
        server-addr: nacos:8848
    sentinel:
      transport:
        dashboard: sentinel:8858
        port: 8719

${SERVER_PORT:8081} 表示:存在 SERVER_PORT 环境变量时使用该值,否则使用 8081。

22.2 在 Compose 中复用同一个镜像

yaml 复制代码
user-1:
  container_name: user-1
  build: ./demo01_user
  ports:
    - "8081:8081"
  environment:
    SPRING_PROFILES_ACTIVE: compose
    SERVER_PORT: 8081
  depends_on:
    - mysql
    - nacos
  restart: always

user-2:
  container_name: user-2
  build: ./demo01_user
  ports:
    - "8083:8083"
  environment:
    SPRING_PROFILES_ACTIVE: compose
    SERVER_PORT: 8083
  depends_on:
    - mysql
    - nacos
  restart: always

两个容器使用同一份代码和镜像,但端口不同。因为基础配置都是:

yaml 复制代码
spring:
  application:
    name: user-service

所以 Nacos 中表现为一个服务、两个实例。

22.3 显示当前实例端口

为了验证负载均衡,可以让接口返回当前端口:

java 复制代码
@Value("${server.port}")
private String port;

@GetMapping("/{id}")
public String getOne(@PathVariable Long id) {
    TbUser user = tbUserService.getById(id);
    return user + System.lineSeparator() + "port=" + port;
}

连续访问:

text 复制代码
http://localhost:8222/tbUser/1

响应中的端口会出现 8081 或 8083,说明 Gateway 从两个健康实例中选择了一个。

23. OpenFeign 调用用户集群

订单服务仍然只声明服务名,不需要知道有几个用户实例:

java 复制代码
@FeignClient("user-service")
public interface UserFeignClient {
    @GetMapping("/tbUser/{id}")
    String getOne(@PathVariable("id") Long id);
}

调用过程:

text 复制代码
order-service
   ├─ 向 Nacos 查询 user-service
   ├─ 获得 user-1:8081 和 user-2:8083
   ├─ LoadBalancer 选择一个健康实例
   └─ OpenFeign 请求 /tbUser/{id}

不要在 Feign 中写固定的 url=http://user-1:8081,否则会绕过 Nacos 和负载均衡。

24. Sentinel 的组成与接入

Sentinel 在本项目中包含两部分:

  1. Sentinel 客户端:位于 Gateway、用户服务和订单服务中,真正执行流量统计、限流和熔断。
  2. Sentinel Dashboard:独立控制台,用于查看应用和下发规则。

给三个应用加入:

xml 复制代码
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

Compose 环境配置:

yaml 复制代码
spring:
  cloud:
    sentinel:
      transport:
        dashboard: sentinel:8858
        port: 8719

说明:

  • dashboard 是控制台地址。
  • port 是 Sentinel 客户端命令通信端口,不是业务 HTTP 端口。
  • 每个容器有独立网络空间,多个容器使用 8719 通常不会冲突。
  • Dashboard 不是业务请求的代理,流量不会先经过 Dashboard。

Compose 中加入 Sentinel:

yaml 复制代码
sentinel:
  container_name: sentinel
  image: registry.cn-beijing.aliyuncs.com/all100/sentinel-dashboard:1.8.6
  ports:
    - "8858:8858"
  environment:
    JAVA_OPTS: "-Dserver.port=8858 -Dcsp.sentinel.dashboard.server=localhost:8858"
  restart: always

浏览器访问:

text 复制代码
http://localhost:8858

如果该镜像保留默认登录信息,通常为 sentinel/sentinel;如果不能登录,应以所用镜像的实际说明为准。

25. 升级后的完整 Compose 配置

yaml 复制代码
services:
  mysql:
    container_name: mysql
    image: mysql:8.0.30
    command: --skip-character-set-client-handshake
    ports:
      - "3307:3306"
    volumes:
      - mysql_data:/var/lib/mysql
      - ./init:/docker-entrypoint-initdb.d
    environment:
      MYSQL_ROOT_PASSWORD: 123
      MYSQL_DATABASE: spring-cloud
    restart: always

  nacos:
    container_name: nacos
    image: nacos/nacos-server:v2.3.2
    ports:
      - "8848:8848"
      - "9848:9848"
    environment:
      MODE: standalone
      SPRING_DATASOURCE_PLATFORM: mysql
      MYSQL_SERVICE_HOST: mysql
      MYSQL_SERVICE_PORT: 3306
      MYSQL_SERVICE_DB_NAME: spring-cloud
      MYSQL_SERVICE_USER: root
      MYSQL_SERVICE_PASSWORD: 123
      NACOS_AUTH_ENABLE: false
      MYSQL_SERVICE_DB_PARAM: characterEncoding=utf8&connectTimeout=10000&socketTimeout=30000&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true
    depends_on:
      - mysql
    restart: always

  sentinel:
    container_name: sentinel
    image: registry.cn-beijing.aliyuncs.com/all100/sentinel-dashboard:1.8.6
    ports:
      - "8858:8858"
    environment:
      JAVA_OPTS: "-Dserver.port=8858 -Dcsp.sentinel.dashboard.server=localhost:8858"
    restart: always

  user-1:
    container_name: user-1
    build: ./demo01_user
    ports:
      - "8081:8081"
    environment:
      SPRING_PROFILES_ACTIVE: compose
      SERVER_PORT: 8081
    depends_on:
      - mysql
      - nacos
      - sentinel
    restart: always

  user-2:
    container_name: user-2
    build: ./demo01_user
    ports:
      - "8083:8083"
    environment:
      SPRING_PROFILES_ACTIVE: compose
      SERVER_PORT: 8083
    depends_on:
      - mysql
      - nacos
      - sentinel
    restart: always

  order-service:
    container_name: order-service
    build: ./demo01_order
    ports:
      - "8082:8082"
    environment:
      SPRING_PROFILES_ACTIVE: compose
    depends_on:
      - mysql
      - nacos
      - sentinel
    restart: always

  gateway-service:
    container_name: gateway-service
    build: ./demo01_gateway
    ports:
      - "8222:8222"
    environment:
      SPRING_PROFILES_ACTIVE: compose
    depends_on:
      - nacos
      - sentinel
    restart: always

volumes:
  mysql_data: {}

新版 Compose 不再需要顶层 version: '3.8'。depends_on 只保证启动顺序,不保证依赖已经健康,因此开发环境可配合 restart: always,生产环境应增加 healthcheck。

26. Gateway Dockerfile

demo01_gateway/Dockerfile:

dockerfile 复制代码
FROM eclipse-temurin:17-jre

COPY target/demo01_gateway-0.0.1-SNAPSHOT.jar /app.jar

EXPOSE 8222

ENTRYPOINT ["java", "-jar", "/app.jar"]

27. 重新构建与启动

在父工程根目录执行:

powershell 复制代码
mvn clean package -DskipTests
docker compose config
docker compose up -d --build
docker compose ps -a

三个 JAR 应分别生成在:

text 复制代码
demo01_user/target/demo01_user-0.0.1-SNAPSHOT.jar
demo01_order/target/demo01_order-0.0.1-SNAPSHOT.jar
demo01_gateway/target/demo01_gateway-0.0.1-SNAPSHOT.jar

查看日志:

powershell 复制代码
docker compose logs --tail=100 gateway-service user-1 user-2 order-service

正常情况下应看到 7 个容器为 Up:

text 复制代码
mysql
nacos
sentinel
user-1
user-2
order-service
gateway-service

28. 按顺序验证

28.1 验证 Nacos

访问:

text 复制代码
http://localhost:8848/nacos/

应看到:

text 复制代码
gateway-service:1 个实例
order-service:1 个实例
user-service:2 个实例

28.2 直接验证后端

text 复制代码
http://localhost:8081/tbUser/1
http://localhost:8083/tbUser/1
http://localhost:8082/tbOrder/list

28.3 通过 Gateway 验证

text 复制代码
http://localhost:8222/tbUser/list
http://localhost:8222/tbUser/1
http://localhost:8222/tbOrder/list
http://localhost:8222/tbOrder/101

28.4 验证集群容错

先停止一个用户实例:

powershell 复制代码
docker compose stop user-1

等待 Nacos 更新实例状态后,再访问 Gateway。请求应由 user-2 处理。测试完成后恢复:

powershell 复制代码
docker compose start user-1

29. 使用 Sentinel 配置限流

Sentinel 通常要在应用产生请求后才会上报资源。因此先访问几次:

text 复制代码
http://localhost:8222/tbUser/list
http://localhost:8222/tbOrder/list
http://localhost:8081/tbUser/list
http://localhost:8082/tbOrder/list

再打开:

text 复制代码
http://localhost:8858

给用户接口添加 QPS 限流:

  1. 左侧选择 user-service。
  2. 打开"簇点链路"。
  3. 找到实际显示的资源,例如 /tbUser/list。
  4. 点击"流控"。
  5. 阈值类型选择 QPS。
  6. 单机阈值设置为 1。
  7. 流控模式选择"直接"。
  8. 流控效果选择"快速失败"。
  9. 保存并快速刷新接口验证。

Gateway 限流时,应选择 gateway-service,再根据控制台实际显示的路由资源添加规则。建议路由使用清晰、固定的 ID:

yaml 复制代码
id: user-service-route
id: order-service-route

两个 user-service 实例归属于同一个应用,但单机 QPS 通常按每个实例分别计算。两个实例各限制 QPS 1,不等于整个集群总 QPS 1。集群共享总阈值属于 Sentinel 集群流控。

Sentinel 规则是否永久保存

Dashboard 中直接创建的规则通常保存在客户端内存中:

  • 应用重启后可能丢失。
  • 适合学习和测试。
  • 不等同于规则持久化。

生产项目通常将 Sentinel 规则持久化到 Nacos,需要加入 Sentinel Nacos 数据源依赖,并在 Nacos 中保存规则。

30. 限流、熔断与降级的区别

  • 限流:请求过多时拒绝一部分请求,保护系统容量。
  • 熔断:下游持续慢调用或错误时,暂时停止继续调用,避免故障扩散。
  • 降级:主逻辑不可用时返回简化或友好的替代结果。

Sentinel 能触发限流和熔断,但要返回友好业务内容,通常还需要编写 BlockHandler、Feign fallback 或 Gateway 异常响应处理。

31. 升级后常见故障

Gateway 返回 404

检查:

  • 请求路径是否匹配 Path。
  • Gateway 转发路径是否与 Controller 的 @RequestMapping 一致。
  • 修改配置后是否重新打包并重建镜像。

Gateway 返回 503

通常表示找不到可用服务实例。检查:

  • Nacos 是否存在目标服务。
  • 实例是否健康。
  • lb://user-service 是否与 spring.application.name 完全一致。
  • Gateway 是否能访问 nacos:8848。

Sentinel 中看不到应用

检查:

  • 应用是否加入 Sentinel starter。
  • Compose 配置是否使用 sentinel:8858。
  • 应用启动后是否真正收到过请求。
  • Sentinel 容器是否为 Up。

Sentinel 规则没有效果

检查:

  • 是否选对应用和资源。
  • 测试请求是否经过配置规则的组件。
  • 阈值是否足够低。
  • Gateway 规则和后端规则是否混淆。

Feign 调用返回 404

Feign 路径必须和用户 Controller 一致:

java 复制代码
@GetMapping("/tbUser/{id}")

修改配置后没有生效

powershell 复制代码
mvn package -DskipTests
docker compose up -d --build

只执行 docker compose restart 不会生成新 JAR,也不会构建新镜像。

32. 第二阶段验收清单

  • 父工程包含 demo01_gateway 模块。
  • Gateway 加入 Gateway、Nacos、LoadBalancer 和 Sentinel 依赖。
  • Gateway 没有混用 spring-boot-starter-web。
  • 用户路由指向 lb://user-service。
  • 订单路由指向 lb://order-service。
  • 路由 Path 与后端 Controller 路径一致。
  • user-1 和 user-2 使用同一份镜像。
  • 两个实例分别监听 8081 和 8083。
  • 两个实例都以 user-service 注册到 Nacos。
  • Nacos 中 user-service 显示两个健康实例。
  • OpenFeign 使用服务名调用用户集群。
  • Gateway、用户服务和订单服务已加入 Sentinel starter。
  • Compose 中使用 sentinel:8858。
  • Sentinel 控制台可以通过 8858 打开。
  • 通过 8222 可以访问用户和订单接口。
  • 响应中的端口可以证明负载均衡生效。
  • 停止一个用户实例后,另一个实例仍能响应。
  • 产生流量后,Sentinel 能显示应用和资源。
  • 低 QPS 规则能够触发限流。
  • 已理解 Dashboard 临时规则默认不会持久保存。

33. 下一阶段建议

  1. 将 Sentinel 规则持久化到 Nacos。
  2. 为 OpenFeign 添加 fallback。
  3. 给 Gateway 添加统一异常响应和跨域配置。
  4. 添加 JWT 登录认证与 Gateway 鉴权过滤器。
  5. 使用 Nacos 配置中心管理公共配置。
  6. 给 Compose 服务增加 healthcheck。
  7. 引入 Prometheus 和 Grafana。
  8. 学习真正的 Nacos 多节点集群。

34. Spring Cloud 完整工作流程

本项目可以用下面这条链路概括:

text 复制代码
客户端
  ↓
Gateway 统一入口
  ↓
Nacos 查询服务实例
  ↓
LoadBalancer 选择实例
  ↓
用户服务或订单服务
  ↓
MyBatis Plus 查询 MySQL
  ↓
结果沿原路径返回客户端

整个过程中 Sentinel 持续统计流量,并执行限流、熔断等规则。

34.1 系统启动阶段

执行:

powershell 复制代码
mvn package -DskipTests
docker compose up -d --build

系统按以下过程启动:

  1. Maven 将用户、订单和 Gateway 打包成 JAR。
  2. Docker 根据各模块的 Dockerfile 构建镜像。
  3. Compose 创建内部网络并启动 MySQL、Nacos、Sentinel、两个用户实例、订单服务和 Gateway。
  4. MySQL 第一次创建数据卷时执行 init 目录中的 SQL。
  5. 每个 Spring Boot 应用读取 application.yml,再由 application-compose.yml 覆盖容器专用配置。
  6. 应用通过 nacos:8848 连接 Nacos。
  7. Gateway、订单服务和两个用户实例把服务名、容器 IP、端口、健康状态注册到 Nacos。
  8. 应用中的 Sentinel 客户端连接 sentinel:8858,准备上报资源和接收规则。

Nacos 中最终大致形成:

text 复制代码
gateway-service
  └─ 172.x.x.x:8222

order-service
  └─ 172.x.x.x:8082

user-service
  ├─ 172.x.x.x:8081
  └─ 172.x.x.x:8083

两个用户容器的名称分别是 user-1 和 user-2,但它们的 spring.application.name 都是 user-service,因此在 Nacos 中属于同一个服务的两个实例。

34.2 客户端访问用户接口

客户端请求:

text 复制代码
GET http://localhost:8222/tbUser/1

完整过程:

  1. 请求先到达 Gateway 的 8222 端口。
  2. Gateway 使用 Path=/tbUser/** 匹配用户路由。
  3. 路由目标 lb://user-service 表示不能使用固定地址,需要查询服务实例。
  4. Gateway 从 Nacos 获取两个健康的 user-service 实例。
  5. LoadBalancer 从 8081 和 8083 实例中选择一个。
  6. Gateway 把 /tbUser/1 转发给选中的用户服务。
  7. 用户服务的 Controller 接收请求并调用 Service。
  8. Service 通过 MyBatis Plus 和 Mapper 查询 spring-cloud-user.tb_user。
  9. 用户服务把查询结果返回 Gateway。
  10. Gateway 再把结果返回客户端。

流程图:

text 复制代码
浏览器 :8222
   ↓
Gateway 匹配 /tbUser/**
   ↓
Nacos 返回 user-service 实例列表
   ↓
LoadBalancer 选择 user-1 或 user-2
   ↓
TbUserController
   ↓
TbUserService
   ↓
TbUserMapper
   ↓
MySQL spring-cloud-user

34.3 客户端访问订单接口

客户端请求:

text 复制代码
GET http://localhost:8222/tbOrder/101

完整过程:

  1. Gateway 使用 Path=/tbOrder/** 匹配订单路由。
  2. Gateway 根据 lb://order-service 从 Nacos 找到订单服务。
  3. 请求被转发到 order-service:8082。
  4. 订单服务从 spring-cloud-order.tb_order 查询订单 101。
  5. 订单数据中包含 userId。
  6. 订单服务调用 UserFeignClient.getOne(userId)。
  7. OpenFeign 根据 @FeignClient("user-service") 查找目标服务。
  8. Nacos 返回两个用户服务实例。
  9. LoadBalancer 选择其中一个实例。
  10. OpenFeign 向该实例发送 /tbUser/{id} 请求。
  11. 用户服务查询用户数据库并返回用户信息。
  12. 订单服务组合订单和用户结果,再经过 Gateway 返回客户端。

流程图:

text 复制代码
浏览器
  ↓
Gateway
  ↓
order-service
  ├─ MyBatis Plus ──> 订单数据库
  │
  └─ OpenFeign
       ↓
     Nacos 查询 user-service
       ↓
     LoadBalancer 选择用户实例
       ↓
     user-service ──> 用户数据库
  ↓
组合订单和用户信息
  ↓
Gateway 返回客户端

34.4 各组件分别负责什么

组件 在项目中的职责
Spring Boot 创建并运行每个独立后端应用
Spring Cloud Gateway 提供统一入口,根据路径转发请求
Nacos Discovery 保存服务名与实例地址,提供注册和发现
Spring Cloud LoadBalancer 从同一服务的多个健康实例中选择一个
OpenFeign 让订单服务通过 Java 接口调用用户服务
Sentinel Client 统计接口和路由流量,执行限流与熔断规则
Sentinel Dashboard 查看应用和资源,下发流控规则
MyBatis Plus 简化实体、Mapper、Service 和数据库 CRUD
MySQL 保存用户、订单和 Nacos 数据
Dockerfile 把每个 Java 应用制作成镜像
Docker Compose 创建网络并统一启动、停止所有容器

34.5 Sentinel 在调用链中的位置

Sentinel 不负责路由,也不是所有请求都必须经过的代理。Sentinel 客户端运行在应用内部:

text 复制代码
请求进入 Gateway
  ↓
Gateway Sentinel 检查路由规则
  ↓
请求进入后端
  ↓
后端 Sentinel 检查接口规则
  ↓
继续执行或立即拒绝

如果请求量超过规则阈值,Sentinel 会快速拒绝部分请求,避免请求继续占用线程、连接和数据库资源。Dashboard 主要用于观察和配置规则,真正执行规则的是各应用中的 Sentinel 客户端。

34.6 一个实例停止后会怎样

例如停止 user-1:

powershell 复制代码
docker compose stop user-1

随后发生:

  1. user-1 不再向 Nacos维持健康状态。
  2. Nacos 将该实例从健康实例列表中移除或标记为不健康。
  3. Gateway 和订单服务更新本地服务列表。
  4. LoadBalancer 不再选择 user-1。
  5. 后续请求继续由 user-2 处理。

这就是多实例部署提供的基本容错能力。但如果所有用户实例都停止,Gateway 或 Feign 将无法找到可用实例;要返回友好结果,还需要继续配置 Gateway 异常处理或 Feign fallback。

34.7 配置和地址的工作方式

同一个应用在不同运行环境中使用不同地址:

text 复制代码
IDEA 本地运行:localhost:3307、localhost:8848、localhost:8858
Compose 容器运行:mysql:3306、nacos:8848、sentinel:8858

Compose 通过下面的环境变量激活容器配置:

yaml 复制代码
SPRING_PROFILES_ACTIVE: compose

Spring Boot 先加载公共的 application.yml,再加载 application-compose.yml 覆盖同名配置。容器通过 Compose 内部 DNS 使用服务名互相访问,不能使用 localhost 访问其他容器。

34.8 完整流程总结

text 复制代码
启动阶段:
Compose 启动容器
→ Spring Boot 加载 compose 配置
→ 服务注册到 Nacos
→ Sentinel 客户端连接 Dashboard

请求阶段:
客户端请求 Gateway
→ Gateway 根据 Path 匹配路由
→ Nacos 提供目标服务实例
→ LoadBalancer 选择实例
→ 后端执行业务并查询 MySQL
→ 如果需要其他服务,OpenFeign 再次经过服务发现和负载均衡
→ 响应经 Gateway 返回客户端

治理阶段:
Sentinel 统计 Gateway 和后端资源流量
→ Dashboard 展示调用情况
→ 管理员添加限流或熔断规则
→ Sentinel 客户端在应用内部执行规则

因此,Spring Cloud 并不是一个单独运行的软件,而是一组协作组件:Nacos 解决"服务在哪里",Gateway 解决"请求从哪里进入并转发到哪里",LoadBalancer 解决"多个实例选择哪个",OpenFeign 解决"服务之间如何方便地调用",Sentinel 解决"流量过大或下游异常时如何保护系统"。


35. 使用 Nacos 远端配置中心

前面使用 Nacos 完成了服务注册与发现。现在用户服务又使用 Nacos Config 作为远端配置中心。

这两个功能虽然由同一个 Nacos 提供,但用途不同:

功能 解决的问题 当前项目示例
Nacos Discovery 服务在哪里、有几个健康实例 user-service 注册 8081 和 8083 两个实例
Nacos Config 应用应该使用什么配置 保存数据库地址和 record.num

远端配置中心的价值是:公共配置不必全部写死在 JAR 中,修改部分配置时也不必重新打包镜像。

35.1 添加 Nacos Config 依赖

在 demo01_user/pom.xml 中加入:

xml 复制代码
<!-- Nacos 配置中心 -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

两个 Nacos starter 的职责不要混淆:

xml 复制代码
<!-- 服务注册与发现 -->
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>

<!-- 读取远端配置 -->
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>

只添加 Discovery 依赖不能读取远端配置;只添加 Config 依赖也不能完成服务注册。

35.2 在 Nacos 中创建配置

打开 Nacos 控制台:

text 复制代码
http://localhost:8848/nacos/

进入:

text 复制代码
配置管理 → 配置列表 → 新建配置

按照当前项目填写:

text 复制代码
Data ID:spring-cloud-user-dev.yml
Group:DEFAULT_GROUP
配置格式:YAML

配置内容:

yaml 复制代码
spring:
  datasource:
    url: jdbc:mysql://mysql:3306/spring-cloud-user?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai

record:
  num: 100

字段含义:

  • spring.datasource.url:用户服务在 Compose 网络中使用的数据库地址。
  • record.num:用于演示远端配置读取和动态刷新的自定义配置。
  • Data ID 相当于远端配置文件名,导入时必须完全一致。
  • Group 用于对配置进行分组;没有特别指定时通常使用 DEFAULT_GROUP。

配置中心适合保存连接地址、功能开关、业务阈值等配置。数据库密码、令牌等敏感内容在正式环境中还需要结合权限、加密或专门的密钥管理系统,不应直接公开保存。

35.3 用户服务导入远端配置

demo01_user/src/main/resources/application-compose.yml 的核心配置:

yaml 复制代码
server:
  port: ${SERVER_PORT:8082}

spring:
  cloud:
    nacos:
      discovery:
        server-addr: nacos:8848
      config:
        server-addr: nacos:8848
  config:
    import:
      - nacos:spring-cloud-user-dev.yml
  sentinel:
    transport:
      dashboard: sentinel:8858
      port: 8719

关键配置解释:

  • nacos.discovery.server-addr:服务注册中心地址。
  • nacos.config.server-addr:配置中心地址。
  • 当前两个地址都是 nacos:8848,但分别服务于注册发现和配置读取。
  • spring.config.import:告诉 Spring Boot 在启动期间导入指定的 Nacos 配置。
  • nacos:spring-cloud-user-dev.yml:导入 Data ID 为 spring-cloud-user-dev.yml 的配置。
  • Compose 中的 SERVER_PORT=8081 或 SERVER_PORT=8083 会分别决定两个用户实例的端口。

为了减少混淆,可以把默认端口写成 8081:

yaml 复制代码
server:
  port: ${SERVER_PORT:8081}

当前 Compose 已为两个用户容器明确设置 SERVER_PORT,因此即使默认值是 8082,容器实际仍分别监听 8081 和 8083。但默认值改成 8081 更符合用户服务的语义。

35.4 为什么使用 spring.config.import

较新的 Spring Boot 和 Spring Cloud 使用 Config Data 机制加载外部配置:

yaml 复制代码
spring:
  config:
    import:
      - nacos:spring-cloud-user-dev.yml

应用启动时的过程是:

text 复制代码
读取本地 application.yml
       ↓
激活 compose profile
       ↓
读取 application-compose.yml
       ↓
发现 spring.config.import
       ↓
连接 nacos:8848
       ↓
读取 DEFAULT_GROUP/spring-cloud-user-dev.yml
       ↓
把远端属性加入 Spring Environment
       ↓
创建 DataSource、Controller 等 Bean

因此,配置必须在相关 Bean 初始化之前成功加载。假如 Nacos 不可用或 Data ID 不存在,应用可能在启动阶段直接失败。

如果远端配置是可选的,可以使用:

yaml 复制代码
spring:
  config:
    import:
      - optional:nacos:spring-cloud-user-dev.yml

optional: 表示远端配置不存在或暂时不可用时,应用仍可以尝试使用本地配置启动。是否使用可选模式,应根据该配置是不是应用启动必需项决定。

35.5 本地运行时关闭导入检查

当前默认 application.yml 中配置了:

yaml 复制代码
spring:
  cloud:
    nacos:
      config:
        import-check:
          enabled: false

原因是:

  • 用户服务已经加入 Nacos Config starter。
  • Compose profile 中配置了 spring.config.import。
  • 本地或测试环境暂时不强制依赖远端配置。
  • 关闭导入检查可以避免本地环境因为没有声明 Nacos import 而直接报错。

这只是关闭"必须声明 import"的校验,不代表 Compose 环境不会读取远端配置。Compose 激活后,application-compose.yml 仍会显式导入 Nacos 配置。

35.6 在代码中读取远端配置

用户 Controller 中加入:

java 复制代码
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;

@RestController
@RequestMapping("/tbUser")
@RefreshScope
public class TbUserController {

    @Value("${server.port}")
    private String port;

    @Value("${record.num}")
    private int recordNum;

    // 省略其他代码
}

在接口响应中输出配置:

java 复制代码
@GetMapping("/{id}")
public String getOne(@PathVariable Long id) {
    TbUser user = tbUserService.getById(id);
    return user + System.lineSeparator()
            + port + System.lineSeparator()
            + recordNum;
}

作用说明:

  • @Value("${record.num}") 从 Spring Environment 中读取 record.num。
  • 该属性来自 Nacos 中的 spring-cloud-user-dev.yml。
  • @RefreshScope 允许这个 Bean 在远端配置变化后重新创建,从而读取新值。
  • 没有 @RefreshScope 时,普通 @Value 字段通常只在 Bean 创建时赋值,远端修改后不一定动态更新。

如果配置项较多,推荐使用 @ConfigurationProperties 将同一前缀的配置集中映射到一个类,而不是声明大量 @Value。

35.7 构建并启动

修改依赖和配置后必须重新打包、重建镜像:

powershell 复制代码
mvn package -DskipTests
docker compose up -d --build user-1 user-2
docker compose ps -a

查看用户服务日志:

powershell 复制代码
docker compose logs --tail=150 user-1 user-2

重点检查:

  • 两个应用是否成功启动。
  • 是否成功连接 nacos:8848。
  • 是否存在 Data ID 找不到、YAML 格式错误或 record.num 无法绑定等异常。

35.8 验证远端配置读取

分别直接访问两个实例:

text 复制代码
http://localhost:8081/tbUser/1
http://localhost:8083/tbUser/1

也可以通过 Gateway:

text 复制代码
http://localhost:8222/tbUser/1

响应中应该包含:

text 复制代码
用户信息
8081 或 8083
100

其中:

  • 8081 或 8083 证明请求落到了哪个用户实例。
  • 100 证明 record.num 已从 Nacos 远端配置读取成功。

35.9 验证动态刷新

在 Nacos 控制台打开:

text 复制代码
spring-cloud-user-dev.yml

把:

yaml 复制代码
record:
  num: 100

修改为:

yaml 复制代码
record:
  num: 200

点击发布,然后重新访问:

text 复制代码
http://localhost:8222/tbUser/1

如果响应从 100 变成 200,并且没有重新打包或重启容器,说明:

text 复制代码
Nacos 发布配置
→ 用户服务收到配置变更
→ @RefreshScope Bean 重新创建
→ @Value 读取新值
→ 接口返回 200

由于 user-1 和 user-2 都导入同一个 Data ID,一次发布会作用于两个用户服务实例。通过 Gateway 多次访问时,两个实例都应该逐步返回新值。

35.10 配置优先级应如何理解

本项目中的配置来源包括:

text 复制代码
application.yml
application-compose.yml
Nacos 远端配置
Compose 环境变量
命令行参数

如果同一个属性在多个位置重复出现,最终值由 Spring Boot 配置优先级决定。实践中建议减少重复定义:

  • 本地公共配置放 application.yml。
  • 容器连接地址和 Nacos import 放 application-compose.yml。
  • 需要集中管理或动态调整的业务配置放 Nacos。
  • 每个实例不同的配置,例如 SERVER_PORT,放 Compose 环境变量。
  • 密码等敏感信息不要直接写进公开仓库。

当前项目的合理分工是:

text 复制代码
application.yml:服务名、本地开发地址
application-compose.yml:Nacos 地址、Sentinel 地址、配置导入声明
Nacos:数据源地址、record.num 等集中配置
Compose 环境变量:两个实例各自的 SERVER_PORT

35.11 常见错误

启动时报缺少 spring.config.import

检查是否配置:

yaml 复制代码
spring:
  config:
    import:
      - nacos:spring-cloud-user-dev.yml

如果某个环境明确不使用配置中心,可以关闭 import 检查或使用 optional:nacos:。

找不到 Data ID

检查以下内容是否完全一致:

text 复制代码
Nacos Data ID:spring-cloud-user-dev.yml
导入名称:nacos:spring-cloud-user-dev.yml
Group:DEFAULT_GROUP

Data ID 的大小写、后缀和字符必须一致。

容器无法连接配置中心

容器内必须使用:

yaml 复制代码
server-addr: nacos:8848

不能使用 localhost:8848,因为容器中的 localhost 是当前用户服务容器自己。

Could not resolve placeholder record.num

表示 Spring Environment 中不存在 record.num。检查:

  • 远端配置是否已经发布。
  • Data ID 和 Group 是否正确。
  • YAML 缩进是否正确。
  • 用户服务是否成功导入远端配置。

如果希望属性缺失时使用默认值,可以写:

java 复制代码
@Value("${record.num:100}")
private int recordNum;
修改 Nacos 后接口没有变化

检查:

  • 使用该属性的 Bean 是否标注 @RefreshScope。
  • 修改后是否点击了"发布",而不只是保存在编辑框。
  • 用户服务日志中是否收到配置变更。
  • 请求是否落到另一个尚未更新的实例。
  • 修改的是否是正确命名空间和 Group 下的 Data ID。
远端数据库地址导致本地运行失败

远端配置当前使用:

text 复制代码
mysql:3306

这个地址只适用于 Compose 网络。IDEA 本地运行应使用:

text 复制代码
localhost:3307

因此,应通过 profile、不同 Data ID 或环境变量区分本地与容器环境,不要让本地运行错误地加载容器专用数据库地址。

35.12 加入配置中心后的完整启动流程

text 复制代码
user-service 容器启动
      ↓
读取 application.yml
      ↓
激活 compose profile
      ↓
读取 application-compose.yml
      ↓
连接 Nacos Config
      ↓
导入 spring-cloud-user-dev.yml
      ↓
创建数据源和 @RefreshScope Bean
      ↓
把 user-service 实例注册到 Nacos Discovery
      ↓
连接 Sentinel Dashboard
      ↓
开始处理 Gateway 和 Feign 请求

运行期间的动态更新流程:

text 复制代码
在 Nacos 修改 record.num 并发布
      ↓
Nacos 通知 user-1 和 user-2
      ↓
两个实例更新 Spring Environment
      ↓
@RefreshScope 重新创建 Controller Bean
      ↓
新请求读取新的 record.num

至此,Nacos 在项目中同时承担了两种职责:用 Discovery 管理服务实例,用 Config 集中管理应用配置。

相关推荐
草莓熊Lotso7 小时前
【C++ 基于微服务的即时通讯系统】基建三件套:gflags + gtest + spdlog 源码编译安装与实战入门
linux·网络·数据库·c++·微服务·log4j
叶总没有会7 小时前
Gateway--网关
spring cloud·微服务·gateway
骇客野人17 小时前
微服务多级缓存架构设计
缓存·微服务·架构
我不是阵雨18 小时前
微服务幂等性深度解剖
微服务·云原生·架构
code_slave(码畜)21 小时前
微服务架构落地:消息队列架构设计(上篇)——异步解耦、削峰填谷,看懂业务事件流转本质
spring boot·spring cloud·微服务·云原生·架构
code_slave(码畜)1 天前
微服务架构落地:消息队列架构设计(下篇)——消息堆积、死信治理与集群监控告警实战
spring boot·spring cloud·微服务·云原生·中间件·架构
Joy T2 天前
从 Nginx 到 Gateway 再到微服务:一次 AI 请求的完整链路
nginx·微服务·gateway·controller·路由转发·ai service·接口错误排查
数据狐(Datafox)3 天前
淘宝图片搜索 API 落地实战:基于以图搜货搭建跨境电商选品系统
java·大数据·微服务
自强的小白5 天前
nacos配置中心面试题(导入配置的优先级),注意是如果后导入相同配置是以 后导入优先。外部入优先和nacos的数据隔离。
java·微服务