问题:接口被频繁请求怎么办
一个对外的接口一旦被脚本高频调用,轻则拖慢正常请求,重则打满线程池、拖垮数据库。登录、短信验证码、搜索、导出这类接口尤其容易成为目标。限流就是在入口处给每个调用方设定单位时间内的请求上限,超出的请求直接拒绝,保护后端资源。
本文用固定窗口计数器实现一个轻量限流过滤器,只依赖 JDK 与 Spring Boot 自带 API,适合单实例服务快速落地。
适用场景
- 登录、注册、找回密码等防暴力破解的接口;
- 短信、邮件验证码发送接口,防止被刷造成费用损失;
- 导出、报表、大查询等耗时接口,避免并发过高拖垮服务;
- 对接第三方回调或开放 API,需要按调用方做配额控制。
如果服务是多实例部署,或者限流规则需要动态调整、按用户等级区分额度,本文的本机计数方案就不够用了,应改用 Redis 集中计数。
方案选型:三种常见算法
- 固定窗口计数器:按固定时间窗口计数,窗口结束清零。实现最简单,缺点是窗口边界处可能出现两倍流量尖峰;
- 滑动窗口:把窗口切成多个小格滚动统计,平滑边界尖峰,实现稍复杂;
- 令牌桶:按固定速率往桶里放令牌,请求消耗令牌,允许短时突发,适合允许突发但要控制平均速率的场景。
多数内部接口先上固定窗口就够用,边界尖峰问题在窗口较短、阈值留有余量时影响有限。
关键代码:限流过滤器
用 Spring 的 OncePerRequestFilter 实现,Spring Boot 3.x 使用 jakarta.servlet 包,2.x 对应 javax.servlet 包,其余写法相同。过滤器按客户端 IP 加请求路径做 key,窗口内计数超过上限返回 429。
@Component
@Order(1)
public class RateLimitFilter extends OncePerRequestFilter {
private static final int MAX_REQUESTS = 100;
private static final long WINDOW_MILLIS = 60_000L;
private final ConcurrentHashMap<String, Window> windows = new ConcurrentHashMap<>();
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
String key = request.getRemoteAddr() + ":" + request.getRequestURI();
Window window = windows.computeIfAbsent(key, k -> new Window());
if (!window.tryAcquire()) {
response.setStatus(HttpServletResponse.SC_TOO_MANY_REQUESTS);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":429,\"message\":\"请求过于频繁,请稍后再试\"}");
return;
}
chain.doFilter(request, response);
}
@Scheduled(fixedDelay = 60_000)
public void cleanUp() {
long now = System.currentTimeMillis();
windows.entrySet().removeIf(e -> now - e.getValue().start > WINDOW_MILLIS);
}
private static class Window {
volatile long start = System.currentTimeMillis();
final AtomicLong count = new AtomicLong(0);
synchronized boolean tryAcquire() {
long now = System.currentTimeMillis();
if (now - start >= WINDOW_MILLIS) {
start = now;
count.set(0);
}
return count.incrementAndGet() <= MAX_REQUESTS;
}
}
}需要补充两处配置:定时清理要生效,需在配置类上加 @EnableScheduling 开启定时任务;阈值与窗口长度建议抽到 application.properties,用 @Value 注入,不要写死在代码里多人共用同一份阈值。
注意点
- 过滤器顺序:@Order 值越小越先执行,限流过滤器应排在认证过滤器之后、业务逻辑之前;排在认证之前会把未登录的探测请求也计入同一 key;
- key 的粒度:按 IP 限流简单但会误伤同一出口 IP 下的正常用户,登录类接口建议按“IP + 用户名”组合限流,已登录接口可按用户 ID 限流;
- 代理后的真实 IP:服务在反向代理或负载均衡之后时,getRemoteAddr 拿到的是代理地址,需从 X-Forwarded-For 取首个 IP,且只信任可信代理传来的该请求头;
- 内存增长:每个 key 对应一个窗口对象,key 空间无界时会持续增长,定时清理过期窗口是必须的;key 本身也不要拼接请求参数全文;
- 拒绝响应要明确:返回 429 状态码并在响应体里说明原因与重试建议,前端与调用方才能正确处理;不要用 200 包一个错误码冒充成功;
- 不要对所有接口一刀切:健康检查、静态资源与内部回调接口应排除在限流之外,可在过滤器里按路径白名单放行;
- 窗口边界尖峰:固定窗口在边界处最多放过约两倍阈值的请求,阈值设定时按后端实际承受能力留余量,窗口建议不短于 10 秒。
小结
固定窗口计数器用一个过滤器加一个 ConcurrentHashMap 就能落地,适合单实例服务的登录、验证码、导出等高频接口做第一道防护。它的边界是单机内存与固定窗口的尖峰问题;当服务扩到多实例,或需要按用户等级动态调阈值时,再升级为 Redis 集中计数或令牌桶方案。