微服务架构 · 01 实战使用手册
定位:企业级落地指南。看完能直接在项目里搭起微服务骨架,接入主流组件。 技术栈:Spring Boot 3.x + Spring Cloud 2022.x + Spring Cloud Alibaba 2022.x
第 1 章:工程搭建
1.1 版本选型
微服务项目最常见的踩坑不是代码问题,是版本对不上。三个框架的版本必须成套使用:
| Spring Boot | Spring Cloud | Spring Cloud Alibaba | 说明 |
|---|---|---|---|
| 3.0.x | 2022.0.x | 2022.0.0.0 | 推荐,支持 JDK 17+ |
| 2.7.x | 2021.0.x | 2021.0.5.0 | 稳定,JDK 8/11 兼容 |
| 2.6.x | 2021.0.x | 2021.0.4.0 | 老项目维护用 |
⚠️ 陷阱:不要自己拼版本,去 Spring Cloud Alibaba 版本说明 看对照表。
spring-cloud-alibaba-dependencies和spring-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();
}
}
⚠️ 陷阱:
blockHandler和fallback是两个不同的兜底,很多人搞混:
blockHandler:Sentinel 触发流控/熔断时调用,参数里有BlockExceptionfallback:接口本身抛了业务异常(如 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-技术深度精讲》对应章节。