🚪 设计一个API网关:守门员的智慧!

34 阅读9分钟

📖 开场:小区门卫

想象小区的门卫大爷 👴:

没有门卫(混乱)

外人:随便进 🚶
    ↓
小偷:也能进 🥷
    ↓
推销员:天天骚扰 📞
    ↓
快递员:不知道送哪栋 📦

结果:
- 不安全 ❌
- 很混乱 ❌
- 效率低 ❌

有门卫(有序)

外人来访:
    ↓
门卫:你找谁?
    ↓
外人:找1栋的张三
    ↓
门卫:
1. 检查身份证 🪪
2. 登记 📝
3. 打电话确认 ☎️
4. 指路:1栋在左边 👈
5. 放行 ✅

结果:
- 安全 ✅
- 有序 ✅
- 高效 ✅

这就是API网关:系统的守门员!


🤔 为什么需要API网关?

问题1:客户端直接调用微服务(混乱)

没有网关:
客户端 → 订单服务:http://order-service:8080/api/order
客户端 → 用户服务:http://user-service:8081/api/user
客户端 → 支付服务:http://pay-service:8082/api/pay

问题:
1. 客户端需要知道所有服务的地址 ❌
2. 服务地址变了,客户端要改代码 ❌
3. 鉴权逻辑每个服务都要写 ❌
4. 跨域问题 ❌
5. 协议不统一(HTTP/gRPC)❌

问题2:有网关(统一入口)

有网关:
客户端 → 网关:http://gateway:8000/api/*
    ↓
网关:
1. 鉴权 🔐
2. 限流 🚦
3. 路由 🧭
4. 协议转换 🔄
5. 监控 📊
    ↓
路由到对应的服务 ✅

优点:
- 统一入口 ✅
- 集中鉴权 ✅
- 简化客户端 ✅

🎯 核心功能

功能1:路由转发 🧭

路由规则:
/api/order/*  → 订单服务
/api/user/*   → 用户服务
/api/pay/*    → 支付服务

例子:
客户端请求:GET http://gateway:8000/api/order/123
    ↓
网关:匹配路由规则 /api/order/*
    ↓
转发到:http://order-service:8080/api/order/123

功能2:鉴权 🔐

鉴权流程:
客户端请求:
    ↓
网关:检查Token
    ↓
Token有效 → 放行 ✅
Token无效 → 返回401 ❌

功能3:限流 🚦

限流规则:
- 用户级别:每个用户每秒100次
- 接口级别:登录接口每秒1000次
- IP级别:同一IP每秒200次

超过限制:
    ↓
返回429(Too Many Requests)

功能4:熔断降级 💔

熔断流程:
订单服务:连续10次超时
    ↓
网关:开启熔断 ⚡
    ↓
后续请求:直接返回降级响应
    ↓
30秒后:尝试恢复

🎯 核心设计

设计1:Spring Cloud Gateway实现 ⭐⭐⭐

引入依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

配置路由

spring:
  cloud:
    gateway:
      routes:
        # ⭐ 订单服务路由
        - id: order-service
          uri: lb://order-service  # lb表示负载均衡
          predicates:
            - Path=/api/order/**
          filters:
            - StripPrefix=1  # 去掉/api前缀
        
        # ⭐ 用户服务路由
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/api/user/**
          filters:
            - StripPrefix=1
        
        # ⭐ 支付服务路由
        - id: pay-service
          uri: lb://pay-service
          predicates:
            - Path=/api/pay/**
          filters:
            - StripPrefix=1

设计2:鉴权过滤器 🔐

@Component
public class AuthFilter implements GlobalFilter, Ordered {
    
    @Autowired
    private JwtService jwtService;
    
    // 白名单(不需要鉴权的接口)
    private static final List<String> WHITE_LIST = Arrays.asList(
        "/api/user/login",
        "/api/user/register"
    );
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getURI().getPath();
        
        // ⭐ 1. 白名单,直接放行
        if (isWhiteList(path)) {
            return chain.filter(exchange);
        }
        
        // ⭐ 2. 获取Token
        String token = request.getHeaders().getFirst("Authorization");
        
        if (token == null || !token.startsWith("Bearer ")) {
            return unauthorized(exchange, "Token不能为空");
        }
        
        token = token.substring(7);  // 去掉"Bearer "前缀
        
        // ⭐ 3. 验证Token
        try {
            Long userId = jwtService.parseToken(token);
            
            // ⭐ 4. 将userId放入请求头,传递给下游服务
            ServerHttpRequest modifiedRequest = request.mutate()
                .header("X-User-Id", String.valueOf(userId))
                .build();
            
            ServerWebExchange modifiedExchange = exchange.mutate()
                .request(modifiedRequest)
                .build();
            
            return chain.filter(modifiedExchange);
            
        } catch (Exception e) {
            return unauthorized(exchange, "Token无效");
        }
    }
    
    @Override
    public int getOrder() {
        return -100;  // 优先级最高
    }
    
    /**
     * 检查是否在白名单
     */
    private boolean isWhiteList(String path) {
        return WHITE_LIST.stream().anyMatch(path::startsWith);
    }
    
    /**
     * 返回401未授权
     */
    private Mono<Void> unauthorized(ServerWebExchange exchange, String message) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        
        Result<Void> result = Result.fail(401, message);
        DataBuffer buffer = response.bufferFactory()
            .wrap(JSON.toJSONBytes(result));
        
        return response.writeWith(Mono.just(buffer));
    }
}

设计3:限流过滤器 🚦

Redis限流(令牌桶)

@Component
public class RateLimitFilter implements GlobalFilter, Ordered {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    private static final String RATE_LIMIT_KEY = "rate_limit:";
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getURI().getPath();
        
        // 获取用户ID(从上游的鉴权过滤器传递过来)
        String userId = request.getHeaders().getFirst("X-User-Id");
        
        if (userId == null) {
            // 未登录用户,按IP限流
            String ip = getClientIp(request);
            return rateLimitByIp(exchange, chain, ip);
        } else {
            // 已登录用户,按用户ID限流
            return rateLimitByUser(exchange, chain, userId);
        }
    }
    
    /**
     * ⭐ 按用户ID限流(每秒100次)
     */
    private Mono<Void> rateLimitByUser(ServerWebExchange exchange, 
                                       GatewayFilterChain chain, 
                                       String userId) {
        String key = RATE_LIMIT_KEY + "user:" + userId;
        return rateLimit(exchange, chain, key, 100);
    }
    
    /**
     * ⭐ 按IP限流(每秒200次)
     */
    private Mono<Void> rateLimitByIp(ServerWebExchange exchange, 
                                     GatewayFilterChain chain, 
                                     String ip) {
        String key = RATE_LIMIT_KEY + "ip:" + ip;
        return rateLimit(exchange, chain, key, 200);
    }
    
    /**
     * ⭐ 限流(Lua脚本)
     */
    private Mono<Void> rateLimit(ServerWebExchange exchange, 
                                 GatewayFilterChain chain, 
                                 String key, 
                                 int limit) {
        // Lua脚本(令牌桶算法)
        String luaScript = 
            "local key = KEYS[1]\n" +
            "local limit = tonumber(ARGV[1])\n" +
            "local current = redis.call('incr', key)\n" +
            "if current == 1 then\n" +
            "    redis.call('expire', key, 1)\n" +
            "end\n" +
            "if current <= limit then\n" +
            "    return 1\n" +
            "else\n" +
            "    return 0\n" +
            "end";
        
        // 执行Lua脚本
        Long result = redisTemplate.execute(
            new DefaultRedisScript<>(luaScript, Long.class),
            Collections.singletonList(key),
            String.valueOf(limit)
        );
        
        if (result != null && result == 1) {
            // 未超限,放行
            return chain.filter(exchange);
        } else {
            // 超限,拒绝
            return tooManyRequests(exchange);
        }
    }
    
    /**
     * 返回429(Too Many Requests)
     */
    private Mono<Void> tooManyRequests(ServerWebExchange exchange) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        
        Result<Void> result = Result.fail(429, "请求太频繁,请稍后再试");
        DataBuffer buffer = response.bufferFactory()
            .wrap(JSON.toJSONBytes(result));
        
        return response.writeWith(Mono.just(buffer));
    }
    
    /**
     * 获取客户端IP
     */
    private String getClientIp(ServerHttpRequest request) {
        HttpHeaders headers = request.getHeaders();
        
        String ip = headers.getFirst("X-Forwarded-For");
        if (ip != null && !ip.isEmpty()) {
            return ip.split(",")[0];
        }
        
        ip = headers.getFirst("X-Real-IP");
        if (ip != null && !ip.isEmpty()) {
            return ip;
        }
        
        return request.getRemoteAddress().getAddress().getHostAddress();
    }
    
    @Override
    public int getOrder() {
        return -90;  // 在鉴权过滤器之后
    }
}

设计4:熔断降级(Resilience4j)💔

引入依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>

配置熔断

spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/order/**
          filters:
            - StripPrefix=1
            # ⭐ 配置熔断
            - name: CircuitBreaker
              args:
                name: orderServiceCircuitBreaker
                fallbackUri: forward:/fallback/order

降级处理器

@RestController
@RequestMapping("/fallback")
public class FallbackController {
    
    /**
     * ⭐ 订单服务降级响应
     */
    @RequestMapping("/order")
    public Result<Void> orderFallback() {
        return Result.fail("订单服务暂时不可用,请稍后再试");
    }
    
    /**
     * 用户服务降级响应
     */
    @RequestMapping("/user")
    public Result<Void> userFallback() {
        return Result.fail("用户服务暂时不可用,请稍后再试");
    }
}

熔断配置

resilience4j:
  circuitbreaker:
    instances:
      orderServiceCircuitBreaker:
        # ⭐ 熔断配置
        failure-rate-threshold: 50  # 失败率阈值(50%)
        minimum-number-of-calls: 10  # 最少调用次数
        sliding-window-size: 20  # 滑动窗口大小
        wait-duration-in-open-state: 30s  # 熔断持续时间
        permitted-number-of-calls-in-half-open-state: 5  # 半开状态允许调用次数

设计5:日志和监控 📊

@Component
@Slf4j
public class LogFilter implements GlobalFilter, Ordered {
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        
        long startTime = System.currentTimeMillis();
        String requestId = UUID.randomUUID().toString();
        
        // ⭐ 记录请求日志
        log.info("⭐ [{}] {} {} 开始", 
            requestId, 
            request.getMethod(), 
            request.getURI().getPath());
        
        return chain.filter(exchange).then(Mono.fromRunnable(() -> {
            long endTime = System.currentTimeMillis();
            long duration = endTime - startTime;
            
            ServerHttpResponse response = exchange.getResponse();
            
            // ⭐ 记录响应日志
            log.info("⭐ [{}] {} {} 完成,耗时:{}ms,状态码:{}", 
                requestId,
                request.getMethod(), 
                request.getURI().getPath(),
                duration,
                response.getStatusCode());
        }));
    }
    
    @Override
    public int getOrder() {
        return -200;  // 最先执行
    }
}

🎓 面试题速答

Q1: API网关有什么作用?

A: 六大作用

  1. 统一入口:客户端只需知道网关地址
  2. 鉴权:集中鉴权,不需要每个服务都写
  3. 限流:保护后端服务
  4. 熔断降级:服务故障时快速失败
  5. 路由转发:根据路径转发到对应服务
  6. 协议转换:HTTP → gRPC

Q2: 如何实现鉴权?

A: GlobalFilter + JWT

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    // 1. 获取Token
    String token = request.getHeaders().getFirst("Authorization");
    
    // 2. 验证Token
    Long userId = jwtService.parseToken(token);
    
    // 3. 将userId放入请求头
    request.mutate().header("X-User-Id", String.valueOf(userId));
    
    // 4. 放行
    return chain.filter(exchange);
}

Q3: 如何实现限流?

A: Redis + Lua脚本

-- Lua脚本(令牌桶)
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local current = redis.call('incr', key)
if current == 1 then
    redis.call('expire', key, 1)
end
if current <= limit then
    return 1  -- 未超限
else
    return 0  -- 超限
end

优点:原子操作,高性能


Q4: 熔断降级如何实现?

A: Resilience4j

resilience4j:
  circuitbreaker:
    instances:
      orderService:
        failure-rate-threshold: 50  # 失败率50%
        wait-duration-in-open-state: 30s  # 熔断30秒

流程

  1. 失败率 > 50% → 开启熔断
  2. 30秒内直接返回降级响应
  3. 30秒后尝试恢复

Q5: Spring Cloud Gateway vs Zuul?

A: Gateway更好⭐:

特性GatewayZuul 1.x
基于WebFlux(异步)Servlet(同步)
性能高 ✅低 ❌
吞吐量高 ✅低 ❌
功能丰富 ✅较少 ❌

推荐:Spring Cloud Gateway


Q6: 网关如何保证高可用?

A: 集群部署 + 负载均衡

             Nginx
              ↓
    ┌─────────┼─────────┐
    ↓         ↓         ↓
Gateway1  Gateway2  Gateway3
    ↓         ↓         ↓
       后端服务集群

优点

  • 网关宕机自动切换
  • 负载均衡

🎬 总结

       API网关核心功能

┌────────────────────────────────────┐
│ 1. 路由转发 🧭                     │
│    - 统一入口                      │
│    - 路径匹配                      │
└────────────────────────────────────┘

┌────────────────────────────────────┐
│ 2. 鉴权 🔐                         │
│    - JWT验证                       │
│    - 白名单                        │
└────────────────────────────────────┘

┌────────────────────────────────────┐
│ 3. 限流 🚦                         │
│    - Redis + Lua                   │
│    - 按用户/IP限流                 │
└────────────────────────────────────┘

┌────────────────────────────────────┐
│ 4. 熔断降级 💔                      │
│    - Resilience4j                  │
│    - 快速失败                      │
└────────────────────────────────────┘

┌────────────────────────────────────┐
│ 5. 日志监控 📊                      │
│    - 请求日志                      │
│    - 性能监控                      │
└────────────────────────────────────┘

🎉 恭喜你!

你已经完全掌握了API网关的设计!🎊

核心要点

  1. 路由转发:统一入口,路径匹配
  2. 鉴权:JWT验证,白名单
  3. 限流:Redis + Lua,令牌桶
  4. 熔断降级:Resilience4j,快速失败
  5. 日志监控:记录请求日志,性能监控

下次面试,这样回答

"API网关是微服务架构的统一入口,承担路由转发、鉴权、限流、熔断降级等职责。我们项目使用Spring Cloud Gateway实现,基于WebFlux异步非阻塞,性能比Zuul更好。

鉴权通过GlobalFilter实现。获取请求头中的Authorization字段,验证JWT Token,解析出用户ID后放入X-User-Id请求头传递给下游服务。白名单接口如登录注册直接放行。验证失败返回401未授权。

限流使用Redis + Lua脚本实现令牌桶算法。已登录用户按用户ID限流每秒100次,未登录用户按IP限流每秒200次。Lua脚本先incr计数器,第一次设置1秒过期,超过限制返回429 Too Many Requests。Lua脚本保证原子性,避免并发问题。

熔断降级集成Resilience4j。配置失败率阈值50%,滑动窗口20次调用,10次最小调用数。失败率超过50%时开启熔断,30秒内请求直接走降级逻辑返回友好提示。30秒后进入半开状态,允许5次调用测试服务是否恢复。

日志监控方面,LogFilter记录每个请求的开始时间、结束时间、耗时、状态码。生成唯一requestId用于链路追踪。集成Prometheus和Grafana监控网关的QPS、响应时间、错误率等指标。

高可用方面,网关集群部署3个节点,前端Nginx负载均衡。网关本身是无状态的,宕机后自动切换。限流数据存储在Redis,熔断状态存储在本地内存并通过事件广播同步到其他节点。"

面试官:👍 "很好!你对API网关的设计理解很深刻!"


本文完 🎬

上一篇: 215-设计一个评论系统.md
下一篇: 217-设计一个分布式爬虫系统.md

作者注:写完这篇,我觉得自己可以当门卫了!🚪
如果这篇文章对你有帮助,请给我一个Star⭐!