实验7全流程
## 实验七:微服务综合项目实战(零基础全流程)
本实验基于 **Spring Boot 3.5.x** + **Spring Cloud 2025.0.1** + **RabbitMQ 4.2.3** + **Redis 7.x**,带你从零搭建一个完整的电商下单系统:
**用户请求 → Gateway网关 → 订单服务 → RabbitMQ消息 → 库存服务**,并实现限流、熔断、认证等功能。
> **⚠️ 重要提示**:本实验已废弃 Hystrix(已停更),改用 **Resilience4j**;Gateway 基于 WebFlux,不可使用 Servlet 相关配置。
---
## 一、准备工作(环境安装)
### 1.1 安装 JDK 17
- 下载:https://adoptium.net/ (选择 Eclipse Temurin 17 LTS)
- 安装后命令行验证:`java -version` 应显示 `openjdk version "17.x.x"`
### 1.2 安装 Maven 3.9+
- 下载:https://maven.apache.org/download.cgi
- 解压并配置环境变量 `MAVEN_HOME`,`PATH` 添加 `%MAVEN_HOME%\bin`
- 验证:`mvn -version`
### 1.3 安装 RabbitMQ 4.2.3(使用 Docker)
```bash
# 拉取带管理插件的镜像
docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:4.2.3-management
# 安装延迟消息插件(本实验非必须,但建议)
docker exec -it rabbitmq bash
cd /plugins
wget https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases/download/v4.2.0/rabbitmq_delayed_message_exchange-4.2.0.ez
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
exit
docker restart rabbitmq
```
验证:浏览器访问 `http://localhost:15672`,用户名/密码 `guest/guest`。
### 1.4 安装 Redis(用于网关限流)
- **Windows**:下载 Redis for Windows(或使用 WSL)
- **Mac**:`brew install redis`
- **Linux**:`sudo apt install redis-server`
- **Docker(推荐)**:
```bash
docker run -d --name redis -p 6379:6379 redis:7-alpine
```
验证:`redis-cli ping` 返回 `PONG`。
### 1.5 安装 Eureka Server(注册中心)
我们单独创建一个 Eureka Server 模块,或使用 Nacos(本实验用 Eureka,简单演示)。
> 注意:Eureka 在 Spring Boot 3.x 中仍然可用,但官方更推荐 Nacos。教学场景保留。
新建 Maven 模块 `eureka-server`,`pom.xml`:
```xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.13</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<version>4.1.2</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2025.0.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
```
启动类 `EurekaServerApplication.java`:
```java
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
```
配置文件 `application.yml`:
```yaml
server:
port: 8761
eureka:
client:
register-with-eureka: false
fetch-registry: false
```
---
## 二、项目整体结构
创建父工程 `microservice-mall`(Maven 项目,packaging 为 pom),然后依次创建以下子模块:
```
microservice-mall
├── gateway-server (端口 8080)
├── order-service (端口 8082)
├── stock-service (端口 8083)
└── eureka-server (端口 8761)
```
每个模块都继承父工程的 `spring-boot-starter-parent` 版本 3.5.13,并统一管理 Spring Cloud 版本 2025.0.1。
**父工程 pom.xml 核心部分**:
```xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.13</version>
<relativePath/>
</parent>
<properties>
<java.version>17</java.version>
<spring-cloud.version>2025.0.1</spring-cloud.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>
</dependencies>
</dependencyManagement>
```
---
## 三、开发网关服务(gateway-server)
### 3.1 添加依赖
```xml
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- 服务发现客户端(Eureka) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>4.1.2</version>
</dependency>
<!-- Redis 响应式限流 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
</dependencies>
```
### 3.2 配置文件 application.yml
```yaml
server:
port: 8080
spring:
application:
name: gateway-server
data:
redis:
host: localhost
port: 6379
cloud:
gateway:
# 动态路由:根据服务名自动转发
discovery:
locator:
enabled: true
lower-case-service-id: true
routes:
- id: order-route
uri: lb://order-service # 负载均衡到 order-service
predicates:
- Path=/order/** # 匹配路径 /order/**
filters:
- StripPrefix=1 # 去掉 /order 前缀
# 限流过滤器(需要 KeyResolver)
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 5 # 每秒令牌数
redis-rate-limiter.burstCapacity: 10 # 最大突发令牌数
key-resolver: "#{@ipKeyResolver}"
# 全局跨域配置(注意 allow-credentials 与 allowed-origin-patterns 配套)
globalcors:
cors-configurations:
'[/**]':
allowed-origin-patterns: "*"
allowed-methods: "*"
allowed-headers: "*"
allow-credentials: true
max-age: 3600
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
```
### 3.3 启动类 GatewayApplication
```java
package com.example.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import reactor.core.publisher.Mono;
@SpringBootApplication
@EnableDiscoveryClient // 可以省略,但加上更清晰
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
// 限流 KeyResolver:按客户端 IP 限流
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()
);
}
}
```
### 3.4 全局 Token 认证过滤器
在 `GatewayApplication` 类中增加一个 `GlobalFilter` Bean,用于简单的 Token 校验。
```java
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Bean
@Order(0)
public GlobalFilter tokenAuthFilter() {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
// 只拦截下单接口
if (path.contains("/order/create")) {
String token = request.getHeaders().getFirst("Token");
if (token == null || !token.startsWith("admin-")) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
}
return chain.filter(exchange);
};
}
```
---
## 四、开发订单服务(order-service)
### 4.1 添加依赖
```xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>4.1.2</version>
</dependency>
<!-- Resilience4j 熔断 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
<!-- Jackson 序列化 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
```
### 4.2 配置文件 application.yml
```yaml
server:
port: 8082
spring:
application:
name: order-service
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
virtual-host: /
publisher-confirm-type: correlated # 开启发送确认
publisher-returns: true
connection-timeout: 10000
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
# Resilience4j 熔断配置
resilience4j:
circuitbreaker:
instances:
orderService:
failure-rate-threshold: 50 # 失败率阈值 50%
wait-duration-in-open-state: 5s # 熔断后等待 5 秒
sliding-window-size: 10
timelimiter:
instances:
orderService:
timeout-duration: 3s # 超时时间 3 秒
```
### 4.3 RabbitMQ 配置类(声明交换机和队列)
**必须**:如果不声明,发送消息到不存在的交换机时会被丢弃。
```java
package com.example.order.config;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitMQConfig {
public static final String ORDER_EXCHANGE = "order-exchange";
public static final String STOCK_QUEUE = "stock-queue";
public static final String ROUTING_KEY = "order.stock";
@Bean
public DirectExchange orderExchange() {
return new DirectExchange(ORDER_EXCHANGE, true, false);
}
@Bean
public Queue stockQueue() {
return QueueBuilder.durable(STOCK_QUEUE).build();
}
@Bean
public Binding stockBinding() {
return BindingBuilder.bind(stockQueue())
.to(orderExchange())
.with(ROUTING_KEY);
}
}
```
### 4.4 消息实体类
```java
package com.example.order.entity;
public class StockMessage {
private String orderId;
private String productId;
private Integer num;
// 无参构造、有参构造、getter/setter 省略(请自行补充)
}
```
### 4.5 订单服务类
```java
package com.example.order.service;
import com.example.order.entity.StockMessage;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.UUID;
@Service
public class OrderService {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private ObjectMapper objectMapper;
public String createOrder(String userId, String productId, Integer num) {
String orderId = UUID.randomUUID().toString().replace("-", "");
StockMessage msg = new StockMessage(orderId, productId, num);
try {
String json = objectMapper.writeValueAsString(msg);
// 发送到 order-exchange,路由键 order.stock
rabbitTemplate.convertAndSend(
RabbitMQConfig.ORDER_EXCHANGE,
RabbitMQConfig.ROUTING_KEY,
json
);
return "订单创建成功,订单ID:" + orderId;
} catch (Exception e) {
e.printStackTrace();
return "订单创建失败:" + e.getMessage();
}
}
}
```
### 4.6 订单控制器(注意使用 POST)
```java
package com.example.order.controller;
import com.example.order.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping("/create")
public String createOrder(@RequestParam String userId,
@RequestParam String productId,
@RequestParam Integer num) {
return orderService.createOrder(userId, productId, num);
}
}
```
### 4.7 启动类
```java
package com.example.order;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
```
---
## 五、开发库存服务(stock-service)
### 5.1 依赖(与 order-service 基本一致,不需要 web 依赖?需要 web 吗?不需要对外提供 HTTP 接口,但保留也可)
简化:只需 amqp 和 eureka client。
```xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
```
### 5.2 配置文件 application.yml
```yaml
server:
port: 8083
spring:
application:
name: stock-service
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
virtual-host: /
listener:
simple:
acknowledge-mode: manual # 手动确认,防止消息丢失
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
```
### 5.3 消息监听器(库存扣减)
```java
package com.example.stock.listener;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Component
public class StockMessageListener {
// 模拟库存数据库
private Map<String, Integer> stockMap = new HashMap<>() {{
put("product001", 100);
put("product002", 200);
}};
@Autowired
private ObjectMapper objectMapper;
@RabbitListener(queues = "stock-queue")
public void handleStockMessage(String message, Channel channel, Message msg) throws IOException {
try {
// 解析消息
com.fasterxml.jackson.databind.JsonNode node = objectMapper.readTree(message);
String productId = node.get("productId").asText();
Integer num = node.get("num").asInt();
String orderId = node.get("orderId").asText();
// 扣减库存
if (stockMap.containsKey(productId) && stockMap.get(productId) >= num) {
int remain = stockMap.get(productId) - num;
stockMap.put(productId, remain);
System.out.println("库存更新成功:商品" + productId + ",扣减" + num + ",剩余" + remain);
// 手动确认消息(告诉 RabbitMQ 可以删除此消息)
channel.basicAck(msg.getMessageProperties().getDeliveryTag(), false);
} else {
System.out.println("库存不足,商品:" + productId + ",请求数量:" + num);
// 拒绝消息并重新入队(可选)
channel.basicNack(msg.getMessageProperties().getDeliveryTag(), false, true);
}
} catch (Exception e) {
e.printStackTrace();
// 消息格式错误,拒绝且不重新入队
channel.basicNack(msg.getMessageProperties().getDeliveryTag(), false, false);
}
}
}
```
### 5.4 启动类
```java
package com.example.stock;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class StockServiceApplication {
public static void main(String[] args) {
SpringApplication.run(StockServiceApplication.class, args);
}
}
```
---
## 六、运行与测试(全链路)
### 6.1 启动顺序
1. **RabbitMQ**(Docker 容器)
2. **Redis**(Docker 容器)
3. **EurekaServer**(端口 8761)
4. **OrderService**(端口 8082)
5. **StockService**(端口 8083)
6. **GatewayServer**(端口 8080)
检查 Eureka 控制台 `http://localhost:8761`,应看到 `GATEWAY-SERVER`、`ORDER-SERVICE`、`STOCK-SERVICE` 均已注册。
### 6.2 正常下单测试
使用 **Postman** 发送 POST 请求:
- **URL**:`http://localhost:8080/order/create?userId=user001&productId=product001&num=5`
- **Headers**:`Token: admin-test`
- **Method**:POST
**预期结果**:
- 网关控制台打印请求日志。
- 订单服务控制台打印“订单创建成功”。
- 库存服务控制台打印“库存更新成功:商品product001,扣减5,剩余95”。
- 客户端收到 `订单创建成功,订单ID:xxxxx`。
### 6.3 无 Token 测试(认证拦截)
去掉 `Token` 头,发送相同请求 → 应返回 **401 Unauthorized**。
### 6.4 限流测试
快速连续发送 10 次请求,超过令牌桶容量(10)后会出现 **429 Too Many Requests**(网关限流生效)。
### 6.5 熔断测试(模拟库存服务故障)
1. 停止 `stock-service` 或断开 RabbitMQ。
2. 发送下单请求 → 订单服务调用 `rabbitTemplate.convertAndSend` 会抛出异常(因为无法连接到 RabbitMQ 或队列不存在)。在 `OrderService` 中我们直接捕获异常并返回失败信息,没有实现熔断降级。为演示 Resilience4j 熔断,我们可以给 `createOrder` 方法加上 `@CircuitBreaker`。
**改进 order-service 的 OrderService**:
```java
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
@Service
public class OrderService {
// ...
@CircuitBreaker(name = "orderService", fallbackMethod = "createOrderFallback")
public String createOrder(String userId, String productId, Integer num) {
// 原有代码
}
public String createOrderFallback(String userId, String productId, Integer num, Throwable t) {
return "订单创建失败,库存服务暂时不可用,请稍后重试";
}
}
```
并在启动类添加 `@EnableCircuitBreaker`(Resilience4j 需要 `@EnableCircuitBreaker` 或直接使用配置)。重新测试,当 RabbitMQ 不可用时,会进入降级方法。
---
## 七、常见问题与解决方案(零基础必看)
| 问题现象 | 可能原因及解决方法 |
| ---------------------------- | -------------------------------------------------------------------------------------------------- |
| Gateway 启动报错 | 1. 检查是否引入了 `spring-boot-starter-web`(与 WebFlux 冲突),删除之。<br>2. 检查 Redis 是否启动,限流需要 Redis。 |
| 请求返回 404 | Gateway 路由断言路径写错;或者 `StripPrefix` 导致转发路径错误;或者下游服务未启动。 |
| 消息发送成功但库存未扣减 | 1. 确认 RabbitMQ 交换机、队列是否存在(登录管理界面查看)。<br>2. 确认路由键与监听器一致。 |
| 库存重复扣减 | 消费者未使用手动确认(`acknowledge-mode: manual`),导致消息重复投递。需要调用 `basicAck`。 |
| 跨域请求失败 | 检查网关 `globalcors` 配置,`allow-credentials: true` 时不能用 `allowed-origins: "*"`,必须用 `allowed-origin-patterns: "*"`。 |
| Eureka 服务注册不成功 | 检查 `eureka.client.service-url.defaultZone` 地址是否正确;网络防火墙是否开放。 |
---
## 八、实验报告要点
1. **架构图**:画出 Gateway、Order、Stock、RabbitMQ、Eureka 之间的关系。
2. **核心配置**:粘贴 Gateway 的 `application.yml`、RabbitMQ 配置类、限流 KeyResolver 等。
3. **代码片段**:订单服务发送消息、库存服务消费消息、手动 ACK 处理。
4. **测试结果**:正常下单截图、无 Token 返回 401 截图、限流 429 截图、熔断降级截图。
5. **问题记录**:记录你实际遇到的错误(如依赖冲突、路由失败、消息丢失)以及解决过程。
6. **思考题**:解释 Gateway 限流原理、Resilience4j 熔断与 Hystrix 的区别、为什么需要手动确认消息。
---
## 九、总结
通过本实验,你完整实现了:
- **Spring Cloud Gateway** 路由、限流、认证拦截。
- **消息驱动** 订单服务与库存服务解耦,RabbitMQ 4.2.3 消息可靠性。
- **服务注册与发现** 使用 Eureka。
- **熔断降级** 使用 Resilience4j 替代过时 Hystrix。
现在你可以启动所有服务,验证完整的电商下单流程了。祝实验顺利!
