架构与集成
这一课你将能做到
- 设计 API 网关层的核心能力:路由、认证、限流、日志、监控
- 实现 BFF(Backend for Frontend)模式,为不同客户端定制 API
- 在微服务架构中处理服务间通信、熔断、降级、重试
为什么要学
单体应用时代,API 设计就是 Controller 设计。微服务时代,API 要经过网关、跨服务调用、多团队协作。不懂架构,你设计的 API 在分布式环境里会处处碰壁:超时、重试风暴、循环依赖、数据不一致。
先建立图像
典型微服务 API 架构:
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Web 端 │ │ App 端 │ │ 第三方 │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
└───────────────┼───────────────┘
▼
┌─────────────────┐
│ API 网关 │
│ (认证/限流/路由) │
└────────┬────────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ BFF-Web │ │ BFF-App │ │ BFF-Open│
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
└────────────┼────────────┘
▼
┌────────────────────────┐
│ 微服务集群 │
│ ┌────┐ ┌────┐ ┌────┐ │
│ │课程 │ │用户 │ │订单 │ │
│ │服务 │ │服务 │ │服务 │ │
│ └──┬─┘ └──┬─┘ └──┬─┘ │
│ └──────┼──────┘ │
│ 服务间调用 │
│ (gRPC/REST/MQ) │
└────────────────────────┘
跟着做
步骤 1:API 网关核心能力
// 以 Spring Cloud Gateway 为例
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
// 课程服务路由
.route("lesson-service", r -> r
.path("/api/lessons/**")
.filters(f -> f
.stripPrefix(1) // 去掉 /api 前缀
.addRequestHeader("X-Request-Source", "gateway")
.circuitBreaker(c -> c
.setName("lessonCB")
.setFallbackUri("forward:/fallback/lessons"))
.retry(3)
)
.uri("lb://lesson-service")) // 负载均衡到课程服务
// 用户服务路由
.route("user-service", r -> r
.path("/api/users/**")
.filters(f -> f.stripPrefix(1))
.uri("lb://user-service"))
.build();
}
}
// 网关统一认证过滤器
@Component
public class AuthGatewayFilterFactory extends AbstractGatewayFilterFactory<AuthGatewayFilterFactory.Config> {
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
if (token == null || !tokenProvider.validateToken(token.replace("Bearer ", ""))) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
// 把用户信息传给下游服务
Long userId = tokenProvider.getUserId(token);
ServerHttpRequest request = exchange.getRequest().mutate()
.header("X-User-Id", userId.toString())
.build();
return chain.filter(exchange.mutate().request(request).build());
};
}
}
步骤 2:BFF 模式
Web 端需要「课程列表 + 分页 + 讲师信息」,App 端只需要「课程列表 + 封面图」。
// BFF-Web:为 Web 端定制
@RestController
@RequestMapping("/bff/web/lessons")
public class WebLessonBffController {
@GetMapping
public ApiResponse<Page<WebLessonCard>> list(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "12") int size) {
// 调用课程服务
Page<Lesson> lessons = lessonServiceClient.list(page, size);
// 批量调用讲师服务,组装数据
List<Long> instructorIds = lessons.getContent().stream()
.map(Lesson::getInstructorId).collect(Collectors.toList());
Map<Long, Instructor> instructors = instructorServiceClient.batchGet(instructorIds);
// 组装 Web 端需要的完整卡片
Page<WebLessonCard> result = lessons.map(lesson ->
WebLessonCard.from(lesson, instructors.get(lesson.getInstructorId())));
return ApiResponse.success(result);
}
}
// BFF-App:为 App 端定制(更轻量)
@RestController
@RequestMapping("/bff/app/lessons")
public class AppLessonBffController {
@GetMapping
public ApiResponse<List<AppLessonItem>> list() {
// 只返回必要字段,减少传输量
return ApiResponse.success(lessonServiceClient.listLite());
}
}
BFF 的价值:前端不直接调微服务,BFF 做「数据聚合 + 裁剪」。Web 和 App 各自有 BFF,互不影响。
步骤 3:服务间通信——REST vs gRPC vs 消息队列
// 方案一:REST(同步,简单)
@Service
public class LessonServiceClient {
private final RestTemplate restTemplate;
public Lesson getLesson(Long id) {
return restTemplate.getForObject(
"http://lesson-service/api/lessons/" + id, Lesson.class);
}
}
// 方案二:gRPC(同步,高性能)
@GrpcClient("lesson-service")
private LessonServiceGrpc.LessonServiceBlockingStub lessonServiceStub;
public Lesson getLesson(Long id) {
LessonResponse response = lessonServiceStub.getLesson(
LessonRequest.newBuilder().setId(id).build());
return Lesson.from(response);
}
// 方案三:消息队列(异步,解耦)
@Service
public class LessonEventPublisher {
private final KafkaTemplate<String, Object> kafkaTemplate;
public void publishLessonCreated(Lesson lesson) {
kafkaTemplate.send("lesson-events", LessonCreatedEvent.from(lesson));
}
}
@KafkaListener(topics = "lesson-events")
public void handleLessonCreated(LessonCreatedEvent event) {
// 搜索服务更新索引、统计服务更新计数
}
- 查询、需要实时响应 → REST/gRPC
- 写操作、可以异步 → 消息队列
- 高频内部调用 → gRPC
- 跨团队、需要文档 → REST
步骤 4:熔断与降级
// Resilience4j 熔断器
@CircuitBreaker(name = "lessonService", fallbackMethod = "getLessonFallback")
@Retry(name = "lessonService")
@Bulkhead(name = "lessonService")
public Lesson getLesson(Long id) {
return lessonServiceClient.getLesson(id);
}
// 降级方法:返回缓存或默认值
public Lesson getLessonFallback(Long id, Exception e) {
log.warn("课程服务降级,id={}", id, e);
return lessonCache.get(id)
.orElse(Lesson.builder()
.id(id)
.title("课程加载中")
.build());
}
# application.yml
resilience4j:
circuitbreaker:
instances:
lessonService:
slidingWindowSize: 10
failureRateThreshold: 50
waitDurationInOpenState: 10s
permittedNumberOfCallsInHalfOpenState: 3
步骤 5:分布式追踪
// Sleuth + Zipkin 自动埋点
// 每个请求生成 traceId,贯穿所有微服务
// 日志输出示例:
// [lesson-service, traceId=abc123, spanId=def456] 查询课程 id=123
// [user-service, traceId=abc123, spanId=ghi789] 查询用户 id=456
// [order-service, traceId=abc123, spanId=jkl012] 创建订单
讲清楚要点
- API 网关是微服务的门面:统一入口、统一认证、统一限流、统一监控。没有网关,每个服务都要重复造轮子。
- BFF 是前端的最佳拍档:不同客户端(Web、App、小程序)需求不同,BFF 为每类客户端定制 API,避免「一个接口满足所有端」的妥协。
- 同步调用 vs 异步消息:同步简单但耦合,异步解耦但复杂。读多写少用同步,写多读少用异步,最终一致性场景用消息队列。
- 熔断是自我保护:下游服务挂了,熔断器快速失败,避免线程池打满。降级是用户体验的底线:返回缓存、默认值、或友好提示。
- 分布式追踪是排障利器:一个请求跨 5 个服务,没有 traceId 就像盲人摸象。Sleuth + Zipkin 或 SkyWalking 是标配。
你可能会懵的地方
-
API 网关和 Nginx 有什么区别?
Nginx 是「流量网关」,做负载均衡、SSL 终止、静态资源;API 网关是「业务网关」,做认证、限流、协议转换、聚合。通常 Nginx 在最外层,API 网关在 Nginx 后面。
-
BFF 和 API 网关有什么区别?
BFF 是「为特定前端定制数据」,API 网关是「为所有请求提供通用能力」。BFF 可以放在网关后面,也可以独立部署。复杂场景两者并存。
-
微服务之间用 REST 还是 gRPC?
跨团队、需要文档 → REST;团队内部、高频调用 → gRPC。没有绝对,很多团队混用:对外 REST,对内 gRPC。
-
消息队列选型:Kafka vs RabbitMQ?
Kafka:高吞吐、持久化、流处理,适合日志、事件流;RabbitMQ:低延迟、灵活路由、任务队列,适合业务消息。大数据场景 Kafka,业务解耦 RabbitMQ。
-
分布式事务怎么做?
尽量避免。如果必须:Saga(补偿模式)、TCC(Try-Confirm-Cancel)、Seata。能用最终一致性就不用强一致性。
错法对照
| 错法 | 问题 | 正解 |
|---|
| 所有微服务直接暴露给前端 | 前端要维护多个地址,认证分散 | API 网关统一入口 |
| 一个通用接口给所有端用 | Web 端数据冗余,App 端流量浪费 | BFF 按端定制 |
| 服务间循环依赖 | A 调 B,B 调 A,部署死锁 | 梳理依赖,引入消息队列解耦 |
| 无熔断直接调用下游 | 下游挂了,上游线程池打满 | 熔断器 + 降级策略 |
| 重试无幂等保障 | 网络超时重试,重复创建数据 | 幂等设计 + 重试次数限制 |
卡点急救
- 网关路由不生效:检查
lb:// 服务名是否注册到注册中心(Nacos/Eureka),以及路由规则优先级。
- 服务间调用超时:设置合理的超时时间(连接 3s,读取 10s),配置重试但不超过 3 次,避免重试风暴。
- 消息队列积压:消费者能力不足。增加消费者实例、批量消费、异步处理。监控 Lag 指标。
面试常问
-
API 网关的核心功能?
路由转发、负载均衡、认证鉴权、限流熔断、日志监控、协议转换、缓存。能讲出每个功能的作用和实现原理。
-
BFF 模式解决什么问题?
不同客户端(Web/App/小程序)对 API 的需求不同,通用 API 会导致数据冗余或不足。BFF 为每类客户端定制聚合接口,前端体验更好,后端微服务保持纯净。
-
微服务之间调用失败怎么办?
超时设置、重试(幂等前提下)、熔断(防止雪崩)、降级(返回兜底数据)、异步消息(最终一致性)。能讲出完整的服务治理链路。
自测三问
- 如果课程服务和用户服务都挂了,订单服务怎么保证「创建订单」接口可用?
- 设计一个「App 端首页」的 BFF 接口,需要聚合哪些数据?
- 为什么消息队列能削峰填谷?什么场景不适合用消息队列?
小结一句话
架构与集成的核心是「解耦与治理」:API 网关统一入口,BFF 定制前端体验,消息队列异步解耦,熔断降级保障稳定。
延伸