文档站点
Skip to content

限流 ​

Laravel 用 RateLimiter + throttle 中间件做接口限流;Spring Boot 没有内置对应的全局限流中间件,但有多种实现路径:Bucket4j(Java 版令牌桶,最贴近 RateLimiter 体验)、Redis 计数器、以及第三方网关限流。这篇给出最实用的两种。

方案一:Bucket4j(代码级限流,最接近 Laravel 体验) ​

RateLimiter::for('api', fn() => Limit::perMinute(60)) 的对应物是 Bucket4j 令牌桶算法。

依赖 ​

xml
<dependency>
    <groupId>com.bucket4j</groupId>
    <artifactId>bucket4j-core</artifactId>
    <version>8.10.1</version>
</dependency>

用拦截器实现限流(推荐做法) ​

先建一个「限流元数据」注解(想限哪些接口就标哪个):

java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
    int perMinute() default 60;      // 每分钟次数
}

再写拦截器,按「IP + 方法」维度限流:

java
@Component
public class RateLimitInterceptor implements HandlerInterceptor {

    private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                             Object handler) throws Exception {
        if (handler instanceof HandlerMethod method) {
            RateLimit limit = method.getMethodAnnotation(RateLimit.class);
            if (limit != null) {
                String key = request.getRemoteAddr() + ":" + method.getMethod().getName();
                Bucket bucket = buckets.computeIfAbsent(key, k ->
                        Bucket.builder()
                                .addLimit(Bandwidth.simple(limit.perMinute(), Duration.ofMinutes(1)))
                                .build());
                if (!bucket.tryConsume(1)) {
                    response.setStatus(429);                    // Too Many Requests
                    response.getWriter().write("{\"code\":429,\"message\":\"请求过于频繁\"}");
                    return false;
                }
            }
        }
        return true;
    }
}

注册(记得 excludePathPatterns 放行登录等接口):

java
@Configuration
public class WebConfig implements WebMvcConfigurer {
    private final RateLimitInterceptor interceptor;

    public WebConfig(RateLimitInterceptor interceptor) {
        this.interceptor = interceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(interceptor).addPathPatterns("/api/**");
    }
}

使用:

java
@PostMapping("/send-code")
@RateLimit(perMinute = 3)          // 一分钟最多发 3 次验证码
public void sendCode(@RequestBody SendCodeDto dto) { ... }

对比 Laravel

LaravelSpring Boot
RateLimiter::for('send-code', ...)@RateLimit 注解 + 拦截器
->middleware('throttle:send-code')在方法上标注注注册
429 Too Many Requestsresponse.setStatus(429)
按用户/IP 区分 key拦截器里自己拼 key

方案二:Redis + 计数器(分布式限流) ​

多实例部署时,上面内存里的 ConcurrentHashMap 每个实例各算各的,加起来是真实限流的 N 倍。需要分布式限流:用 Redis 原子计数。

java
@Service
public class RedisRateLimiter {
    private final StringRedisTemplate redis;

    public RedisRateLimiter(StringRedisTemplate redis) {
        this.redis = redis;
    }

    public boolean tryAcquire(String key, int max, Duration window) {
        long now = System.currentTimeMillis();
        // 简单滑动窗口:统计窗口内请求数(生产可用 Lua 脚本保证原子性)
        Long count = redis.opsForValue().increment(key, 1);
        if (count != null && count == 1) {
            redis.expire(key, window);
        }
        return count != null && count <= max;
    }
}

TIP

RedisRateLimiter 的原子性问题(判断 + 递增不是原子的)生产要用 Lua 脚本保证,或直接用 Spring 的 spring-boot-starter-data-redis + RedisScript。这只是思路演示。

方案三:网关限流(不改代码) ​

如果项目有 Spring Cloud Gateway(微服务网关)或 Nginx,限流直接在入口层做:

  • Nginx:limit_req_zone / limit_conn_zone
  • Spring Cloud Gateway:RequestRateLimiter + Redis

优点:业务代码零改动、全局统一、性能最好。缺点:规则不灵活,改策略要动网关。

怎么选?

场景方案
单实例 / 简单接口Bucket4j 拦截器
多实例 / 微服务Redis 分布式限流
统一入口管理网关 / Nginx
登录、验证码等敏感接口任意方案,必须限

实践建议 ​

  1. 给关键接口限流:登录、发送验证码、短信、支付回调、开放 API
  2. 返回 429:别返回 200 再带业务码,HTTP 语义要正确
  3. 按场景给不同额度:登录接口 5 次/分钟,普通接口 60 次/分钟
  4. 限流与缓存穿透防护结合:见 缓存

面向 PHP / Laravel 开发者的 Spring Boot 中文文档