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

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

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

学习

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

更多

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

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
API 设计实战入门/课时/可观测与合规
RawBlame
课时目录12 节

API 设计实战入门

  1. 01API 基础
  2. 02HTTP 协议层
  3. 03REST 资源设计
  4. 04错误处理
  5. 05认证
  6. 06授权模型
  7. 07API 安全
  8. 08API 风格
  9. 09架构与集成
  10. 10测试
  11. 11可观测与合规
  12. 12文档与工具

可观测与合规

限流、监控与 GDPR。

30 分钟

可观测与合规

这一课你将能做到

  1. 为 API 接入完整的可观测体系:指标(Metrics)、日志(Logging)、链路追踪(Tracing)
  2. 实现多维度限流策略:按用户、按 IP、按接口、按租户
  3. 理解 GDPR 等合规要求,在 API 设计中落实数据保护

为什么要学

API 上线只是开始。流量突增时系统会不会崩?某个接口突然变慢怎么定位?用户数据被泄露怎么追溯?可观测让你「看见」系统运行状态,合规让你「合法」处理用户数据。两者都是生产环境的生死线。

先建立图像

可观测三支柱:

┌─────────────┐  ┌─────────────┐  ┌─────────────┐
│   Metrics   │  │   Logging   │  │   Tracing   │
│   指标       │  │   日志       │  │   链路追踪   │
├─────────────┤  ├─────────────┤  ├─────────────┤
│ QPS、延迟    │  │ 错误日志、    │  │ 请求跨服务   │
│ 错误率、CPU  │  │ 业务日志、    │  │ 的完整路径   │
│ 内存、磁盘   │  │ 审计日志     │  │ 和耗时分布   │
└──────┬──────┘  └──────┬──────┘  └──────┬──────┘
       │                │                │
       └────────────────┼────────────────┘
                        ▼
               ┌─────────────────┐
               │   可观测平台      │
               │ Prometheus +    │
               │ Grafana + ELK   │
               │ + Jaeger/SkyWalking│
               └─────────────────┘

跟着做

步骤 1:Spring Boot Actuator + Micrometer 指标

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  endpoint:
    health:
      show-details: always
  metrics:
    tags:
      application: codeforge-api
// 自定义业务指标
@Component
public class LessonMetrics {

    private final Counter lessonCreateCounter;
    private final Timer lessonQueryTimer;

    public LessonMetrics(MeterRegistry registry) {
        this.lessonCreateCounter = Counter.builder("lesson.create.total")
                .description("课程创建总数")
                .register(registry);
        this.lessonQueryTimer = Timer.builder("lesson.query.duration")
                .description("课程查询耗时")
                .publishPercentiles(0.5, 0.95, 0.99)
                .register(registry);
    }

    public void recordCreate() {
        lessonCreateCounter.increment();
    }

    public void recordQuery(Runnable action) {
        lessonQueryTimer.record(action);
    }
}

Prometheus 抓取配置:

# prometheus.yml
scrape_configs:
  - job_name: 'codeforge-api'
    metrics_path: '/actuator/prometheus'
    static_configs:
      - targets: ['localhost:8080']

步骤 2:结构化日志

// 使用 SLF4J + Logback + JSON 格式
// logback-spring.xml
<configuration>
    <appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="net.logstash.logback.encoder.LogstashEncoder">
            <includeMdcKeyName>traceId</includeMdcKeyName>
            <includeMdcKeyName>userId</includeMdcKeyName>
        </encoder>
    </appender>
    <root level="INFO">
        <appender-ref ref="JSON"/>
    </root>
</configuration>
// 业务日志
@Service
public class LessonService {

    private static final Logger log = LoggerFactory.getLogger(LessonService.class);

    public Lesson create(LessonRequest request, Long userId) {
        MDC.put("userId", userId.toString());
        MDC.put("operation", "createLesson");

        log.info("开始创建课程, title={}", request.getTitle());

        try {
            Lesson lesson = doCreate(request, userId);
            log.info("课程创建成功, lessonId={}", lesson.getId());
            return lesson;
        } catch (Exception e) {
            log.error("课程创建失败, title={}", request.getTitle(), e);
            throw e;
        } finally {
            MDC.clear();
        }
    }
}

输出:

{
  "timestamp": "2026-10-07T10:30:00.000+08:00",
  "level": "INFO",
  "logger": "com.codeforge.LessonService",
  "message": "课程创建成功, lessonId=123",
  "traceId": "abc123def456",
  "userId": "1001",
  "operation": "createLesson"
}

步骤 3:分布式追踪——Sleuth + Zipkin

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>
spring:
  sleuth:
    sampler:
      probability: 0.1  # 采样率 10%,生产环境不要 100%
  zipkin:
    base-url: http://zipkin:9411

请求链路:

GET /api/lessons/123
  ├── lesson-service (50ms)
  │     ├── lessonRepository.findById() (30ms)
  │     └── instructorServiceClient.get() (15ms)
  │           └── user-service (10ms)
  └── 返回

Zipkin 界面能看到每个 Span 的耗时、标签、日志。

步骤 4:多维度限流

@Service
public class RateLimitService {

    private final RedisTemplate<String, String> redisTemplate;

    // 固定窗口限流
    public boolean fixedWindow(String key, int limit, int seconds) {
        String redisKey = "rate:fixed:" + key;
        String current = redisTemplate.opsForValue().get(redisKey);
        if (current == null) {
            redisTemplate.opsForValue().set(redisKey, "1", Duration.ofSeconds(seconds));
            return true;
        }
        int count = Integer.parseInt(current);
        if (count >= limit) {
            return false;
        }
        redisTemplate.opsForValue().increment(redisKey);
        return true;
    }

    // 滑动窗口限流(更精确)
    public boolean slidingWindow(String key, int limit, int seconds) {
        long now = System.currentTimeMillis();
        long windowStart = now - seconds * 1000L;

        String redisKey = "rate:sliding:" + key;

        // 删除窗口外的记录
        redisTemplate.opsForZSet().removeRangeByScore(redisKey, 0, windowStart);

        // 统计当前窗口请求数
        Long count = redisTemplate.opsForZSet().count(redisKey, windowStart, now);
        if (count != null && count >= limit) {
            return false;
        }

        // 记录本次请求
        redisTemplate.opsForZSet().add(redisKey, String.valueOf(now), now);
        redisTemplate.expire(redisKey, Duration.ofSeconds(seconds));

        return true;
    }

    // 令牌桶(应对突发流量)
    public boolean tokenBucket(String key, int capacity, int ratePerSecond) {
        // 用 Redis + Lua 实现原子操作
        String luaScript = """
                local capacity = tonumber(ARGV[1])
                local rate = tonumber(ARGV[2])
                local now = tonumber(ARGV[3])
                local key = KEYS[1]

                local bucket = redis.call('HMGET', key, 'tokens', 'last_refill')
                local tokens = tonumber(bucket[1]) or capacity
                local last_refill = tonumber(bucket[2]) or now

                -- 计算应补充的令牌
                local elapsed = math.max(0, now - last_refill)
                local refill = elapsed * rate
                tokens = math.min(capacity, tokens + refill)

                if tokens >= 1 then
                    tokens = tokens - 1
                    redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)
                    redis.call('EXPIRE', key, 60)
                    return 1
                else
                    return 0
                end
                """;

        String redisKey = "rate:bucket:" + key;
        Long result = redisTemplate.execute(
                new DefaultRedisScript<>(luaScript, Long.class),
                Collections.singletonList(redisKey),
                String.valueOf(capacity),
                String.valueOf(ratePerSecond),
                String.valueOf(System.currentTimeMillis())
        );

        return result != null && result == 1;
    }
}
// 使用示例
@GetMapping("/api/lessons")
public ApiResponse<List<Lesson>> list(HttpServletRequest request,
                                     @AuthenticationPrincipal User user) {
    // 按 IP 限流:100 次/分钟
    String ip = getClientIp(request);
    if (!rateLimitService.slidingWindow("ip:" + ip, 100, 60)) {
        throw new BusinessException(429, "请求过于频繁,请稍后再试");
    }

    // 按用户限流:1000 次/小时
    if (user != null && !rateLimitService.slidingWindow("user:" + user.getId(), 1000, 3600)) {
        throw new BusinessException(429, "用户请求超限");
    }

    return ApiResponse.success(lessonService.findAll());
}

步骤 5:GDPR 合规要点

// 数据主体权利响应
@RestController
@RequestMapping("/api/gdpr")
public class GdprController {

    // 数据可携权:导出用户所有数据
    @GetMapping("/export")
    public ApiResponse<UserDataExport> exportData(@AuthenticationPrincipal User user) {
        UserDataExport export = gdprService.exportUserData(user.getId());
        return ApiResponse.success(export);
    }

    // 被遗忘权:删除用户所有数据
    @DeleteMapping("/erase")
    public ApiResponse<Void> eraseData(@AuthenticationPrincipal User user) {
        gdprService.eraseUserData(user.getId());
        return ApiResponse.success(null);
    }
}

GDPR 关键要求:

  • 数据最小化:只收集必要数据,不过度采集
  • 用户同意:明确告知数据用途,获取同意
  • 数据可携:用户有权导出自己的数据
  • 被遗忘:用户有权要求删除数据
  • 数据保护:加密存储、访问控制、泄露通知(72 小时内)

讲清楚要点

  1. 可观测不是可选的:没有 Metrics 你不知道系统健不健康,没有 Logging 你排查问题靠猜,没有 Tracing 你定位慢请求靠运气。三支柱缺一不可。
  2. 指标要分层次:系统指标(CPU、内存、磁盘)、应用指标(JVM GC、线程池)、业务指标(订单量、注册数、课程创建数)。业务指标最能反映系统价值。
  3. 日志要结构化:JSON 格式比纯文本更容易检索。带 traceId 的日志能串联整个请求链路,排查问题时「一键定位」。
  4. 限流算法有取舍:固定窗口简单但有临界问题;滑动窗口精确但内存消耗大;令牌桶应对突发但实现复杂。按场景选择,或组合使用。
  5. GDPR 是法律,不是建议:违反 GDPR 最高罚款全球营收 4%。欧盟用户的数据必须合法处理,数据本地化、跨境传输都有严格规定。

你可能会懵的地方

  • Prometheus 和 InfluxDB 怎么选? Prometheus:拉模式、生态丰富、K8s 原生,适合云原生;InfluxDB:推模式、写入性能高、SQL-like 查询,适合 IoT、传感器。监控用 Prometheus,时序数据用 InfluxDB。

  • 日志量太大怎么办? 采样(如错误日志 100%,普通日志 1%)、分级(ERROR 存长期,DEBUG 存短期)、压缩、冷热分离(热数据 Elasticsearch,冷数据 S3)。

  • 限流阈值怎么定? 根据系统容量测试得出:CPU 70%、内存 80%、RT P99 < 500ms 时的最大 QPS,再留 30% 余量。逐步调整,不要拍脑袋。

  • 分布式追踪采样率多少合适? 核心链路(如支付、下单)100%;普通查询 1-10%。采样率太高影响性能,太低丢失关键信息。

  • 中国用户需要 GDPR 吗? 不需要 GDPR,但需要《个人信息保护法》(PIPL)。要求类似:知情同意、最小必要、数据本地化、跨境安全评估。出海业务两者都要考虑。

错法对照

错法问题正解
只监控系统指标,不监控业务指标系统正常但业务下跌发现不了业务指标(订单量、注册数)同样告警
日志打印敏感信息(密码、Token)日志泄露 = 数据泄露脱敏后打印,或完全不打印敏感字段
所有接口一个限流阈值登录接口被刷爆,正常用户也登录不了按接口、按用户、按 IP 多维度限流
生产环境日志级别 DEBUG磁盘打满,性能下降生产环境 INFO 以上,按需临时开 DEBUG
用户数据随意跨境传输违反 PIPL/GDPR,面临罚款数据本地化,跨境前做安全评估

卡点急救

  1. Prometheus 采集不到指标:检查 management.endpoints.web.exposure.include 是否包含 prometheus,防火墙是否放行端口。
  2. Zipkin 看不到链路:检查 Sleuth 版本与 Spring Boot 版本兼容性(Spring Boot 3.x 用 Micrometer Tracing),采样率是否为 0。
  3. Redis 限流在集群环境下不准:Redis Cluster 有主从延迟,用 Lua 脚本保证原子性,或改用本地缓存 + 分布式协调。

面试常问

  • 可观测三支柱是什么?怎么落地? Metrics(Prometheus + Grafana)、Logging(ELK/Loki)、Tracing(Jaeger/SkyWalking)。能讲出每个支柱的工具选型和数据流。

  • 怎么设计一个限流系统? 算法(固定窗口、滑动窗口、令牌桶、漏桶)、维度(IP、用户、接口、租户)、存储(Redis、本地缓存)、降级策略(限流后返回缓存或排队)。

  • GDPR 的核心要求?怎么在技术层面落实? 知情同意(Cookie 弹窗、隐私政策)、数据最小化(只收集必要字段)、数据可携(导出接口)、被遗忘(删除接口)、数据保护(加密、访问控制、泄露通知)。

自测三问

  1. 如果 API 响应时间突然从 100ms 涨到 2s,你会怎么排查?
  2. 设计一个「秒杀」场景的限流方案,预期 QPS 10 万。
  3. 用户要求删除所有个人数据,你的系统需要怎么处理?

小结一句话

可观测让你「看见」系统,合规让你「合法」运营;指标、日志、追踪是排障三剑客,限流、加密、审计是防护三盾牌。

延伸

  • Prometheus 官方文档
  • OpenTelemetry 文档
  • GDPR 官方文本
  • 下一课:文档与工具——OpenAPI 规范、Swagger 与协作流程。
加载本节练习…
← 上一课测试下一课 →文档与工具