限流

限流(Rate Limiting)通过控制单位时间内请求处理量,防止系统因流量过载而崩溃,是高可用架构的核心防护手段。


算法对比

算法原理特点适用场景
固定窗口固定时间窗内计数,超出拒绝实现简单,存在临界突刺问题对精度要求不高的场景
滑动窗口按当前时间向前滑动统计解决临界突刺,内存稍大通用限流
漏桶请求入桶,以固定速率流出平滑输出,无法应对突发恒定速率处理
令牌桶按速率生成令牌,有令牌才放行允许一定突发,最常用接口限流、API 网关

令牌桶是生产中最常见的选择:平均速率可控,同时允许短时突发流量消耗桶中积累的令牌。


方案一:Bucket4j(本地令牌桶)

适合单机或无需跨实例同步的场景。

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

基本用法

@Component
public class RateLimitService {
 
    // 每秒 100 个令牌,桶容量 100
    private final Bucket bucket = Bucket.builder()
        .addLimit(Bandwidth.classic(100, Refill.greedy(100, Duration.ofSeconds(1))))
        .build();
 
    public boolean tryConsume() {
        return bucket.tryConsume(1);
    }
 
    // 阻塞等待令牌(适合后台任务)
    public void consume() throws InterruptedException {
        bucket.asBlocking().consume(1);
    }
}

多维度限流(接口 + 用户)

@Component
public class UserRateLimiter {
 
    // 每个用户独立的桶
    private final Map<String, Bucket> userBuckets = new ConcurrentHashMap<>();
 
    private Bucket getBucket(String userId) {
        return userBuckets.computeIfAbsent(userId, k ->
            Bucket.builder()
                .addLimit(Bandwidth.classic(10, Refill.intervally(10, Duration.ofMinutes(1))))
                .build()
        );
    }
 
    public boolean tryConsume(String userId) {
        return getBucket(userId).tryConsume(1);
    }
}

注解 + 拦截器封装

// 1. 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
    long capacity() default 100;         // 桶容量
    long refillTokens() default 100;     // 每次补充令牌数
    long refillPeriodSeconds() default 1; // 补充周期(秒)
    String keyPrefix() default "";        // 限流 key 前缀
}
 
// 2. 拦截器
@Component
@RequiredArgsConstructor
@Slf4j
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)) return true;
 
        RateLimit annotation = method.getMethodAnnotation(RateLimit.class);
        if (annotation == null) return true;
 
        String key = annotation.keyPrefix() + ":" + request.getRequestURI();
        Bucket bucket = buckets.computeIfAbsent(key, k ->
            Bucket.builder()
                .addLimit(Bandwidth.classic(
                    annotation.capacity(),
                    Refill.greedy(annotation.refillTokens(),
                        Duration.ofSeconds(annotation.refillPeriodSeconds()))
                ))
                .build()
        );
 
        if (bucket.tryConsume(1)) {
            return true;
        }
 
        log.warn("限流触发: uri={}", request.getRequestURI());
        response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.getWriter().write("{\"code\":429,\"message\":\"请求过于频繁,请稍后再试\"}");
        return false;
    }
}
 
// 3. 使用
@RestController
public class UserController {
 
    @GetMapping("/users")
    @RateLimit(capacity = 50, refillTokens = 50, refillPeriodSeconds = 1)
    public List<User> listUsers() {
        return userService.findAll();
    }
}

方案二:Resilience4j RateLimiter

Resilience4j 是 Spring Boot 3.x 推荐的弹性组件,支持限流、熔断、重试、舱壁等模式。

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot3</artifactId>
    <version>2.2.0</version>
</dependency>
resilience4j:
  ratelimiter:
    instances:
      userApi:
        limit-for-period: 100          # 每个周期最多 100 次请求
        limit-refresh-period: 1s       # 周期长度
        timeout-duration: 0            # 等待令牌超时时间(0 = 立即失败)
        register-health-indicator: true
      orderApi:
        limit-for-period: 20
        limit-refresh-period: 1s
        timeout-duration: 500ms        # 等待最多 500ms

注解方式

@Service
public class UserService {
 
    @RateLimiter(name = "userApi", fallbackMethod = "getUserFallback")
    public User getById(Long id) {
        return userRepository.findById(id).orElseThrow();
    }
 
    // 限流触发时的降级方法(参数列表 + RequestNotPermitted)
    public User getUserFallback(Long id, RequestNotPermitted ex) {
        log.warn("限流降级: userId={}", id);
        return User.empty();  // 返回空对象或抛出友好异常
    }
}

编程式

@Component
@RequiredArgsConstructor
public class OrderService {
 
    private final RateLimiterRegistry rateLimiterRegistry;
 
    public Order createOrder(CreateOrderRequest req) {
        RateLimiter limiter = rateLimiterRegistry.rateLimiter("orderApi");
 
        // 尝试获取许可(立即返回 boolean)
        if (!limiter.acquirePermission()) {
            throw new TooManyRequestsException("下单过于频繁");
        }
 
        return processOrder(req);
    }
 
    // 装饰函数式
    public Supplier<Order> decoratedCreate(CreateOrderRequest req) {
        return RateLimiter.decorateSupplier(
            rateLimiterRegistry.rateLimiter("orderApi"),
            () -> processOrder(req)
        );
    }
}

与熔断、重试组合

resilience4j:
  ratelimiter:
    instances:
      paymentApi:
        limit-for-period: 30
        limit-refresh-period: 1s
  circuitbreaker:
    instances:
      paymentApi:
        failure-rate-threshold: 50
        sliding-window-size: 10
  retry:
    instances:
      paymentApi:
        max-attempts: 3
        wait-duration: 500ms
// 组合顺序:Retry(CircuitBreaker(RateLimiter(fn)))
@RateLimiter(name = "paymentApi")
@CircuitBreaker(name = "paymentApi")
@Retry(name = "paymentApi")
public PaymentResult pay(PaymentRequest request) {
    return paymentClient.pay(request);
}

熔断参见 安全,重试参见 WebClient


方案三:Redis + Lua 滑动窗口(分布式)

跨多实例共享限流计数,适合集群部署。

@Component
@RequiredArgsConstructor
public class RedisRateLimiter {
 
    private final StringRedisTemplate redisTemplate;
 
    // Lua 脚本:滑动窗口限流(原子操作)
    private static final String SLIDING_WINDOW_SCRIPT = """
        local key = KEYS[1]
        local now = tonumber(ARGV[1])
        local window = tonumber(ARGV[2])
        local limit = tonumber(ARGV[3])
        local unique = ARGV[4]
        
        -- 移除窗口外的旧记录
        redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
        -- 统计窗口内的请求数
        local count = redis.call('ZCARD', key)
        
        if count < limit then
            -- 未达到限制,记录本次请求
            redis.call('ZADD', key, now, unique)
            redis.call('EXPIRE', key, math.ceil(window / 1000))
            return 1
        end
        return 0
        """;
 
    private final DefaultRedisScript<Long> script = new DefaultRedisScript<>(
        SLIDING_WINDOW_SCRIPT, Long.class);
 
    /**
     * 滑动窗口限流
     * @param key    限流 key(如 "ratelimit:api:/users:127.0.0.1")
     * @param windowMs 窗口大小(毫秒)
     * @param limit  窗口内最大请求数
     */
    public boolean tryAcquire(String key, long windowMs, int limit) {
        long now = System.currentTimeMillis();
        Long result = redisTemplate.execute(
            script,
            List.of(key),
            String.valueOf(now),
            String.valueOf(windowMs),
            String.valueOf(limit),
            UUID.randomUUID().toString()
        );
        return Long.valueOf(1L).equals(result);
    }
}

基于 Redis 令牌桶(Redisson)

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.27.2</version>
</dependency>
@Component
@RequiredArgsConstructor
public class RedissonRateLimiter {
 
    private final RedissonClient redissonClient;
 
    public RRateLimiter getOrCreate(String name, long rate, long rateInterval) {
        RRateLimiter limiter = redissonClient.getRateLimiter(name);
        // 初始化:每 rateInterval 秒补充 rate 个令牌
        limiter.trySetRate(RateType.OVERALL, rate, rateInterval, RateIntervalUnit.SECONDS);
        return limiter;
    }
 
    public boolean tryAcquire(String name) {
        RRateLimiter limiter = getOrCreate(name, 100, 1);
        return limiter.tryAcquire();
    }
 
    // 带超时等待
    public boolean tryAcquireWithTimeout(String name, long timeout, TimeUnit unit) {
        RRateLimiter limiter = getOrCreate(name, 100, 1);
        return limiter.tryAcquire(timeout, unit);
    }
}

Redis 集成详见 Redis集成


方案四:Spring Cloud Gateway 网关层限流

网关统一限流,应用层无需关心,适合微服务架构。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
spring:
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/api/users/**
          filters:
            - name: RequestRateLimiter
              args:
                # 令牌桶:每秒补充 10 个令牌,桶容量 20
                redis-rate-limiter.replenishRate: 10
                redis-rate-limiter.burstCapacity: 20
                redis-rate-limiter.requestedTokens: 1
                # 根据什么维度限流(此处按 IP 限流)
                key-resolver: "#{@ipKeyResolver}"
@Configuration
public class GatewayConfig {
 
    // 按 IP 限流
    @Bean
    public KeyResolver ipKeyResolver() {
        return exchange -> Mono.just(
            Objects.requireNonNull(exchange.getRequest().getRemoteAddress())
                .getAddress().getHostAddress()
        );
    }
 
    // 按用户 ID 限流(需解析 Token)
    @Bean
    public KeyResolver userKeyResolver() {
        return exchange -> {
            String token = exchange.getRequest().getHeaders()
                .getFirst(HttpHeaders.AUTHORIZATION);
            String userId = jwtUtil.extractUserId(token);
            return Mono.just("user:" + userId);
        };
    }
 
    // 按接口路径限流
    @Bean
    public KeyResolver apiKeyResolver() {
        return exchange -> Mono.just(
            exchange.getRequest().getPath().value()
        );
    }
}

方案五:Sentinel(阿里巴巴,功能最全)

Sentinel 支持 QPS 限流、并发线程数限流、热点参数限流、系统自适应限流,并提供实时监控控制台。

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8080   # Sentinel 控制台地址
        port: 8719
      eager: true
@Service
public class ProductService {
 
    // @SentinelResource 定义资源 + 降级方法
    @SentinelResource(
        value = "getProduct",
        blockHandler = "handleBlock",   // 限流/熔断时调用
        fallback = "handleFallback"     // 业务异常时调用
    )
    public Product getProduct(Long id) {
        return productRepository.findById(id).orElseThrow();
    }
 
    // blockHandler:FlowException(限流)/ DegradeException(熔断)
    public Product handleBlock(Long id, BlockException ex) {
        log.warn("Sentinel 限流: id={}, rule={}", id, ex.getRule());
        return Product.empty();
    }
 
    // fallback:业务异常
    public Product handleFallback(Long id, Throwable t) {
        log.error("服务降级: id={}", id, t);
        return Product.empty();
    }
}
 
// 编程式配置规则(也可通过控制台动态推送)
@PostConstruct
public void initRules() {
    // QPS 限流规则
    FlowRule rule = new FlowRule("getProduct")
        .setGrade(RuleConstant.FLOW_GRADE_QPS)
        .setCount(50)                          // 每秒最多 50 次
        .setStrategy(RuleConstant.STRATEGY_DIRECT)
        .setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT);  // 直接拒绝
    FlowRuleManager.loadRules(List.of(rule));
 
    // 热点参数限流(对特定参数值单独限流)
    ParamFlowRule paramRule = new ParamFlowRule("getProduct")
        .setParamIdx(0)       // 第 0 个参数(id)
        .setCount(10);        // 每秒最多 10 次
    ParamFlowRuleManager.loadRules(List.of(paramRule));
}

限流响应统一处理

@RestControllerAdvice
public class GlobalExceptionHandler {
 
    // Bucket4j / 自定义限流异常
    @ExceptionHandler(TooManyRequestsException.class)
    @ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
    public ApiResponse<?> handleRateLimit(TooManyRequestsException ex) {
        return ApiResponse.error(429, "请求过于频繁,请稍后再试");
    }
 
    // Resilience4j
    @ExceptionHandler(RequestNotPermitted.class)
    @ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
    public ApiResponse<?> handleRateLimiter(RequestNotPermitted ex) {
        return ApiResponse.error(429, "系统繁忙,请稍后再试");
    }
 
    // Sentinel
    @ExceptionHandler(FlowException.class)
    @ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
    public ApiResponse<?> handleSentinel(FlowException ex) {
        return ApiResponse.error(429, "当前访问人数过多");
    }
}

全局异常处理详见 全局异常处理


方案选型建议

场景推荐方案
单机、简单接口限流Bucket4j
单机、需要熔断/重试组合Resilience4j
多实例、分布式限流Redis(Redisson / Lua 脚本)
微服务网关统一限流Spring Cloud Gateway + Redis
大流量、需实时监控Sentinel

相关链接