大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
架构模式与术语
这一课你将能做到
- 看懂一张典型微服务拓扑图,准确说出注册中心、网关、配置中心、熔断器各自的职责
- 用标准术语完整描述「用户提交一道题」从浏览器到判题完成的全链路
- 对任意一个业务交互,判断它该用同步调用还是异步消息,并给出理由
为什么要学
微服务的文档和面试里满屏 Gateway、Registry、Config Server、Circuit Breaker——术语对不齐,就没法和同事对齐需求,也没法答好架构题。更重要的是:拆微服务的第一步不是写代码,而是画架构图、定边界,这一课就是给你画图和说话的词汇表。
结合本站场景:假设把 CodeForge 拆成用户服务(user-service)、题目服务(problem-service)、判题服务(judge-service)。「提交一道题」会横跨三个服务,每种基础组件在哪个环节出场,学完本课你就能对着图讲清楚。
先建立图像
┌────────────────────┐
浏览器 / App ─────────► │ API Gateway:8080 │ 统一入口:路由/鉴权/限流
└─────────┬──────────┘
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
user-service:8081 problem-service:8082 judge-service:8083
│ │ │
▼ ▼ ▼
user_db problem_db judge_db + Redis
【后台基础设施】
Eureka:8761 注册中心 —— 各服务启动上报地址,调用方按名字查
Config Server:8888 —— 配置集中存放,各服务启动拉取
MQ(RabbitMQ/Kafka)—— 异步事件通道,如「判题结果已出」
一次「提交代码」的完整链路:
1. 浏览器 POST /api/submissions ──► Gateway 校验 JWT,路由到 judge-service
2. judge-service 落库「提交记录:PENDING」
3. judge-service 发事件「需要评测 submission#1024」到 MQ,立刻返回「已接收」
4. 评测 Worker 消费事件 → 同步调 problem-service 拿测试用例
5. 编译运行 → 写回结果 → 前端轮询/WebSocket 拿到判题结果
注意第 4 步是同步(必须拿到用例才能跑),第 3 步是异步(用户不必等判完)。
跟着做
步骤 1:先定服务间契约(DTO)
微服务第一原则:接口契约先行。在 judge-service 里定义它需要的数据结构:
package com.example.judge.dto;
/** 判题所需的题目信息——从 problem-service 获取 */
public record ProblemBrief(
Long id,
String title,
int timeLimitMs,
int memoryLimitMb,
int caseCount
) {}
用 Java 17 的 record 做不可变 DTO,Jackson 2.15+ 原生支持,不用手写 getter/setter。
步骤 2:同步调用的直觉代码
第 4 步「拿测试用例」是同步调用。Spring 6 自带 RestClient,先手写一次理解本质(后面课程会换成 Feign):
package com.example.judge.remote;
import com.example.judge.dto.ProblemBrief;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
@Component
public class ProblemClient {
private final RestClient rest = RestClient.builder()
.baseUrl("http://localhost:8082") // 问题:地址写死了!
.build();
public ProblemBrief getProblem(Long id) {
return rest.get()
.uri("/problems/{id}/brief", id)
.retrieve()
.body(ProblemBrief.class);
}
}
能跑,但暴露两个大问题:地址硬编码(扩容即失效)、没有超时与容错。这正是后面「注册中心 + Feign + 熔断」要逐个解决的。
步骤 3:异步消息的直觉代码
第 3 步「通知评测」先用 Spring 事件模拟解耦(生产换 MQ,代码结构不变):
package com.example.judge.event;
/** 领域事件:一道提交等待评测 */
public record SubmissionSubmittedEvent(Long submissionId, Long problemId, String language) {}
package com.example.judge.service;
import com.example.judge.event.SubmissionSubmittedEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
@Service
public class SubmissionService {
private final ApplicationEventPublisher publisher;
public SubmissionService(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public Long submit(Long problemId, String language, String code) {
Long submissionId = saveAsPending(problemId, language, code);
// 发布事件:谁关心谁监听,本服务不再关心后续流程
publisher.publishEvent(new SubmissionSubmittedEvent(submissionId, problemId, language));
return submissionId;
}
private Long saveAsPending(Long problemId, String language, String code) {
// 省略落库:INSERT INTO submissions(status='PENDING', ...) RETURNING id
return 1024L;
}
}
关键收益:提交接口只负责「受理」,判题快慢不影响用户拿到响应。换成 RabbitMQ 后,把 publisher.publishEvent(...) 换成 rabbitTemplate.convertAndSend(...),监听方挪到独立 Worker 进程,其余代码不动。
步骤 4:画出这条链路的时序图
拿纸画出:浏览器 → Gateway → judge-service → MQ → Worker → problem-service,并标注每条箭头是「同步 HTTP」「异步消息」还是「本地调用」。面试白板题常要求现场画架构图,画得出来才算真懂。
讲清楚要点
- 服务实例:一个运行中的进程,如
problem-service:8082。同一服务可跑多个实例,这就是「水平扩容」。 - 服务注册 / 发现:实例启动时向注册中心上报「我叫 problem-service,地址 10.0.0.5:8082」;调用方按名字查列表——本质是电话簿。
- API 网关:对外唯一入口。路由、鉴权、限流、跨域都在这层做,业务服务不重复造轮子。
- 配置中心:配置从代码剥离、集中管理,改配置不用重新打包,还能动态刷新。
- 同步调用(HTTP/RPC):发起方阻塞等结果,适合「下一步必须依赖返回值」的场景(拿测试用例)。
- 异步消息(MQ):发完就走,适合「结果晚点无所谓、需要削峰」的场景(判题、发通知)。
- 数据私有:每个服务用自己的库,别的服务想看数据只能调 API——这是服务自治的底线。
- 熔断降级:下游故障时快速失败或返回兜底数据,防止故障沿调用链扩散成雪崩。
你可能会懵的地方
-
API 网关和 Nginx 不是一回事吗? Nginx 是 L7 反向代理,擅长静态资源、TLS 终止、负载均衡;Gateway 是 Java 进程内的应用层网关,能用代码写鉴权、限流、灰度路由,并与注册中心/Spring Security 无缝集成。生产常见「Nginx 在前收 TLS,Gateway 在后做路由」。
-
注册中心和 DNS 有什么区别? DNS 也是「名字→地址」,但记录静态、变更传播慢(TTL 缓存);注册中心是动态的:实例秒级上下线、带健康检查、可挂元数据(版本号、机房)。K8s 的 Service DNS 相当于内建了动态注册。
-
什么时候该用 MQ,什么时候该同步? 判断标准:调用方是否需要立刻拿到结果。判题要几分钟 → 异步;渲染页面要题目详情 → 同步。乱用同步会把长任务挂死 HTTP 线程;乱用异步会让简单查询变成事件溯源噩梦。
-
BFF(Backend for Frontend)是什么? 面向特定前端聚合后端数据的中间层。例如给 Web 端一个 web-bff,一次请求聚合「用户信息 + 今日推荐题目」,避免前端连发 5 个请求。小项目可先由网关聚合,别急着加层。
-
微服务还要不要 Redis? 要,而且更分散:判题结果缓存放 judge-service 侧,登录态放 user-service 侧。原则是缓存归属服务私有,禁止跨服务直连别人的 Redis。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 把「用户」拆成用户读服务 + 用户写服务再加 CRUD 服务 | 粒度过细,网络开销爆炸 | 先按业务域粗拆,出现真实瓶颈再细化 |
| 服务 A 直连服务 B 的数据库查数据 | 数据耦合,B 改表 A 崩 | B 提供 API,A 通过调用/事件获取 |
| 判题这类长任务用同步 HTTP 等结果 | 线程占满,超时大面积失败 | 受理后立即返回,MQ 通知 Worker |
| 所有服务共用一个 Redis/一台 MySQL | 基础设施耦合,故障连带 | 缓存/存储随服务走,故障域隔离 |
| 把「注册中心」说成「负载均衡器」 | 沟通歧义 | 注册中心管地址列表,负载均衡管选哪个实例 |
卡点急救
- Spring 事件两个 Bean 互相收不到:
@EventListener的监听者必须和发布者在同一个 Spring 容器(同进程);跨进程直接上 MQ,不要硬绕。 - RestClient 调用报 404:占位符按顺序填充,
uri("/problems/{id}/brief", id);确认目标服务真实暴露的路径与端口。 - 以为发布事件就是异步,结果接口还是慢:
ApplicationEventPublisher默认同步发布(同线程执行监听器)。要真异步需在监听方法加@Async并开启@EnableAsync,或直接换 MQ。
面试常问
-
同步调用和异步消息怎么选型? 看三点:实时性(要结果→同步)、削峰需求(大流量→异步)、最终一致性容忍度(可容忍→异步)。举「判题异步、查题目同步」并说明线程资源角度的原因,就是满分答案。
-
微服务里一次请求变慢,你怎么定位? 先看网关日志拿 trace-id → 链路追踪(Zipkin/Jaeger)看每一跳耗时 → 定位是哪次跨服务调用或哪条 SQL 慢 → 结合该服务的指标(P99、连接池、GC)确认根因。这正是可观测性一课的内容。
-
什么是服务雪崩?怎么防? 下游变慢 → 调用方线程被占满 → 线程池耗尽 → 上游也超时 → 层层传导全站瘫痪。防线组合:调用方设超时 → 熔断快速失败 → 降级兜底 → 必要时限流保护自身。
自测三问
- 注册中心、网关、配置中心各解决什么问题?各用一个比喻。
- 「判题」为什么该走异步而不是同步 HTTP?
- 服务 B 想读服务 A 的用户表,正确做法是什么?
小结一句话
微服务架构 = 按业务拆服务 + 注册中心寻址 + 网关统一入口 + 配置中心管配置 + 同步/异步混合通信 + 熔断兜底;术语对齐后,后面每课认领一个组件。
延伸
- Spring Cloud 官方参考文档(总览)
- 下一课:服务注册与 Eureka——让服务按名字互相找到。