Skip to content

Spring Cloud 核心组件 ​

S4-020建议学高级约 90 分钟JDK 21企业开发进阶 · 微服务

1. 本节定位 ​

《微服务架构与拆分原则》讲了"为什么要拆";本节讲**"拆完之后靠什么把它们连起来"**:

text
Spring Cloud 的一整套组件,都在解决同一类问题:
「服务很多、互相调用、还会挂」——怎么让整体还能正常工作  ✓

★ 本节重点讲其中最核心的一个:网关 ✓ —— 因为它是所有流量的必经之路,也是问题最先暴露的地方 ✓✓

2. 学习目标 ​

  • 能说出 Spring Cloud 六大组件各自解决什么问题 ★★★;
  • 能说出网关的四项职责与三个"不要做" ★★★;
  • 知道网关的三大性能陷阱(尤其"过滤器里的远程调用") ★★★;
  • 能对比三种限流算法,并按场景选 ★★★;
  • 知道限流的四个维度与选错的风险 ★★★。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

先看一个"网关扛不住"的真实场景 ✗✗:

text
现象:网关 CPU 100%,所有服务都超时,但后端服务本身很闲 ✗
排查:网关的鉴权过滤器里**每次都远程调用鉴权服务** ✗
      → 鉴权服务慢 50 ms,网关的线程全卡在那儿 → 网关成为瓶颈 ✗✗

★ 本节的第 5.3 节就是讲这个 ✓ —— 网关的性能陷阱不在"过滤器多",而在"过滤器里有远程调用" ✓✓

5. 核心概念 ​

5.1 六大组件各自解决什么 ★★★ ​

组件解决什么本项目对应章节
注册与配置(Nacos)★★★服务在哪、配置是什么《Nacos 注册与配置中心》 ✓
网关(Gateway)★★★统一入口、路由、鉴权、限流本节 ✓
负载均衡(LoadBalancer)★★请求分给哪个实例《Nginx 负载均衡与缓存》 ✓
熔断限流(Sentinel)★★★依赖出问题时不被拖死《Sentinel 限流与熔断》 ✓
分布式事务(Seata)★★跨服务的数据一致性《Seata 分布式事务》 ✓
链路追踪(Sleuth/Micrometer)★★★一次请求经过了哪些服务见 5.4 ✓

★ 一条主线 ✓✓:

注册与配置是"通讯录",网关是"门",负载均衡是"分诊",熔断是"保险丝",追踪是"行车记录仪" ✓

5.2 网关了四项职责与三个"不要做" ★★★ ​

★ 四项职责 ✓:

职责说明
路由 ★★★把 /api/order/** 转给订单服务 ✓
过滤 ★★★鉴权、日志、埋点、改写请求头 ✓
限流 ★★按维度限制速率(见 5.5)✓
协议转换 ★对外 HTTPS / 对内 HTTP(TLS 卸载,《Nginx 安装与反向代理》)✓

★★ 三个"不要做"(分层原则) ✓✓:

#不要做原因
①不要在网关里写业务逻辑 ✗✗那会让网关变成新的单体,且改业务要发网关 ✓
②不要在网关里做重 IO(查库、调多个服务)✗✗它是所有流量的必经之路 ✓✓
③不要让网关成为单点 ✗✗它自己也要多实例 + 前面还有 LB ✓

★ 一句话定位 ✓✓:

网关只做"所有服务都需要、且与业务无关"的事 —— 路由、鉴权、限流、TLS 卸载、跨域、灰度、日志、追踪入口 ✓

5.3 网关的三大性能陷阱 ★★★ ​

★ 陷阱一:路由匹配 ★★

本节的实验用"30 条路由逐个匹配、命中最后一条"来测最坏情况 ✓ —— 每条路由都要做一次字符串比较或正则匹配 ✓

★ 关键认识 ✓✓:

单次匹配只要几微秒,但网关是「每个请求」都要做 ✓

10 万 QPS 下,全花在路由匹配上的 CPU 时间就很可观 ✗

所以真实网关的优化是「别逐个匹配」 ✓✓: 按前缀建 Trie、把最贵的正则路由放到最后、预编译 Pattern、缓存路由结果 ✓

★ 陷阱二:过滤器链 ★

本节的实验对比了 0 / 1 / 3 / 6 / 10 个过滤器的开销 ✓ —— 但这个数字其实不重要 ✗ ——因为空过滤器的开销几乎可以忽略 ✓

★★ 真正的陷阱是第三个 ✓✓:

过滤器里做什么成本
解析 JWT 并本地验签 ✓微秒级 ✓
查 Redis 做限流 ⚠️一次网络往返(0.1~1 ms) ✗
调鉴权服务 ✗✗一次远程调用 ✗✗

★★★ 对策 ✓✓:

#对策说明
①JWT 用公钥本地验签 ★★★不要每次都调鉴权服务 ✓✓
②两级限流 ★★本地计数器粗筛 + Redis 精确 ✓
③业务逻辑放各自的微服务 ★★★网关只做通用逻辑 ✓✓

5.4 链路追踪:拆完之后的第一刚需 ★★★ ​

★ 没有它会发生什么 ✗✗:

text
用户报"下单很慢" → 你查订单服务日志(正常)→ 查库存服务(正常)
→ 查优惠券服务(也正常)→ **到底哪慢?** ✗✗

★ 核心机制只有两件事 ✓✓:

机制作用
TraceId ★★★一次请求的全局唯一 ID,跨进程透传 ✓✓
Span ★★每个服务内部的一段耗时,拼起来就是调用树 ✓

★ 实现要点 ✓:

text
① 入口生成 TraceId(网关)
② 每次远程调用把它放进请求头(如 traceparent / X-Trace-Id)
③ 每个服务把它放进日志的 MDC → 日志里能按 TraceId 搜全链路 ✓✓

★ 一条实战经验 ✓✓:

TraceId 一定要打进日志 ✓

否则"能追踪"只存在于控制台里,排查时还是得靠猜 ✗

★ 本项目已有的基础 ✓:《日志框架与日志规范》 讲过日志与 traceId 的落地 ✓

5.5 三种限流算法与四个维度 ★★★ ​

★ 三种算法的差别(本节的实验对比了它们的实际放行数) ✓:

算法行为适用代表
固定窗口 ✗窗口边界可能双倍放行 ✗✗精度要求不高计数器 + 过期 ✓
滑动窗口 ✓✓准确控制总速率要求精确Sentinel 默认 ✓
令牌桶 ✓✓允许突发(桶里有存量就能一次性拿走)秒杀放行一批Guava RateLimiter ✓
漏桶 ✓强制匀速保护脆弱的下游nginx limit_req ✓

★ 固定窗口的问题(本实验刻意设计的对比) ✓✓:

text
限额 100 QPS,请求均匀到达时看不出差别;
但如果请求集中在窗口边界(如 0.9s~1.1s),
固定窗口会放行「前一个窗口的 100 + 后一个窗口的 100 = 200」✗✗

★★ 四个限流维度(选错会"误伤"或"漏网") ✓✓:

维度适用风险
按 IP ★未登录 / 防刷NAT 后大量用户共用一个 IP → 误伤 ✗✗
按用户 ★★已登录业务需要先解析登录态(有成本)⚠️
按接口 ★★保护特定昂贵接口无法防"单用户刷全站"⚠️
按 API Key ★★★开放平台最推荐 ✓✓

★★ 一条最重要的限流原则 ✓✓:

限流要限在「被保护方的能力」上,而不是拍脑袋定数字 ✗

先压测出下游容量(T2-003 的方法)✓

再按容量的 70%~80% 设阈值 ✓✓

★ 还有一条:限流之后要有"优雅的拒绝" ✓✓:

返回客户端行为
429 + Retry-After ✓✓客户端会按提示退避重试 ✓
直接断连或返回 500 ✗✗客户端可能立刻重试 → 放大问题 ✗

5.6 配置对照 ★★ ​

★ 本节的实验模型 vs Spring Cloud Gateway 配置 ✓:

yaml
spring:
  cloud:
    gateway:
      routes:
        - id: order-route
          uri: lb://order-service              # lb 表示走负载均衡(S4-016)
          predicates:
            - Path=/api/order/**               # 路径匹配
            - Method=GET,POST                  # 方法匹配
            - Header=X-Api-Version, v1         # 头匹配
          filters:
            - StripPrefix=1                    # 去掉 /api 再转发
            - name: RequestRateLimiter         # 限流
              args:
                redis-rate-limiter.replenishRate: 100    # 令牌桶:每秒补 100
                redis-rate-limiter.burstCapacity: 200    # 桶容量(允许突发到 200)
                key-resolver: "#{@userKeyResolver}"      # 按用户 / 按 IP / 按接口

★ 三个要点 ✓✓:

#要点说明
①lb:// 前缀 ★★表示"按服务名 + 负载均衡找实例"(依赖 《Nacos 注册与配置中心》 的注册中心)✓
②replenishRate vs burstCapacity ★★★前者决定稳态速率,后者决定允许多大的突发 ✓
③key-resolver 决定限流维度 ★★★选错就是 5.5 表的风险 ✓

6. 递进示例 ​

6.1 最小示例:一条路由 ​

yaml
spring:
  cloud:
    gateway:
      routes:
        - id: user-route
          uri: lb://user-service
          predicates:
            - Path=/api/user/**

6.2 业务示例:按用户限流 ​

java
@Bean
public KeyResolver userKeyResolver() {
    // 从请求里取用户 ID 作为限流的 key
    return exchange -> Mono.justOrEmpty(
            exchange.getRequest().getHeaders().getFirst("X-User-Id"))
            .defaultIfEmpty("anonymous");
}

6.3 反例:在网关里做业务校验 ​

java
// ✗ 网关里查库存、算价格、判断能不能下单
if (!inventoryService.hasStock(productId)) { return 400; }

★ 后果 ✓: ① 网关变成单体;② 每次请求多一次远程调用;③ 改业务规则要发网关 ✗✗

7. 完整可运行代码与实测 ​

代码:labs/distributed-lab/src/main/java/cn/kb/learn/dist/micro/GatewayLesson.java

运行:

powershell
cd labs
.\mvnw.cmd -pl distributed-lab compile
java "-Dstdout.encoding=UTF-8" -cp distributed-lab/target/classes cn.kb.learn.dist.micro.GatewayLesson

本实验测三件事 ✓:

实验量出什么
路由匹配30 条路由逐个匹配(命中最后一条)的单次耗时,并用"10 万 QPS 要花多少 CPU"换算 ✓
过滤器链0 / 1 / 3 / 6 / 10 个过滤器的开销对比(结论:空过滤器几乎可忽略)✓
限流算法限额 100、300 个请求挤在 1 秒内时,三种算法各自放行多少 ✓✓

★ 限流实验的设计意图 ✓:

text
300 个请求、每 3 ms 来一个(总跨度约 900 ms,小于 1 秒窗口)
  固定窗口 → 放行 100(都在同一窗口内,不超限)
  滑动窗口 → 放行 100(同样不超限)
  令牌桶   → 放行约 190(初始 100 个 + 这段时间补充的约 90 个)
→ **令牌桶"允许突发"的语义就是这样体现的** ✓✓

★ 三条可落地的结论 ✓:

#结论
①网关的性能陷阱不是"过滤器多",而是"过滤器里有远程调用" ✓✓
②路由匹配要避免逐个正则匹配(把最贵的放最后 + 预编译 + 缓存)✓
③限流按"被保护方的容量"定阈值,并返回 429 + Retry-After ✓

8. Debug 观察点 ​

  1. 在网关的一个过滤器里加 Thread.sleep(50):观察整体吞吐断崖 ✓✓;
  2. 把路由顺序调换(把正则路由放最前):观察匹配耗时上升 ✓;
  3. 把限流维度从"按用户"改成"按 IP":在 NAT 环境下观察误伤 ✗;
  4. 把限流的拒绝响应从 429 改成 500:观察客户端重试放大流量 ✗✗;
  5. 打开网关的 access log 并带上 TraceId:验证全链路可查 ✓。

9. 常见错误 ​

#错误说明
1在网关里写业务逻辑 ✗✗网关变单体 ✓
2过滤器里每次都调鉴权服务 ✗✗网关成为瓶颈 ✓✓
3网关只部署一台 ✗✗单点 = 全站挂 ✓
4用固定窗口限流,却在窗口边界被打穿 ✗✗双倍放行 ✓
5按 IP 限流却不考虑 NAT ✗✗误伤整栋楼/整个公司 ✓
6限流返回 500 而不是 429 ✗✗客户端重试放大问题 ✓
7没有链路追踪就上微服务 ✗✗出问题查不出来 ✓

10. 动手实验 ​

目标:把网关的三类问题都亲手跑一遍。

步骤:

  1. 跑 GatewayLesson,记录路由耗时、过滤器开销、三种限流的放行数 ✓;
  2. 在实验里加一个"模拟远程调用"的过滤器(Thread.sleep(20)),观察吞吐下降 ★★★;
  3. 把限流的 key 从"用户"改成"IP",思考在你业务里会不会误伤 ✓;
  4. 给网关加 TraceId 透传,并在下游日志里打出来 ★★★;
  5. 用压测验证:去掉网关 vs 经过网关的 P99 差多少 ✓。

验收方式:能说出"你的网关在最高峰时会先崩在哪一环"。

扩展任务:给网关设计一个"灰度路由"方案:

  • 按用户 ID 尾号分流(0~9 走新版本)✓;
  • 按 Header 强制指定版本(测试用)✓;
  • 支持秒级回滚(把权重改回 0)✓;
  • 说明:为什么灰度必须在网关层(提示:它能看到全部流量)★★

11. 练习题 ​

  1. 判断:网关里过滤器越多,性能越差。(对 / 错)
  2. 填空:网关的四项职责是 、、、。
  3. 简答:为什么"在网关里调鉴权服务"是危险的?
  4. (配置阅读)
yaml
- name: RequestRateLimiter
  args:
    key-resolver: "#{@ipKeyResolver}"
    redis-rate-limiter.replenishRate: 1000

这个配置在什么场景下会误伤用户? 5. (设计)给"开放平台"设计限流方案:不同客户等级不同配额,超限要能看出是谁超的。

参考答案(要点)
  1. 错。空过滤器几乎可忽略 ✗ —— 真正的瓶颈是"过滤器里有远程调用或重 IO" ✓✓★
  2. 路由;过滤;限流;协议转换 ✓
  3. 因为它给每个请求都加一次远程调用 ✗✗ —— 鉴权服务慢 50 ms,网关的线程就全卡在那里 → 网关自己成为瓶颈, 而后端服务可能很闲 ✓✓(可改用 JWT 本地验签)
  4. 按 IP 限流,而客户端可能在同一个 NAT 出口后(公司/学校/小区宽带)—— 1000 QPS 被整栋楼共享 → 少数人刷接口会让其他人全部 429 ✗✗★
  5. 要点:按 API Key 限流(每客户一个 key + 配额)✓✓; 不同等级用不同 replenishRate / burstCapacity(按 key 动态取配额)✓; 拒绝时返回 429 + 剩余配额 + 重置时间,并在日志里带上客户 ID ✓✓

12. 掌握标准 ​

  • [ ] 能说出 Spring Cloud 六大组件各自解决什么 ★★★;
  • [ ] 能说出网关的四项职责与三个"不要做" ★★★;
  • [ ] 知道网关的性能陷阱在"过滤器里的远程调用" ★★★;
  • [ ] 知道链路追踪只需 TraceId + Span 两个概念,且必须打进日志 ★★★;
  • [ ] 能对比四种限流算法,并按场景选 ★★★;
  • [ ] 知道四个限流维度的风险(尤其按 IP 的 NAT 误伤) ★★★;
  • [ ] 知道限流要按"被保护方的容量"定阈值 ★★★。

13. 扩展资源 ​

14. 下一步学习 ​

《Nacos 注册与配置中心》:服务怎么找到彼此、配置怎么动态生效。

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引