微服务架构 · 01 实战使用手册

定位:企业级落地指南。看完能直接在项目里搭起微服务骨架,接入主流组件。 技术栈:Spring Boot 3.x + Spring Cloud 2022.x + Spring Cloud Alibaba 2022.x


第 1 章:工程搭建

1.1 版本选型

微服务项目最常见的踩坑不是代码问题,是版本对不上。三个框架的版本必须成套使用:

Spring BootSpring CloudSpring Cloud Alibaba说明
3.0.x2022.0.x2022.0.0.0推荐,支持 JDK 17+
2.7.x2021.0.x2021.0.5.0稳定,JDK 8/11 兼容
2.6.x2021.0.x2021.0.4.0老项目维护用

⚠️ 陷阱:不要自己拼版本,去 Spring Cloud Alibaba 版本说明 看对照表。spring-cloud-alibaba-dependenciesspring-cloud-dependencies 版本搞错,启动时会出现诡异的 Bean 找不到问题。

1.2 父子模块结构

标准的企业级微服务工程结构:

mall-parent/                    # 父工程,只管版本,不写业务代码
├── pom.xml                     # 统一管理所有依赖版本
├── mall-common/                # 公共模块(工具类、公共实体、异常)
│   └── pom.xml
├── mall-gateway/               # 网关服务(8080端口)
│   └── pom.xml
├── mall-user-service/          # 用户服务(8081端口)
│   └── pom.xml
├── mall-order-service/         # 订单服务(8082端口)
│   └── pom.xml
└── mall-product-service/       # 商品服务(8083端口)
    └── pom.xml

父工程 pom.xml 核心配置:

<project>
    <groupId>com.mall</groupId>
    <artifactId>mall-parent</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>  <!-- 必须是 pom,不是 jar -->

    <modules>
        <module>mall-common</module>
        <module>mall-gateway</module>
        <module>mall-user-service</module>
        <module>mall-order-service</module>
        <module>mall-product-service</module>
    </modules>

    <properties>
        <java.version>17</java.version>
        <spring-boot.version>3.0.9</spring-boot.version>
        <spring-cloud.version>2022.0.3</spring-cloud.version>
        <spring-cloud-alibaba.version>2022.0.0.0</spring-cloud-alibaba.version>
    </properties>

    <!-- 版本锁定,子模块引入时不需要写版本号 -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <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>
        </dependencies>
    </dependencyManagement>
</project>

第 2 章:服务注册与发现(Nacos)

2.1 Nacos 快速启动

Docker 一键启动(本地开发用):

# 单机模式启动,数据存内存(重启丢数据,仅开发用)
docker run -d \
  --name nacos \
  -e MODE=standalone \
  -p 8848:8848 \
  nacos/nacos-server:v2.2.3

# 访问控制台:http://localhost:8848/nacos
# 默认账号密码:nacos / nacos

2.2 服务接入 Nacos 三步走

Step 1:引入依赖(子服务 pom.xml)

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    <!-- 父工程已锁版本,这里不写 version -->
</dependency>

Step 2:配置 Nacos 地址(application.yml)

spring:
  application:
    name: mall-user-service   # 服务名,Nacos 里显示的名字,命名要规范
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848   # Nacos 地址
        namespace: dev                 # 命名空间,隔离不同环境
        group: DEFAULT_GROUP           # 分组
        # username: nacos             # 开启鉴权时配置
        # password: nacos

Step 3:启动类加注解

@SpringBootApplication
@EnableDiscoveryClient  // 开启服务发现
public class UserServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);
    }
}

启动后在 Nacos 控制台「服务管理 → 服务列表」里能看到 mall-user-service,说明注册成功。

2.3 多环境隔离(命名空间)

生产环境:namespace = prod
测试环境:namespace = test  
开发环境:namespace = dev

不同命名空间的服务相互看不见,天然隔离,不会出现开发服务被测试调用的情况。

⚠️ 陷阱:命名空间 ID 不是命名空间名称,在 Nacos 控制台「命名空间」页面复制的是 UUID 格式的 ID(如 a1b2c3d4-...),把名称填进去会找不到服务。

2.4 健康检查与服务剔除

Nacos 对临时实例(默认)的健康检查机制:

每 5 秒发一次心跳 → 15 秒内没收到心跳 → 标记为不健康 → 30 秒内没收到 → 从列表剔除

调整心跳参数(不建议随意改,理解即可):

spring:
  cloud:
    nacos:
      discovery:
        heart-beat-interval: 5000    # 心跳间隔(ms)
        heart-beat-timeout: 15000    # 超过此时间无心跳标记不健康
        ip-delete-timeout: 30000     # 超过此时间无心跳从列表删除

第 3 章:服务间调用(OpenFeign)

3.1 OpenFeign 接入

引入依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- 负载均衡(替代已废弃的 Ribbon)-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

启动类开启 Feign:

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients  // 扫描 @FeignClient 接口
public class OrderServiceApplication { ... }

声明 Feign 接口:

// 放在 order-service 里,调用 user-service 的接口
@FeignClient(
    name = "mall-user-service",      // Nacos 中注册的服务名
    path = "/user",                   // 接口统一前缀
    fallback = UserFeignFallback.class // 降级实现类(需开启 Sentinel)
)
public interface UserFeignClient {

    // 方法签名和被调用方的 Controller 保持一致
    @GetMapping("/{userId}")
    UserDTO getUserById(@PathVariable("userId") Long userId);

    @PostMapping("/batch")
    List<UserDTO> getUserBatch(@RequestBody List<Long> userIds);
}

使用 Feign 接口(和普通 Service 一样注入):

@Service
@RequiredArgsConstructor
public class OrderService {

    private final UserFeignClient userFeignClient;

    public OrderDetailVO getOrderDetail(Long orderId) {
        Order order = orderMapper.selectById(orderId);
        // Feign 调用远程服务,代码上看不出是 RPC
        UserDTO user = userFeignClient.getUserById(order.getUserId());
        return OrderDetailVO.of(order, user);
    }
}

3.2 超时配置

Feign 的超时不配容易踩坑——默认超时极短,生产环境慢 SQL 或网络抖动时疯狂报错:

spring:
  cloud:
    openfeign:
      client:
        config:
          default:            # 全局默认配置
            connect-timeout: 3000   # 建立连接超时(ms),一般 1-3s
            read-timeout: 10000     # 等待响应超时(ms),根据下游接口响应时间定
          mall-user-service:  # 针对特定服务单独配置(覆盖默认值)
            read-timeout: 5000

⚠️ 陷阱read-timeout 是等待服务端返回数据的时间,不是整个请求时间。如果你的接口本来就要算 3 秒,设 2 秒就会一直超时失败。

3.3 请求拦截器(传递 Token/TraceId)

微服务调用链里,Token 和链路追踪 ID 需要在请求头里透传,用拦截器统一处理:

@Component
public class FeignRequestInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {
        // 从当前请求上下文取出 Token,透传给下游服务
        ServletRequestAttributes attributes =
            (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes != null) {
            HttpServletRequest request = attributes.getRequest();
            String token = request.getHeader("Authorization");
            if (StringUtils.hasText(token)) {
                template.header("Authorization", token);
            }
            // 透传链路追踪 ID
            String traceId = request.getHeader("X-Trace-Id");
            if (StringUtils.hasText(traceId)) {
                template.header("X-Trace-Id", traceId);
            }
        }
    }
}

3.4 Feign 常见报错排查

报错信息原因解法
feign.FeignException$NotFound: 404被调用服务的接口路径不对检查 @FeignClient(path) + Controller 路径是否拼对
No instances available for mall-xxx服务未注册到 Nacos,或服务名写错检查 Nacos 控制台服务列表,核对服务名大小写
Read timed out响应超时增大 read-timeout,或排查下游服务性能问题
Connection refused下游服务未启动先启动被依赖的服务
com.fasterxml.jackson.databind.exc.MismatchedInputException返回值类型不匹配检查 DTO 的字段类型,特别是 Long/String 混用

第 4 章:API 网关(Spring Cloud Gateway)

4.1 网关快速接入

⚠️ 网关不能引入 spring-boot-starter-web,Gateway 基于 WebFlux(响应式),与 Servlet 体系不兼容,引了 web 包启动会报错。

依赖(gateway 模块 pom.xml):

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

路由配置(application.yml):

spring:
  application:
    name: mall-gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    gateway:
      discovery:
        locator:
          enabled: true         # 开启自动路由(从 Nacos 服务名自动生成路由)
          lower-case-service-id: true  # 服务名转小写
      routes:
        # 手动配置精细化路由(推荐,比自动路由可控)
        - id: user-service-route
          uri: lb://mall-user-service   # lb:// 表示负载均衡,后面是 Nacos 服务名
          predicates:
            - Path=/api/user/**          # 匹配这个路径的请求转发到 user-service
          filters:
            - StripPrefix=1              # 去掉路径前缀(/api/user/1 → /user/1)
        - id: order-service-route
          uri: lb://mall-order-service
          predicates:
            - Path=/api/order/**
          filters:
            - StripPrefix=1

4.2 统一鉴权过滤器

@Component
@Order(-1)  // 优先级最高,在其他过滤器前执行
public class AuthGlobalFilter implements GlobalFilter {

    // 不需要鉴权的路径白名单
    private static final List<String> WHITE_LIST = List.of(
        "/api/user/login",
        "/api/user/register"
    );

    @Autowired
    private JwtUtil jwtUtil;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String path = exchange.getRequest().getPath().toString();

        // 白名单路径直接放行
        if (WHITE_LIST.stream().anyMatch(path::startsWith)) {
            return chain.filter(exchange);
        }

        // 校验 Token
        String token = exchange.getRequest().getHeaders().getFirst("Authorization");
        if (!StringUtils.hasText(token) || !jwtUtil.validateToken(token)) {
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }

        // Token 合法,解析用户信息,放入请求头传给下游
        Long userId = jwtUtil.getUserId(token);
        ServerHttpRequest mutatedRequest = exchange.getRequest().mutate()
            .header("X-User-Id", String.valueOf(userId))
            .build();
        return chain.filter(exchange.mutate().request(mutatedRequest).build());
    }
}

4.3 全局跨域配置

@Configuration
public class CorsConfig {

    @Bean
    public CorsWebFilter corsWebFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedOriginPattern("*");    // 生产环境换成具体域名
        config.addAllowedMethod("*");
        config.addAllowedHeader("*");
        config.setAllowCredentials(true);
        config.setMaxAge(3600L);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return new CorsWebFilter(source);
    }
}

⚠️ 陷阱:如果下游微服务也配了 CORS,会产生重复的 Access-Control-Allow-Origin 响应头,浏览器会报错。要么只在网关配,要么下游服务全部关掉 CORS 配置。

4.4 接口限流(Redis + 令牌桶)

<!-- 网关限流需要 Redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
@Bean
public KeyResolver userKeyResolver() {
    // 按请求 IP 限流(也可以按用户 ID、接口路径等)
    return exchange -> Mono.just(
        Objects.requireNonNull(exchange.getRequest().getRemoteAddress())
            .getAddress().getHostAddress()
    );
}
spring:
  cloud:
    gateway:
      routes:
        - id: order-service-route
          uri: lb://mall-order-service
          predicates:
            - Path=/api/order/**
          filters:
            - StripPrefix=1
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10    # 每秒补充 10 个令牌
                redis-rate-limiter.burstCapacity: 20    # 令牌桶最大容量
                key-resolver: "#{@userKeyResolver}"     # 引用上面的 Bean

第 5 章:熔断限流(Sentinel)

5.1 Sentinel 接入

依赖:

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- Feign 整合 Sentinel -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

配置:

spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8080   # Sentinel Dashboard 地址
        port: 8719                  # 客户端与 Dashboard 通信端口
      eager: true                   # 服务启动时立即连接 Dashboard,不等第一次请求

feign:
  sentinel:
    enabled: true   # 开启 Feign + Sentinel 整合

Sentinel Dashboard 启动:

# 下载 sentinel-dashboard-x.x.x.jar 后
java -Dserver.port=8080 -jar sentinel-dashboard-1.8.6.jar
# 访问 http://localhost:8080,账号密码均为 sentinel

5.2 流量控制规则

在 Dashboard 里配置(生产环境要持久化到 Nacos,见 5.3):

规则字段含义常用值
资源名被保护的接口/方法名/user/getUserById 或 Feign 接口名
阈值类型QPS 或并发线程数接口限并发用线程数;限速用 QPS
单机阈值达到此值触发流控视接口承载能力,从压测数据来
流控模式直接/关联/链路通常用直接
流控效果快速失败/预热/排队秒杀场景用预热,异步场景用排队等待

5.3 规则持久化到 Nacos

Sentinel 默认规则在内存里,服务重启就丢了。生产环境必须持久化:

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
spring:
  cloud:
    sentinel:
      datasource:
        flow-rules:           # 流控规则
          nacos:
            server-addr: localhost:8848
            namespace: prod
            dataId: ${spring.application.name}-flow-rules
            groupId: SENTINEL_GROUP
            rule-type: flow   # 规则类型
        degrade-rules:        # 熔断降级规则
          nacos:
            server-addr: localhost:8848
            namespace: prod
            dataId: ${spring.application.name}-degrade-rules
            groupId: SENTINEL_GROUP
            rule-type: degrade

在 Nacos 里创建对应 DataId 的配置,内容是 JSON 格式的规则数组:

[
  {
    "resource": "/user/getUserById",
    "grade": 1,
    "count": 100,
    "strategy": 0,
    "controlBehavior": 0,
    "clusterMode": false
  }
]

5.4 @SentinelResource 注解

@RestController
public class UserController {

    @GetMapping("/user/{id}")
    @SentinelResource(
        value = "getUserById",           // 资源名,在 Dashboard 里配规则用这个名字
        blockHandler = "getUserBlocked", // 触发流控/熔断时调用(BlockException)
        fallback = "getUserFallback"     // 业务异常时的降级(RuntimeException)
    )
    public UserDTO getUserById(@PathVariable Long id) {
        return userService.getById(id);
    }

    // blockHandler:方法签名必须加 BlockException 参数
    public UserDTO getUserBlocked(Long id, BlockException e) {
        return UserDTO.builder().id(id).name("限流中,请稍后重试").build();
    }

    // fallback:方法签名可加 Throwable 参数
    public UserDTO getUserFallback(Long id, Throwable t) {
        log.error("getUserById fallback, id={}, error={}", id, t.getMessage());
        return UserDTO.builder().id(id).name("服务降级").build();
    }
}

⚠️ 陷阱blockHandlerfallback 是两个不同的兜底,很多人搞混:

  • blockHandler:Sentinel 触发流控/熔断时调用,参数里有 BlockException
  • fallback:接口本身抛了业务异常(如 NPE、数据库异常)时调用
  • 两个都不配就直接抛异常到调用方,接口会返回 500

第 6 章:配置中心(Nacos Config)

6.1 接入配置中心

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- Spring Boot 3.x 需要这个依赖才能加载 bootstrap.yml -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>

bootstrap.yml(优先于 application.yml 加载):

spring:
  application:
    name: mall-user-service
  cloud:
    nacos:
      config:
        server-addr: localhost:8848
        namespace: dev
        file-extension: yaml        # Nacos 上配置文件的格式
        # DataId 默认是:${spring.application.name}.${file-extension}
        # 即:mall-user-service.yaml

在 Nacos 控制台「配置管理 → 配置列表」新建配置:

  • Data ID:mall-user-service.yaml
  • Group:DEFAULT_GROUP
  • 配置格式:YAML
  • 配置内容:数据库连接、业务参数等

6.2 动态刷新

@RestController
@RefreshScope   // 加这个注解,Nacos 配置变更时自动刷新 Bean 中的配置值
public class ConfigController {

    @Value("${mall.order.max-items:10}")  // 冒号后面是默认值
    private Integer maxOrderItems;

    @GetMapping("/config")
    public String getConfig() {
        return "最大下单数量:" + maxOrderItems;
    }
}

⚠️ 陷阱@RefreshScope 只对 @Value 注入的字段有效。如果用 @ConfigurationProperties 绑定的配置类,需要在配置类上加 @RefreshScope(会导致整个 Bean 重建,有性能开销),或者改用 @NacosConfigListener 监听变更。

6.3 多配置文件共享

spring:
  cloud:
    nacos:
      config:
        server-addr: localhost:8848
        file-extension: yaml
        # 共享配置(多服务公用)
        shared-configs:
          - dataId: common-db.yaml      # 公共数据库配置
            group: COMMON_GROUP
            refresh: true
          - dataId: common-redis.yaml   # 公共 Redis 配置
            group: COMMON_GROUP
            refresh: true
        # 扩展配置(本服务自己的额外配置)
        extension-configs:
          - dataId: mall-user-service-ext.yaml
            group: DEFAULT_GROUP
            refresh: true

配置加载优先级(从高到低):

本服务配置(mall-user-service.yaml)
  > 扩展配置(extension-configs,后配置的优先)
  > 共享配置(shared-configs,后配置的优先)
  > 本地 application.yml

第 7 章:链路追踪(Micrometer Tracing + Zipkin)

Spring Boot 3.x 已废弃 Sleuth,改用 Micrometer Tracing。

7.1 接入链路追踪

<!-- Micrometer Tracing 核心 -->
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<!-- 上报到 Zipkin -->
<dependency>
    <groupId>io.zipkin.reporter2</groupId>
    <artifactId>zipkin-reporter-brave</artifactId>
</dependency>
<!-- Feign 调用自动传递 TraceId -->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-micrometer</artifactId>
</dependency>

Zipkin 本地启动:

docker run -d --name zipkin -p 9411:9411 openzipkin/zipkin
# 访问 http://localhost:9411

application.yml 配置:

management:
  tracing:
    sampling:
      probability: 1.0    # 采样率,1.0=100%(开发用),生产环境建议 0.1(10%)
  zipkin:
    tracing:
      endpoint: http://localhost:9411/api/v2/spans

7.2 让 TraceId 出现在日志里

<!-- logback-spring.xml 中加入 TraceId -->
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} [traceId=%X{traceId}] - %msg%n</pattern>

引入上述依赖后,每个请求的日志都会自动带上 traceId。出问题时在 ELK 里搜 traceId=xxx 就能把整条调用链的日志都找出来。

7.3 快速排错指南

现象可能原因排查方法
日志里没有 traceId依赖未引入或配置未生效检查 micrometer-tracing 是否在 classpath
Zipkin 里看不到追踪数据采样率为 0 或 endpoint 地址错误把采样率设为 1.0,检查 Zipkin 地址
跨服务 traceId 断链Feign 没引入 feign-micrometer加上该依赖
TraceId 在异步线程里丢失异步线程没有 Span 上下文使用 Tracer 手动传递 Span

附录:本地开发环境一键启动脚本

#!/bin/bash
# 按依赖顺序启动所有基础设施

echo "Starting Nacos..."
docker start nacos || docker run -d --name nacos -e MODE=standalone -p 8848:8848 nacos/nacos-server:v2.2.3

echo "Starting Zipkin..."
docker start zipkin || docker run -d --name zipkin -p 9411:9411 openzipkin/zipkin

echo "Starting Sentinel Dashboard..."
# 假设 jar 在当前目录
nohup java -Dserver.port=8080 -jar sentinel-dashboard-1.8.6.jar > sentinel.log 2>&1 &

echo "All infrastructure started."
echo "Nacos:    http://localhost:8848/nacos"
echo "Zipkin:   http://localhost:9411"
echo "Sentinel: http://localhost:8080"

📎 延伸阅读:各组件的工作原理(为什么要这样配),详见《微服务-02-技术深度精讲》对应章节。