CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
系统设计入门/课时/微服务与网关
RawBlame
课时目录12 节

系统设计入门

  1. 01系统设计是什么
  2. 02性能与扩展基础
  3. 03一致性与 CAP
  4. 04负载均衡
  5. 05缓存模式
  6. 06异步与消息
  7. 07数据架构
  8. 08微服务与网关
  9. 09云设计模式
  10. 10可靠性模式
  11. 11性能反模式
  12. 12综合练习

微服务与网关

服务发现、BFF、API Gateway、Strangler Fig。

35 分钟

微服务与网关

这一课你将能做到

  1. 用三条可观测信号判断本站单体该不该拆,而不是凭感觉
  2. 写出 Spring Cloud Gateway 的路由配置,说清网关与 Nginx、BFF 的职责边界
  3. 设计一次 Strangler Fig(绞杀者模式)渐进迁移方案,做到随时可回退

为什么要学

「我们该上微服务吗」是技术决策里最容易被情绪化的问题。拆早了是自找分布式复杂度(分布式事务、链路追踪、运维翻倍);拆晚了是发布互相阻塞、一个模块 OOM 全站陪葬。这课给你的是判断框架,不是信仰。

面试必考:「单体和微服务怎么选?」「服务拆分的粒度?」「网关的作用?」「单体怎么平滑迁移到微服务?」——最后一问用绞杀者模式回答,是高级感所在。

人话:单体像一家人一起做饭,省事但谁想加菜全桌等;微服务像楼下美食城,每个档口独立经营,但要引入统一的收银台(网关)、配送(服务调用)、卫生标准(监控)。摊位越多越自由,协调成本也越高。

先建立图像

本站现状:一个 Spring Boot 单体装着题目管理、判题调度、用户进度、排行榜。什么时候到拆分临界点?三条信号:

信号 1(扩展差异化):判题机要扩容到 50 台,但题目服务 3 台就够
                     —— 资源画像完全不同,却绑在同一发布单元
信号 2(发布争抢):运营改题目和判题引擎升级总在打架,
                   一次回归测试要覆盖全站
信号 3(团队规模):多于 2 个披萨团队(每队 4~8 人),
                   代码合并冲突频繁、领域边界模糊

只有信号 1 时,先拆「部署」(判题 worker 独立部署,代码还是同一个仓库甚至同一个 jar);三条信号都有才考虑拆「服务」。先拆部署单元,再拆代码仓库,最后拆团队——这是最省钱的路径。

拆分后的形态:

   Web / 小程序 / App
          │
    API Gateway(鉴权、限流、路由)
          │
   ┌──────┼──────────┬──────────┐
题目服务  判题服务    用户服务    排行榜服务
(problem) (judge)    (user)     (ranking)
  MySQL    Kafka      MySQL      Redis

组件职责表:

组件职责本站实现
API Gateway统一入口:鉴权、限流、路由、TLS、灰度Spring Cloud Gateway / Nginx
BFF为特定前端聚合裁剪接口,减少往返网关聚合或轻量聚合层
服务发现实例注册与健康,动态寻址Nacos / Consul / K8s Service
服务间通信同步 RPC / 异步事件OpenFeign + Kafka

跟着做

步骤 1:网关路由配置(Spring Cloud Gateway)

spring:
  cloud:
    gateway:
      routes:
        - id: problem-route
          uri: lb://problem-service        # lb=从注册中心负载均衡
          predicates:
            - Path=/api/problems/**
          filters:
            - name: RequestRateLimiter     # 网关层统一限流
              args:
                redis-rate-limiter.replenishRate: 100
                redis-rate-limiter.burstCapacity: 200
        - id: judge-route
          uri: lb://judge-service
          predicates:
            - Path=/api/submissions/**
        - id: judge-internal-deny          # 内部接口外部禁止直达
          uri: no://op
          predicates:
            - Path=/internal/**
          filters:
            - name: SetStatus
              args:
                status: 403

三条路由说明网关的三个核心动作:按路径路由到不同服务、统一限流、保护内部接口。鉴权也在网关做(解析 token 后透传 userId Header),下游服务不再重复校验。

步骤 2:服务间调用的正确姿势

// 同步:题目页需要用户服务补充「我的提交状态」
@FeignClient(name = "user-service")
public interface UserClient {
    @GetMapping("/internal/users/{id}/progress")
    Map<Long, String> getProgress(@PathVariable long id, @RequestParam List<Long> problemIds);
}

// 异步:判题结果走事件(第 6 课已建),不阻塞判题链路

选择标准:实时要答案的用同步(带超时+熔断),不阻塞主流程的用事件。判题完成→排行榜是异步;题目页→我的进度是同步聚合。

步骤 3:BFF 消灭 Chatty I/O

题目详情页原来要调 3 个接口(题目信息、我的进度、相关推荐):

改造前:浏览器 → 3 次 HTTP → 3 个服务      RTT 3×50ms + 串行
改造后:浏览器 → 1 次 /api/problem-page → BFF 并行调 3 个服务
        RTT = max(3 次并行调用) ≈ 60ms,且前端逻辑简化

BFF 可以是网关的聚合 filter,也可以是独立薄层。注意分工:网关管「能不能进」(安全),BFF 管「怎么拼」(体验)。

步骤 4:Strangler Fig 渐进迁移(面试压轴题)

不要大爆炸重写。以「拆排行榜服务」为例:

第 1 步:新服务实现排行榜逻辑,数据双写(单体写 MySQL,同时投事件给新服务)
第 2 步:网关加灰度路由 —— /api/rank/** 10% 流量走新服务,比对两边响应
第 3 步:逐步 10% → 50% → 100%,期间新服务读模型从事件重建,随时可回退到单体
第 4 步:100% 稳定 2 周后,删除单体里的排行榜代码 —— 数据迁移完成

绞杀者的名字来源:新服务像藤蔓缠绕旧树(单体),逐枝替换,最后旧树枯死移除。每一步都可回退,是它与大爆炸重写的本质区别。

讲清楚要点

  1. 微服务的收益是组织和扩展,不是性能:拆分让团队独立发布、资源画像独立扩容;单纯追求性能,缓存+多实例通常更便宜。没有组织痛点和扩展痛点就不拆。
  2. 按业务能力拆,不按技术层次拆:拆成「读服务/写服务/定时任务服务」是错误示范,改一个需求要跨三个服务。正确切法是题目、判题、用户这种领域边界(DDD 的限界上下文)。
  3. 网关是安全与治理的收口:鉴权、限流、灰度、审计统一在网关,下游专注业务。但要防网关变成「上帝服务」——业务逻辑一律下沉,网关只做横切关注点。
  4. 每个服务独立数据(Database per Service):共享数据库的微服务是假微服务——表结构一改,全站一起发布。跨服务取数走 API 或事件投影,代价是跨服务一致性(第 3 课的幂等与补偿派上用场)。
  5. 可观测性必须先行:拆分后一次请求跨 3 个服务,没有 traceId 贯穿就是盲飞。先上链路追踪(Micrometer Tracing/SkyWalking)、统一日志、每个服务独立大盘,再开始迁移。

你可能会懵的地方

  • 拆多细合适? 看两个约束:一个服务一个团队(康威定律),一个服务一个数据域。经验信号是「这个服务能不能独立发布而不牵连别人」。宁粗勿细,拆错了再合并的成本远小于拆多了的协调成本。

  • 服务间数据一致性怎么保证? 放弃跨服务强一致事务(Seata 能做但复杂度高),用 Saga 模式:每步可补偿,失败逆序回滚。本站场景:提交服务写库+发事件,判题服务失败时发补偿事件回滚状态——衔接第 3 课幂等与第 6 课死信。

  • 服务发现是什么,K8s 里还需要吗? 服务解决「实例动态上下线后调用方怎么找到它」。Nacos/Consul 是注册中心方案;K8s 的 Service/DNS 天然提供了服务发现,此时应用内可以不再引入注册中心组件——基础设施给了就不要重复造。

  • 单体性能不够,拆微服务能救吗? 大概率不能。瓶颈分析(第 2 课)显示本站卡在 DB 和判题池,拆服务不增加 DB 容量也不增加判题机。先做缓存、读写分离、异步化,确认瓶颈是「互相拖累」而不是「资源不足」时才拆。

  • 网关和 Nginx 重复了? 流量大时两者并存分层:Nginx(或云 LB)在最外层做 TLS 终结与静态分流,网关在后面做业务路由与治理。小流量 Nginx 直接路由到各服务即可,网关是治理需求出现后(多团队、灰度、统一限流)才引入。

错法对照

错法问题正解
按技术层拆服务(读/写/任务)一个需求跨三个服务发布按业务能力/限界上下文拆
多个服务共享一个 MySQL表结构耦合,发布互相阻塞Database per Service + 事件同步
网关里写业务逻辑网关成上帝服务,谁都改不动它网关只做鉴权限流路由等横切职责
大爆炸重写替代渐进迁移半年新系统不上线,老系统停维护Strangler Fig 每步可回退
拆完才上链路追踪跨服务问题无从排查迁移第一天就贯穿 traceId

卡点急救

  1. 网关路由 404/503:查三处——服务是否注册到注册中心(lb:// 依赖服务名精确匹配)、路由 predicate 是否被前面的路由抢先匹配(顺序敏感)、网关到注册中心的网络。看网关日志的 RouteMatch 输出最快定位。
  2. 服务间调用偶发超时:Feign 默认超时可能过短或过长(长会拖死线程池),显式配置 connect/read 超时 + 重试次数;超时集中在某接口说明该服务慢,用链路追踪定位到具体方法。
  3. 双写期间新旧数据不一致:双写必有窗口,接受「新服务读模型最终一致」,用对账任务修数;关键校验场景(计分)以旧系统(单体 DB)为准,新系统只读影子——灰度期数据正确性 > 一致性完美。

面试常问

  • 单体到微服务,什么时候拆、按什么拆? 先给三条信号(扩展差异化、发布争抢、团队规模),强调「先拆部署再拆代码最后拆组织」的省钱路径;拆分依据是业务能力(限界上下文),配 Database per Service;最后补「没有这些信号就不拆,单体多实例+模块化是更优解」——能说出不拆的条件是加分项。

  • API Gateway 和 BFF 的区别? 网关是横切收口(鉴权/限流/路由/灰度),面向所有客户端;BFF 是体验层(聚合/裁剪/协议适配),面向特定前端(Web 一个、小程序一个)。可以都不要(小项目),可以网关+BFF(中大型)。用本站题目详情页聚合三服务的例子说明 BFF 价值。

  • 如何保证微服务拆分后的数据一致性? 三层作答:① 架构上接受最终一致,事件驱动(Kafka)同步;② 补偿上用 Saga,每步设计补偿动作,失败逆序执行;③ 兜底上消费端幂等 + 对账任务。强调「放弃分布式强一致事务不是妥协,是正确的工程判断」。

自测三问

  1. 本站三条拆分信号分别是什么?只有第一条时你先做什么?
  2. 网关、BFF、Nginx 三者职责如何划分?小流量时可以省掉谁?
  3. 写出 Strangler Fig 迁移排行榜服务的四个步骤,每步的回退手段是什么?

小结一句话

微服务是组织与扩展问题的工具而非信仰——先拆部署单元再拆服务,Gateway 管安全横切、BFF 管聚合体验,Strangler Fig 让每一步迁移可回退。

延伸

  • Spring Cloud Gateway 官方文档:路由、谓词、过滤器完整参考
  • Martin Fowler — Microservices 与 StranglerFigApplication:绞杀者模式出处
  • Sam Newman《Monolith to Microservices》:渐进迁移方法论
  • 下一课:云设计模式——Bulkhead 舱壁、Valet Key 代客密钥、Claim Check 引用票据,一套可复用的架构套路。
加载本节练习…
← 上一课数据架构下一课 →云设计模式