适合人群:具备 Spring Web 基础、希望在接口层做统一治理的开发者或技术爱好者。
建议先看:软件工程实践二(Spring Boot 知识回顾)、实践三(RESTful API 设计原则)。
1. 方案选型速览
- Filter(Servlet 层):最靠前,处理包括静态资源;适合全局跨域、XSS、统一编码等。
- HandlerInterceptor(Spring MVC 层):在进入 Controller 前后执行;适合接口打点、鉴权、限流、审计。
- AOP(方法层):对注解/切点进行横切;适合注解式限流、审计、幂等等。
- API Gateway(边界层):最靠外;适合全局流控、黑白名单、熔断、灰度,一处管多服务。
一般应用内优先用 Interceptor + AOP;如果有网关,也可在网关做粗粒度限流,在应用内做精细化限流与监控。
国内比较熟悉的:Sentinel
2. 基于 HandlerInterceptor 的接口监控
目标:记录每次请求的关键信息与耗时,并输出到日志或指标系统。
示例:
// RequestLoggingInterceptor.java
// 功能:为每个进入的 HTTP 请求生成请求ID、记录入站/出站日志与耗时
package com.example.demo.web;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import java.util.UUID;
public class RequestLoggingInterceptor implements HandlerInterceptor {
private static final Logger log = LoggerFactory.getLogger(RequestLoggingInterceptor.class);
private static final String REQ_ID = "reqId"; // MDC 中的请求 ID 键
private static final String START = "startAt"; // 请求开始时间属性键
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String requestId = UUID.randomUUID().toString(); // 生成请求唯一 ID,便于链路追踪
MDC.put(REQ_ID, requestId); // 放入日志 MDC
request.setAttribute(START, System.currentTimeMillis()); // 记录起始时间
String method = request.getMethod();
String uri = request.getRequestURI();
String ip = getClientIp(request); // 识别真实客户端 IP(支持代理场景)
log.info("[IN] {} {} ip={} ua={}", method, uri, ip, request.getHeader("User-Agent"));
return true; // 返回 true 放行请求
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
// 可在此处添加对响应体或模型的额外处理
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
Long start = (Long) request.getAttribute(START);
long costMs = start == null ? -1 : (System.currentTimeMillis() - start); // 计算耗时
int status = response.getStatus();
if (ex != null) {
log.warn("[OUT] status={} costMs={} ex={}", status, costMs, ex.toString()); // 异常时告警
} else {
log.info("[OUT] status={} costMs={}", status, costMs); // 正常返回
}
MDC.remove(REQ_ID); // 清理 MDC,避免线程复用造成污染
}
private String getClientIp(HttpServletRequest request) {
String xff = request.getHeader("X-Forwarded-For"); // 代理链:第一个为原始客户端 IP
if (xff != null && !xff.isEmpty()) {
return xff.split(",")[0].trim();
}
String realIp = request.getHeader("X-Real-IP"); // 一些代理使用该头
if (realIp != null && !realIp.isEmpty()) {
return realIp;
}
return request.getRemoteAddr(); // 直接连接时的远端地址
}
}
注册拦截器:
// WebMvcConfig.java
// 功能:注册全局拦截器,并对健康检查/文档/静态资源等路径放行
package com.example.demo.config;
import com.example.demo.web.RequestLoggingInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new RequestLoggingInterceptor()) // 注册请求日志拦截器
.addPathPatterns("/**") // 默认拦截所有路径
.excludePathPatterns( // 放行以下常见非业务路径
"/actuator/**",
"/swagger-resources/**",
"/v3/api-docs/**",
"/doc.html",
"/swagger-ui/**",
"/error",
"/static/**"
);
}
}
3. log配置文件 logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 控制台输出 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- %X 代表输出全部MDC内容,%X{key}代表指定key -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg %X%n</pattern>
</encoder>
</appender>
<!-- 文件输出,可以按需配置 -->
<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/app.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg %X%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="console"/>
<appender-ref ref="file"/>
</root>
</configuration>
4. 流量控制的常见策略与算法
- 维度:全局、按接口、按 IP、按用户、按来源(AppId)、按租户等
算法:
- 固定窗口(simple, 可能边界抖动)
- 滑动窗口(更平滑)
- 令牌桶(Token Bucket,允许突发)
- 漏桶(Leaky Bucket,恒定速率)
选型建议:
- 单机快速落地:Guava RateLimiter 或 Bucket4j
- 分布式一致性:Redis
- 网关层粗粒度 + 应用内细粒度
5. 本地单机限流:Guava RateLimiter(简单上手)
<!-- Maven 依赖:Guava 提供本地令牌桶(RateLimiter) -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.0.0-jre</version>
</dependency>
// SimpleRateLimitInterceptor.java
// 功能:针对 URI 维度基于 Guava RateLimiter 的单机限流
package com.example.demo.limit;
import com.google.common.util.concurrent.RateLimiter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.HandlerInterceptor;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class SimpleRateLimitInterceptor implements HandlerInterceptor {
private final ConcurrentMap<String, RateLimiter> uriToLimiter = new ConcurrentHashMap<>(); // 每个 URI 一个限流器
private final double permitsPerSecond; // 每秒允许的请求数(QPS)
public SimpleRateLimitInterceptor(double permitsPerSecond) {
this.permitsPerSecond = permitsPerSecond;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String key = request.getRequestURI(); // 维度选择:可替换为 URI+IP/用户ID 等
RateLimiter limiter = uriToLimiter.computeIfAbsent(key, k -> RateLimiter.create(permitsPerSecond));
boolean allowed = limiter.tryAcquire(); // 非阻塞获取令牌,失败立即返回 429
if (!allowed) {
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
byte[] body = "Too Many Requests".getBytes(StandardCharsets.UTF_8);
response.getOutputStream().write(body);
return false;
}
return true;
}
}
注册:
// 将限流器应用到 /api/** 的请求路径
registry.addInterceptor(new SimpleRateLimitInterceptor(50.0)) // 每秒 50 次/接口
.addPathPatterns("/api/**");
优点:简单易用;缺点:单机生效,不适合分布式一致性。
6. 本地单机限流:Bucket4j(功能更强)
<!-- Bucket4j:功能丰富的令牌桶库,支持多种后端集成(Redis/Hazelcast) -->
<dependency>
<groupId>com.bucket4j</groupId>
<artifactId>bucket4j-core</artifactId>
<version>8.10.1</version>
</dependency>
// Bucket4jRateLimitInterceptor.java
// 功能:基于 Bucket4j 的单机令牌桶限流
package com.example.demo.limit;
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Bucket4j;
import io.github.bucket4j.Refill;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.HandlerInterceptor;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class Bucket4jRateLimitInterceptor implements HandlerInterceptor {
private final ConcurrentMap<String, Bucket> bucketMap = new ConcurrentHashMap<>(); // 每个 URI 一个桶
private Bucket newBucket() {
// 令牌桶配置:容量 100,按每秒补充 50 个令牌;允许一定突发
Bandwidth limit = Bandwidth.classic(100, Refill.intervally(50, Duration.ofSeconds(1)));
return Bucket.builder().addLimit(limit).build();
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String key = request.getRequestURI();
Bucket bucket = bucketMap.computeIfAbsent(key, k -> newBucket()); // 延迟创建桶
if (bucket.tryConsume(1)) { // 消耗 1 个令牌
return true;
}
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.getOutputStream().write("Too Many Requests".getBytes(StandardCharsets.UTF_8));
return false;
}
}
优势:策略多、可配额外指标;劣势:默认仍是单机。可选用 bucket4j-redis
或 Hazelcast 实现分布式令牌桶。
7. 分布式限流
思路:使用固定窗口计数法(Fixed Window)。对某个限流 key(如 接口+IP),在一个窗口期内使用 INCR
计数,首次创建时设置 EXPIRE
为窗口大小,到期自动清除。无需 Lua,保证实现简单、易维护。
特性:
- 简单高效;适合大多数分布式限流需求。
- 边界在窗口切换时可能出现短暂突发(固定窗口特性)。若需更平滑的滑动窗口,可使用 Redis 事务(WATCH/MULTI/EXEC)或 Lua(本文不使用 Lua)。
// RedisFixedWindowRateLimitInterceptor.java
// 功能:基于 Redis 的固定窗口限流(纯 Redis 命令:INCR + EXPIRE)
package com.example.demo.limit;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.HandlerInterceptor;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
public class RedisFixedWindowRateLimitInterceptor implements HandlerInterceptor {
private final StringRedisTemplate redis;
private final int maxReq; // 窗口内最大请求数
private final Duration window; // 窗口大小
public RedisFixedWindowRateLimitInterceptor(StringRedisTemplate redis, int maxReq, Duration window) {
this.redis = redis;
this.maxReq = maxReq;
this.window = window;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String baseKey = buildBaseKey(request); // 例如: rl:{uri}:{ip}
// 将窗口时间片纳入 key,避免首次请求起始时间的不确定性
long windowMs = window.toMillis();
long bucket = System.currentTimeMillis() / windowMs;
String key = baseKey + ":" + bucket; // 形如 rl:/api/xx:1.2.3.4:19736622
Long count = redis.opsForValue().increment(key);
if (count != null && count == 1L) {
// 首次创建时设置过期时间,避免键长期滞留
redis.expire(key, window);
}
if (count != null && count <= maxReq) {
return true;
}
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.getOutputStream().write("Too Many Requests".getBytes(StandardCharsets.UTF_8));
return false;
}
private String buildBaseKey(HttpServletRequest request) {
String uri = request.getRequestURI();
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty()) ip = request.getRemoteAddr();
return "rl:" + uri + ":" + ip; // key 结构:rl:/api/xxx:1.2.3.4
}
}
注册:
// 在 /api/** 启用分布式固定窗口限流:1 秒内最多 100 次
registry.addInterceptor(new RedisFixedWindowRateLimitInterceptor(stringRedisTemplate, 100, Duration.ofSeconds(1)))
.addPathPatterns("/api/**");
说明:
key
维度可按需自定义(接口/用户/租户/来源等)。- 如果需要更平滑的限流,可在应用层做“本地桶 + Redis 合并”或升级为滑动窗口(仍可用纯 Redis 事务实现)。
- 返回 429(Too Many Requests)。
8. 注解式限流(AOP)
定义注解:
// RateLimit.java
// 功能:声明式限流参数,支持在类或方法上使用
package com.example.demo.limit;
import java.lang.annotation.*;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {
int max() default 100; // 窗口内最大请求数
long windowMs() default 1000; // 窗口大小(毫秒)
String key() default "#request.requestURI"; // 基于 SpEL 的 key 表达式
}
切面实现(示例改为每个 key 一个 Guava RateLimiter;按注解参数换算速率):
@Aspect
@Component
public class RateLimitAspect {
private final HttpServletRequest request;
// private final ConcurrentMap<String, RateLimiter> uriToLimiter = new ConcurrentHashMap<>();//
private final ConcurrentMap<String, Bucket> bucketMap = new ConcurrentHashMap<>(); // 每个 URI 一个桶
public RateLimitAspect(HttpServletRequest request) {
this.request = request;
}
@Around("@annotation(rateLimit)")
public Object around(ProceedingJoinPoint pjp, RateLimit rateLimit) throws Throwable {
String key = buildKey(rateLimit); // 可结合 SpEL 动态生成
double permitsPerSecond = calcPermitsPerSecond(rateLimit.max(), rateLimit.windowMs());
// RateLimiter limiter = uriToLimiter.computeIfAbsent(key, k -> RateLimiter.create(permitsPerSecond));
Bucket bucket = bucketMap.computeIfAbsent(key, (k)->{
Bandwidth limit = Bandwidth.classic(Long.parseLong(permitsPerSecond + ""),
Refill.intervally(Long.parseLong(permitsPerSecond + ""), Duration.ofSeconds(1)));
return Bucket.builder().addLimit(limit).build();
});
boolean allowed = bucket.tryConsume(1);
if (allowed) {
return pjp.proceed();
}
throw new ResponseStatusException(HttpStatus.TOO_MANY_REQUESTS, "Too Many Requests");
}
private String buildKey(RateLimit rateLimit) {
String uri = request.getRequestURI();
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty()) ip = request.getRemoteAddr();
return "rl:" + uri + ":" + ip;
}
private double calcPermitsPerSecond(int max, long windowMs) {
if (windowMs <= 0) return Double.POSITIVE_INFINITY;
double sec = windowMs / 1000.0;
double qps = max / sec;
return Math.max(qps, 0.000001); // 避免 0
}
}
使用:
// 在 1 秒窗口内限制最多 20 次
@RateLimit(max = 20, windowMs = 1000)
@GetMapping("/api/resource")
public String resource() { return "ok"; }
9. 统一异常与统一返回
// GlobalExceptionHandler.java
// 功能:统一将异常转换为标准响应体,便于前端与调用方处理
package com.example.demo.web;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.server.ResponseStatusException;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResponseStatusException.class)
public ResponseEntity<Map<String, Object>> handle(ResponseStatusException ex) {
Map<String, Object> body = new HashMap<>();
body.put("code", ex.getStatusCode().value()); // 与 HTTP 状态一致
body.put("message", ex.getReason()); // 由抛出方提供的原因
return new ResponseEntity<>(body, ex.getStatusCode());
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handle(Exception ex) {
Map<String, Object> body = new HashMap<>();
body.put("code", 500);
body.put("message", ex.getMessage()); // 生产可隐藏或映射为通用提示
return new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
10. 网关与真实 IP
- 使用 Nginx/Ingress 时,转发需设置并在应用端读取
X-Forwarded-For
或X-Real-IP
。 若部署在网关后:
- 网关做全局粗粒度限流(防 DDoS),
- 应用内做细粒度限流(按用户/接口)。
11. 压测与验证
curl 验证:
# 并发 100,持续 10s(需要已安装 hey) hey -z 10s -c 100 http://localhost:8080/api/resource
- 观察日志中
[IN]/[OUT]
与 429; - 查看
/actuator/prometheus
指标,并在 Grafana 绘制 QPS、P95、限流次数等。
12. 最佳实践清单
- 日志与指标:所有入口统一打点(请求、状态、耗时、异常)。
- 限流维度:按场景选择(全局/接口/用户/IP/租户)。
- 算法选择:突发流量多用令牌桶;需要平滑用滑动窗口。
- 分布式一致性:使用 Redis/Lua 或 Bucket4j 分布式支持。
- 白名单/开关:在限流前置判断白名单;提供动态阈值与紧急开关。
- 安全:注意绕过手段(多 IP、代理);对敏感接口更严格。
- 性能:Redis 压力大时做本地缓存合并、批量/管道、热点降级。
- 观测:Actuator + Micrometer + Prometheus + Grafana,统一观察服务健康。
13. 参考依赖清单(择需)
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Actuator:健康检查/指标暴露 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Micrometer Prometheus:注册 Prometheus 指标 -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<!-- Redis:用于分布式限流、缓存等 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 可选:Guava 或 Bucket4j(本地限流) -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.0.0-jre</version>
</dependency>
<dependency>
<groupId>com.github.vladimir-bukhtoyarov</groupId>
<artifactId>bucket4j-core</artifactId>
<version>8.10.1</version>
</dependency>
</dependencies>