之前接手一个从单体向微服务演进的中间项目,最头疼的不是某个组件怎么用,而是版本不匹配、组件选型摇摆、以及“网上教程各自为战”的问题。Spring Cloud 和 Spring Cloud Alibaba 的教程铺天盖地,但往往只讲一个亮点组件,缺少一套能贯穿注册中心、远程调用、网关、限流熔断、分布式事务的闭环实战。这篇文章打算把这条链路上最关键的组件和落地思路完整梳理一遍,面向两类读者:一是刚把 Java 基础学完、想系统入微服务的同学,二是已经在做 Spring Boot 单体项目、准备向微服务架构迁移的开发者。
文章会先讲清楚微服务架构和两个技术栈的关系,再给出一套可运行的最小微服务电商案例,围绕 Nacos 注册与配置、OpenFeign 远程调用、Spring Cloud Gateway 网关、Sentinel 流量防护和 Seata 分布式事务展开。每个环节都会解释配置项的作用和常见坑点,尽量让读者能照着复制、改参数、跑通,而不是看了一堆概念最后还是搭不起来。
1. 微服务架构与 AlibabaCloud、SpringCloud 的关系
1.1 微服务架构到底解决什么问题
微服务架构不是某一种具体框架,而是一种软件架构风格。它把传统单体应用按照业务域拆分成多个独立部署的小服务,每个服务运行在自己的进程中,通过轻量级通信机制(通常是 HTTP REST 或消息队列)协作。
单体应用在业务规模不大时开发效率很高,但随着团队扩张和业务复杂化,会出现几个明显问题:代码仓库臃肿导致构建部署变慢;模块之间耦合深,任何一个功能出问题都可能导致整个应用不可用;不同模块对技术栈和资源的需求不同,却被迫绑定在同一套环境中。微服务架构的价值在于:每个服务可以独立开发、独立部署、独立扩缩容,团队之间通过接口契约协作。
但微服务不是免费的午餐。服务拆分后,原来的“函数调用”变成了“网络调用”,随之而来的是服务发现、配置管理、负载均衡、熔断限流、分布式链路追踪、分布式事务等一系列新问题。Spring Cloud 和 Spring Cloud Alibaba 的核心价值,就是用一套相对统一的组件把这些分布式难题封装起来,让开发者不必从零实现。
1.2 Spring Cloud 与 Spring Cloud Alibaba 的定位差异
Spring Cloud 是建立在 Spring Boot 之上的微服务解决方案集合,它定义了一组微服务开发规范,包括服务注册发现、API 网关、配置中心、熔断器、负载均衡、链路追踪等。
Spring Cloud Alibaba 则是阿里巴巴基于 Spring Cloud 规范实现的组件集合。它提供了 Nacos(注册中心 + 配置中心)、Sentinel(流量防护)、Seata(分布式事务)、RocketMQ(消息队列)等组件。
两者不是替代关系,而是“规范 + 实现”的关系。Spring Cloud 体系内有很多官方和社区实现,比如服务注册中心有 Eureka、Consul、Zookeeper、Nacos,熔断组件有 Hystrix、Resilience4j、Sentinel。在实际项目中,使用 Spring Cloud Alibaba 多数情况下是指“以 Spring Cloud 规范为基础,用 Nacos、Sentinel、Seata 等阿里组件完成关键能力”。
下表是常见的选型组合思路:
| 能力 | Spring Cloud 原生体系 | Spring Cloud Alibaba 体系 | 生产偏好 |
|---|---|---|---|
| 服务注册发现 | Eureka / Consul | Nacos | 国内项目多数选 Nacos,支持配置中心二合一 |
| 配置中心 | Spring Cloud Config | Nacos Config | Nacos 更方便,支持动态刷新、版本回溯 |
| 远程调用 | OpenFeign / RestTemplate | OpenFeign + LoadBalancer | OpenFeign 是主流声明式调用 |
| 网关 | Spring Cloud Gateway | Spring Cloud Gateway | 两者共用,阿里生态没有独立网关 |
| 流量控制与熔断 | Hystrix / Resilience4j | Sentinel | Sentinel 控制台更强大,规则持久化方便 |
| 分布式事务 | 无统一方案 | Seata | Seata 在跨服务事务上应用广泛 |
1.3 微服务拆分的基本思路
微服务拆分有两种常见驱动方式:一种是从业务边界出发,把用户、订单、商品、支付、库存划分为独立服务;另一种是从团队结构出发,按照康威定律让每个团队负责一个可独立演进的域。
拆分时核心原则是:
- 服务内部高内聚,服务之间低耦合。
- 每个服务拥有独立的数据库 schema,避免多个服务直接操作同一张表。
- 服务间只能通过 API 或消息通信,不能共享 Service 层代码。
- 拆分的粒度控制在一个团队能独立维护的范围内,过度拆分会导致运维成本爆炸。
网上经常讨论“微服务架构图”怎么画,其实画图之前更应该先想清楚服务边界。一个经典的参考拆分方式如下:
code复制用户服务(user-service)
商品服务(product-service)
订单服务(order-service)
库存服务(stock-service)
网关服务(gateway-service)
其实就是电商最基础的六边形链路。本文的实战案例就基于这种拆分思路,但只实现其中三个核心服务 + 网关,避免案例过于复杂影响理解。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与版本说明
2.1 环境清单
搭建 Spring Cloud Alibaba 项目,前提是先准备一套 Java 开发环境:
| 工具 | 版本建议 | 说明 |
|---|---|---|
| JDK | JDK 8 或 JDK 17 | Spring Boot 2.x 用 JDK 8 为主,Spring Boot 3.x 需要 JDK 17 |
| Maven | 3.6 及以上 | 项目管理工具 |
| IDE | IntelliJ IDEA | 社区版或旗舰版均可,安装 Lombok 插件 |
| Nacos | 2.x 稳定版 | 注册中心和配置中心 |
| Sentinel 控制台 | 1.8.x | 流量控制可视化界面,可选用 |
| MySQL | 5.7 或 8.x | 订单服务的业务库,也可用 H2 替代 |
2.2 版本对应关系是最大的坑
Spring Cloud Alibaba 的版本命名方式与 Spring Cloud 不同,它使用 2021.x、2022.x 这种版本号来对应 Spring Cloud 的版本周期。这个对应关系在官方 GitHub README 中有详细表格,但很多新手直接复制旧教程的依赖版本,导致启动时 Bean 找不到、组件无法注册等问题。
本文示例采用的是一种经过大量项目验证的组合方式:
xml复制<properties>
<java.version>8</java.version>
<spring-boot.version>2.7.18</spring-boot.version>
<spring-cloud.version>2021.0.8</spring-cloud.version>
<spring-cloud-alibaba.version>2021.0.5.0</spring-cloud-alibaba.version>
</properties>
重要提示:以上版本组合在多数 Spring Boot 2.7 项目中可以正常运行,但具体版本号应以官方文档和 Maven 仓库最新信息为准。如果你的项目已经基于 Spring Boot 3.x,需要调整到对应的 Spring Cloud 2023.x 和 Spring Cloud Alibaba 2023.x 系列,切勿混用。版本对应关系建议参考官方文档中的版本说明页面。
2.3 搭建父工程的基础骨架
实际项目中通常会创建一个父 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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.damai</groupId>
<artifactId>microservice-demo</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<properties>
<java.version>8</java.version>
<spring-boot.version>2.7.18</spring-boot.version>
<spring-cloud.version>2021.0.8</spring-cloud.version>
<spring-cloud-alibaba.version>2021.0.5.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>
这里使用 dependencyManagement 而不是直接引入依赖,核心目的是统一版本管理。子模块只需要声明依赖的 groupId 和 artifactId,不需要写 version,Maven 会从父工程的依赖管理中找到匹配版本。
3. Nacos 服务注册与配置中心实践
3.1 启动 Nacos Server
Nacos 可以从 GitHub Releases 页面下载压缩包,解压后进入 bin 目录执行启动命令。
Linux/macOS:
bash复制sh startup.sh -m standalone
Windows:
bash复制startup.cmd -m standalone
单机模式启动后访问 Nacos 控制台:http://localhost:8848/nacos,默认账号密码都是 nacos。
在真实生产环境中,Nacos Server 需要以集群模式部署,并配置 MySQL 存储。本文先以单机模式演示核心流程。
3.2 商品服务注册到 Nacos
以商品服务 product-service 为例,在子模块中引入 Nacos 注册和配置中心依赖。
文件路径:product-service/pom.xml
xml复制<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
</dependencies>
spring-cloud-starter-bootstrap 的作用是启用 bootstrap.yml 配置文件的加载,Nacos 配置中心需要在应用启动早期接入,因此不能只使用 application.yml。
文件路径:product-service/src/main/resources/bootstrap.yml
yaml复制spring:
application:
name: product-service
cloud:
nacos:
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
discovery:
group: DEFAULT_GROUP
config:
file-extension: yaml
group: DEFAULT_GROUP
这里的 server-addr 是 Nacos Server 地址,file-extension 表示配置文件后缀为 yaml。Nacos 配置中心默认加载规则为:
code复制${spring.application.name}.${file-extension}
也就是会加载 dataId 为 product-service.yaml 的配置。应用启动时,会先连上 Nacos 配置中心拉取配置,再执行后续初始化逻辑。
商品服务启动类加上 @EnableDiscoveryClient 注解:
文件路径:product-service/src/main/java/com/damai/product/ProductApplication.java
java复制package com.damai.product;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class ProductApplication {
public static void main(String[] args) {
SpringApplication.run(ProductApplication.class, args);
}
}
启动后,Nacos 控制台的服务列表会出现 product-service。这个注册动作做了什么?服务启动后,Nacos 客户端会向 Nacos Server 发送一个包含服务名、IP、端口号的注册请求,同时定时发送心跳。如果 product-service 需要被其他服务调用,调用方只需要知道服务名,而不需要硬编码 IP 地址,这就是服务发现的核心价值。
3.3 配置中心的动态刷新
在 Nacos 配置控制台新增一条配置:
dataId:product-service.yaml
yaml复制product:
name: 小米手机
price: 1999
desc: Nacos配置中心读取的商品信息
在代码中读取配置:
java复制package com.damai.product.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/product")
@RefreshScope
public class ProductController {
@Value("${product.name:默认商品}")
private String productName;
@Value("${product.price:0}")
private Integer productPrice;
@Value("${product.desc:暂无}")
private String productDesc;
@GetMapping("/info")
public String info() {
return "商品名称:" + productName + ",价格:" + productPrice + ",描述:" + productDesc;
}
}
@RefreshScope 是 Spring Cloud 提供的动态刷新机制。当 Nacos 上的配置发生变化后,Spring Cloud 会发布一个 RefreshEvent,被 @RefreshScope 标记的 Bean 会重新创建,从而读取到最新配置值。如果没有这个注解,配置值只有在应用重启后才会生效。
在 Nacos 控制台修改 product.name 的值为“小米手机 Pro”,然后调用 GET http://localhost:8081/product/info,可以看到返回结果已经变化,无需重启服务。这在生产环境中可以减少因修改配置而触发重启的运维成本。
4. OpenFeign 远程调用实现服务间通信
4.1 OpenFeign 是什么
服务间调用在一个大单体里就是简单的方法调用,但在微服务架构中变成了网络调用。OpenFeign 是一个声明式 HTTP 客户端,开发者只需要定义一个接口,然后加上注解,Feign 就会在运行时动态生成代理类,完成 HTTP 请求的发送和响应结果的封装。
对比 RestTemplate,Feign 的代码可读性更好,而且天然适配服务发现机制。在 Nacos 环境下,Feign 会优先通过服务名获取可用服务实例列表,再配合负载均衡策略选择一个实例发起调用。
4.2 订单服务调用商品服务
订单服务在下单时需要获取商品信息,而商品数据在 product-service 中,订单服务不可能直接操作商品服务的数据库。这时就需要通过 OpenFeign 发起一次带服务名的远程调用。
订单服务依赖:
xml复制<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
定义 Feign 客户端接口:
文件路径:order-service/src/main/java/com/damai/order/feign/ProductFeignClient.java
java复制package com.damai.order.feign;
import com.damai.order.dto.ProductDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient(name = "product-service")
public interface ProductFeignClient {
@GetMapping("/product/{id}")
ProductDTO getProductById(@PathVariable("id") Long id);
}
@FeignClient(name = "product-service") 中的 name 不是随便写的,它对应目标服务在 Nacos 中注册的服务名。Feign 会拿着这个名字去 Nacos 查询可用实例。
商品服务需要提供对应的 HTTP 接口:
文件路径:product-service/src/main/java/com/damai/product/controller/ProductController.java
java复制package com.damai.product.controller;
import com.damai.product.dto.ProductDTO;
import org.springframework.beans.factory.annotation.Value;
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;
@RestController
@RequestMapping("/product")
public class ProductController {
@Value("${product.name:默认商品}")
private String productName;
@Value("${product.price:0}")
private Integer productPrice;
@GetMapping("/{id}")
public ProductDTO getProductById(@PathVariable("id") Long id) {
ProductDTO dto = new ProductDTO();
dto.setId(id);
dto.setName(productName);
dto.setPrice(productPrice);
return dto;
}
}
商品服务返回的 ProductDTO 和订单服务 Feign 接口中的 ProductDTO 并不是同一个类,它们只是 JSON 结构相似。这点很关键,微服务之间通过 JSON 序列化传输数据,服务双方各自维护自己的 DTO,避免不同服务共享同一个实体类导致耦合。
订单服务中注入 Feign 客户端:
java复制package com.damai.order.service;
import com.damai.order.feign.ProductFeignClient;
import com.damai.order.dto.ProductDTO;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private final ProductFeignClient productFeignClient;
public OrderService(ProductFeignClient productFeignClient) {
this.productFeignClient = productFeignClient;
}
public String createOrder(Long productId) {
// 远程调用商品服务查询商品信息
ProductDTO product = productFeignClient.getProductById(productId);
// 落到业务里就是:校验商品状态、计算价格、生成订单号、扣减库存
return "订单创建成功,商品:" + product.getName() + ",价格:" + product.getPrice();
}
}
订单服务的启动类需要加 @EnableFeignClients:
java复制package com.damai.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 OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
4.3 调用链与负载均衡
当请求到达 order-service 的接口后,Feign 会执行以下步骤:
- 根据
@FeignClient(name = "product-service")中的服务名,向 Nacos 发起服务发现请求。 - Nacos 返回
product-service当前注册的实例列表。 - Feign 通过内部集成的 Spring Cloud LoadBalancer 从实例列表中选出一个实例。
- 发起真实 HTTP 请求,结果被反序列化为
ProductDTO。
这里要特别提醒:OpenFeign 官方在早期版本中集成的是 Ribbon,后来从 Ribbon 迁移到了 Spring Cloud LoadBalancer。如果代码里配置了 ribbon.NFLoadBalancerRuleClassName 这类 Ribbon 专属配置,在新版本中可能不生效;更通用的做法是配置 Spring Cloud LoadBalancer 的负载均衡策略,或者在服务提供方多部署几个实例后观察默认轮询效果。
5. Spring Cloud Gateway 网关统一入口
5.1 网关的作用与核心概念
网关是整个微服务架构的“前置门户”,所有外部请求先到达网关,再由网关根据路由规则转发到具体的微服务。网关承担了统一认证、灰度发布、流量控制、请求日志、跨域处理等职责,避免每个微服务重复实现这些横切逻辑。
Spring Cloud Gateway 是 Spring 官方推出的第二代网关,底层基于 Spring WebFlux,使用 Netty 作为运行时,具有非阻塞、响应式的特点。它的核心模型有三个概念:
| 概念 | 作用 |
|---|---|
| Route 路由 | 网关的基础单元,包含 ID、目标 URI、断言规则和过滤器集合 |
| Predicate 断言 | 匹配请求的条件,例如路径匹配、请求头匹配、查询参数匹配 |
| Filter 过滤器 | 在请求转发前后执行过滤逻辑,例如修改请求头、记录日志 |
5.2 网关服务基础配置
文件路径:gateway-service/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>
</dependencies>
注意:网关基于 WebFlux,不要引入 spring-boot-starter-web,否则会导致启动冲突。
文件路径:gateway-service/src/main/resources/application.yml
yaml复制server:
port: 8080
spring:
application:
name: gateway-service
cloud:
nacos:
server-addr: 127.0.0.1:8848
gateway:
routes:
- id: product-route
uri: lb://product-service
predicates:
- Path=/api/product/**
filters:
- StripPrefix=1
- id: order-route
uri: lb://order-service
predicates:
- Path=/api/order/**
filters:
- StripPrefix=1
lb://product-service 表示使用负载均衡协议,网关会从 Nacos 中发现 product-service 的实例并轮询转发。StripPrefix=1 的作用是转发时去掉第一段路径前缀,比如外部请求路径为 /api/product/info,网关会将路径调整为 /product/info 再转发给商品服务。
5.3 网关过滤器实现统一日志
打印请求日志是网关最常见的需求。可以通过实现 GlobalFilter 来完成:
文件路径:gateway-service/src/main/java/com/damai/gateway/filter/LogFilter.java
java复制package com.damai.gateway.filter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class LogFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
String method = request.getMethodValue();
String ip = request.getRemoteAddress() == null ? "unknown" : request.getRemoteAddress().getAddress().getHostAddress();
System.out.println("[网关日志] " + method + " " + path + ",来源IP:" + ip);
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
getOrder() 返回值越小优先级越高。网关过滤器可以做成一个责任链,认证过滤器、日志过滤器、限流过滤器各自设置不同的 order,就能控制执行先后顺序。
6. Sentinel 限流熔断与系统保护
6.1 Sentinel 的基本模型
当流量洪峰来袭,任何一个下游服务都可能被打垮。比如订单服务被刷单攻击,或者商品服务的数据库连接池被拖垮,故障会沿着调用链向上传播,最终拖垮整个系统。Sentinel 的核心能力就是流量控制和熔断降级。
Sentinel 有两个核心概念:
- 资源:可以是任意一段代码,但常见的是一个 HTTP 接口或一个方法。
- 规则:告诉 Sentinel 如何保护这个资源,包括流控规则、熔断降级规则、系统保护规则。
6.2 引入 Sentinel 依赖并接入 Nacos
订单服务依赖:
xml复制<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
在订单服务配置中指定 Sentinel 控制台地址和 Nacos 数据源:
yaml复制spring:
cloud:
sentinel:
transport:
dashboard: 127.0.0.1:8088
datasource:
flow:
nacos:
server-addr: 127.0.0.1:8848
dataId: order-service-flow-rules
rule-type: flow
这里使用了 Sentinel 的 Nacos 数据源扩展。规则不直接写在配置文件里,而是保存在 Nacos 配置中心,规则变更时可以动态下发,不需要重启应用。
但这部分需要引入额外的依赖:
xml复制<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
6.3 流控规则配置
在 Nacos 配置中心新建 dataId:order-service-flow-rules,内容如下:
json复制[
{
"resource": "POST:/order/create",
"limitApp": "default",
"grade": 1,
"count": 5,
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
}
]
参数含义:
| 参数 | 含义 |
|---|---|
| resource | 要保护的接口资源名 |
| grade | 限流维度,1 表示 QPS,0 表示线程数 |
| count | 单机阈值,这里表示每秒最多允许 5 个请求 |
| strategy | 流控策略,0 表示直接拒绝 |
| controlBehavior | 流量效果,0 表示快速失败,1 表示 Warm Up |
| clusterMode | 是否集群模式 |
规则配置好之后,当 /order/create 接口的 QPS 超过阈值,Sentinel 会直接返回 Blocked by Sentinel (flow limiting),而不会让请求继续打到业务代码上造成系统压力。
Sentinel 提供了注解 @SentinelResource 可以更精细地控制资源粒度和处理逻辑:
java复制package com.damai.order.controller;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.damai.order.service.OrderService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/order")
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping("/create")
@SentinelResource(value = "createOrder", blockHandler = "createOrderBlockHandler")
public String createOrder(@RequestParam("productId") Long productId) {
return orderService.createOrder(productId);
}
public String createOrderBlockHandler(Long productId, BlockException ex) {
return "当前请求量过大,请稍后重试: " + ex.getClass().getSimpleName();
}
}
blockHandler 必须与原始方法的参数列表保持兼容,并且额外增加一个 BlockException 参数。方法访问级别必须是 public,否则 Sentinel 无法正确注入降级逻辑。
6.4 熔断规则:保护服务调用链
限流是针对入口流量,熔断是针对下游依赖。比如订单服务调用商品服务时,如果商品服务出现了大量超时,订单服务不应该一直等待,而应该快速失败并执行降级逻辑。
熔断规则配置示例:
json复制[
{
"resource": "createOrder",
"grade": 2,
"count": 5,
"timeWindow": 10,
"statIntervalMs": 1000,
"minRequestAmount": 5
}
]
这里的 grade: 2 表示慢调用比例熔断,count 是慢调用阈值(单位毫秒),如果在一个统计周期内有超过一定比例的请求响应时间超过 2000ms,后续请求会直接熔断,timeWindow 为熔断恢复时间。相比手动写 try-catch,Sentinel 的熔断规则能自动统计调用指标并完成状态切换,可以显著提升调用链的稳定性。
7. 分布式事务与 Seata 的引入
7.1 微服务架构下的事务困境
本地事务通过数据库的 ACID 特性保证一致性,比如下单时要扣库存、锁优惠券、生成订单,这几步操作在一个数据库一个事务里可以保证要么全部成功要么全部失败。
但在微服务架构中,订单数据和库存数据分别存储在不同的数据库中,甚至由不同的服务管理。一个下单操作跨 order-service 和 stock-service,本地事务无法解决这种跨服务的数据一致性。
Seata 是阿里巴巴开源的分布式事务解决方案,提供了 AT、TCC、SAGA 和 XA 四种模式。其中 AT 模式对业务代码侵入最小,也是目前使用最广泛的模式。
7.2 AT 模式的执行原理
AT 模式的核心思路是两阶段提交(2PC):
| 阶段 | 动作 |
|---|---|
| 一阶段 | 业务 SQL 正常执行,Seata 在操作数据前记录前后镜像,生成 undo_log |
| 二阶段提交 | 删除 undo_log,事务正式完成 |
| 二阶段回滚 | 根据 undo_log 反向补偿,恢复原始数据 |
由于 Seata 需要解析 SQL 和操作 undo_log 表,使用 AT 模式时必须保证业务数据库中有一张 undo_log 表。Seata Server 则独立部署,负责管理全局事务的提交和回滚状态。
7.3 Seata 引入的最小步骤
首先部署 Seata Server,然后客户端引入依赖:
xml复制<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
业务代码中,在分布式事务发起方的顶层方法上加 @GlobalTransactional:
java复制package com.damai.order.service;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.stereotype.Service;
@Service
public class CreateOrderService {
private final OrderService orderService;
private final StockFeignClient stockFeignClient;
public CreateOrderService(OrderService orderService, StockFeignClient stockFeignClient) {
this.orderService = orderService;
this.stockFeignClient = stockFeignClient;
}
@GlobalTransactional(rollbackFor = Exception.class)
public String execute(Long productId, Integer count) {
// 本地事务:创建订单
orderService.createOrder(productId, count);
// 远程事务:扣减库存
stockFeignClient.deductStock(productId, count);
// 模拟失败场景,观察回滚效果
if (count > 10) {
throw new RuntimeException("单次购买数量不能超过10件");
}
return "下单成功";
}
}
@GlobalTransactional 会开启一个全局事务。当流程中出现异常时,Seata 会通知所有参与分支事务的服务进行回滚。这里要强调一个开发习惯:不要把所有方法都加上 @GlobalTransactional。全局事务有性能和并发开销,只应该在真正跨服务写操作的入口方法上使用,查询操作和单服务事务不要滥用。
关于 Seata 的完整集成,本文只做了概念铺垫,因为 Seata 的部署配置在不同版本之间差异较大,实际落地时建议以对应版本的官方部署文档为准,重点关注 file.conf 和 registry.conf 中的事务组配置。这也是微服务学习中很重要的一课:组件版本跟着官方文档走,不要盲信旧教程。
8. 常见问题与排查思路
微服务组件多、链路长,排错难度比单体大得多。这里整理一套实际问题排查表:
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
应用启动失败,提示 Please check serverAddr property |
Nacos 客户端无法连接注册中心 | 检查 Nacos 是否启动,检查 bootstrap.yml 的 server-addr |
服务注册成功,但调用时报 No instances available |
Feign 服务名拼写错误,或服务未注册成功 | 登录 Nacos 控制台查看服务列表,核对 @FeignClient(name) 的值 |
| 网关启动失败,报端口被占用 | 同时引入了 spring-boot-starter-web 和 Gateway |
删除 Web 依赖,网关基于 WebFlux 不可与 MVC 混用 |
| 配置修改后不生效 | 类上缺少 @RefreshScope,或配置中心 dataId 不对 |
确认 dataId 是否符合 ${spring.application.name}.${file-extension} |
| 请求被 Sentinel 拦截但日志没有记录 | 资源名与接口路径不一致 | 检查 @SentinelResource 的 value 和规则中的 resource 是否一致 |
| Feign 调用超时 | 默认连接超时时间过短,或下游接口执行时间过长 | 在配置中调大 feign.client.config.default.connectTimeout 和 readTimeout |
| 高并发场景下出现数据不一致 | 未引入全局事务,或 Seata 配置错误 | 检查全局事务组配置,确认参与方是否都接入 Seata |
8.1 Feign 超时时间配置
Feign 默认连接超时和读取超时都较短。生产环境中如果业务方法涉及较复杂的计算或等待其他服务响应,需要显式调大超时时间:
yaml复制feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 10000
这里的 default 表示对所有 Feign 客户端生效。如果某个服务需要的超时时间不同,可以指定服务名,例如 product-service:。
8.2 网关层常见的状态码排查
使用 Spring Cloud Gateway 时经常出现 404 或 503:
- 404:路由匹配到了,但转发后的服务没有对应接口路径。
- 503:服务实例不可用,或 lb:// 后面的服务在 Nacos 中找不到。
排查顺序是:先确认 Nacos 服务列表存在目标服务,再确认路由断言是否匹配,最后用 curl 直接请求目标服务的接口地址,确认服务本身能正常响应。
8.3 配置中心加载顺序问题
如果 bootstrap.yml 写错或未引入 spring-cloud-starter-bootstrap,Nacos 配置中心的内容不会被加载。Spring Cloud 2020 版本之后默认不再自动引入 bootstrap 机制,需要手动添加依赖。这是新教程和旧教程之间差异最大的地方,也是很多开发者升级版本后碰到的第一道坎。
9. 最佳实践与工程建议
9.1 依赖版本治理
微服务项目的依赖数量非常多,如果每个子模块自己写版本号,很容易出现版本漂移。最佳实践是在父工程中使用 dependencyManagement 统一锁定版本,子模块只声明坐标。每个季度或每个大版本更新节点,安排一次依赖升级评估,重点检查 Spring Cloud Alibaba 与 Spring Cloud 的版本对应关系。
9.2 Nacos 配置管理的规范建议
配置中心是微服务的生命线,配置错了会导致整个系统不可用。可以遵守几个原则:
- 环境隔离:开发、测试、生产环境使用独立的 Nacos 命名空间(namespace)。
- 分组管理:不同业务域使用不同 group,避免互相覆盖配置。
- 配置变更遵循发布流程:先修改配置,在控制台进行比对,再发布到目标环境,不要直接在数据库里改。
- 生产环境配置变更前备份:Nacos 虽然有历史版本可以回滚,但备份仍然是最可靠的兜底手段。
yaml复制spring:
cloud:
nacos:
server-addr: 127.0.0.1:8848
namespace: dev-order
namespace 的值必须是命名空间 ID,而不是命名空间名称。在 Nacos 控制台“命名空间”页面可以查到对应的 ID。
9.3 服务调用的异常处理
Feign 远程调用失败时的异常处理需要分层设计:
- 网络异常:连接超时、读取超时,应该重试或快速失败。
- 业务异常:下游返回的业务错误码,应该透传给调用方,不能简单打印日志。
- 序列化异常:参数或返回结果字段不匹配,需要在联调阶段排查。
推荐在 Feign 接口上使用 fallback 指定降级类:
java复制@FeignClient(name = "product-service", fallback = ProductFeignClientFallback.class)
public interface ProductFeignClient {
@GetMapping("/product/{id}")
ProductDTO getProductById(@PathVariable("id") Long id);
}
降级类必须实现 Feign 接口:
java复制@Component
public class ProductFeignClientFallback implements ProductFeignClient {
@Override
public ProductDTO getProductById(Long id) {
ProductDTO dto = new ProductDTO();
dto.setId(id);
dto.setName("商品服务暂不可用");
dto.setPrice(-1);
return dto;
}
}
另外注意:使用 fallback 时必须开启 Feign 的熔断能力:
yaml复制feign:
circuitbreaker:
enabled: true
9.4 幂等设计与接口安全
服务重试和消息重发都要求接口具备幂等性。比如订单服务创建订单接口,如果客户端因为超时重试,可能产生两笔订单。常见的幂等方案是使用“业务幂等号”:调用方生成一个唯一流水号,服务端在执行业务前先检查该流水号是否已经处理过。
网关层则可以统一校验 token 和签名,将认证逻辑放在网关而不是每个服务单独实现,使安全问题收敛到一个入口,减少遗漏的风险。
9.5 日志链路追踪的预留
微服务一次请求会跨多个服务,排查问题时如果没有 traceId,很难将散落在多个服务中的日志串联起来。建议在网关全局过滤器中为每个请求生成一个 traceId,放入请求头,在 Feign 调用时自动透传,各服务日志输出统一带上这个标记。这样可以在一开始就为后续接入 SkyWalking、Zipkin 等链路追踪系统打好基础。
10. 总结与学习路线
到这里,一套最小可运行的微服务电商案例已经梳理完整:从 Nacos 完成服务注册和配置管理,到 OpenFeign 实现服务间远程调用,再到 Spring Cloud Gateway 统一入口转发,Sentinel 保护流量和调用链,最后用 Seata 解决跨服务事务问题。
如果只看教程不动手,很难真正理解微服务架构。建议按下面顺序逐层练习:
- 先跑通基于 Nacos 的服务注册和配置中心。
- 用 OpenFeign 实现两个服务之间的调用。
- 接入 Gateway,把外部请求从单调端口变成统一网关入口。
- 给核心接口配置 Sentinel 流控和熔断规则。
- 模拟一个跨服务写操作,理解 Seata 的回滚流程。
每一步都建议在写代码之前先画一张简单的调用链路图,哪怕只是手绘的差不多也行,目的不是画图本身,而是逼着自己理清请求从入口到落库经过了哪些服务和组件,每一层保护了什么、牺牲了什么。这套思维建立起来后,再遇到架构选型和线上排错,就不会只靠搜索片段拼凑方案了。
