1. 概述

本文重点介绍如何在 Spring REST 接口开发中,使用 Bucket4j 实现请求频率限制(Rate Limiting)。

我们将先理解接口限流的基本概念,再深入 Bucket4j 的核心机制,最后通过多个实际场景演示如何在 Spring 应用中集成并灵活应用 Bucket4j。

2. 接口限流(API Rate Limiting)

接口限流是一种控制客户端在特定时间窗口内调用次数的策略。它的主要目的包括:

  • ✅ 防止接口被恶意刷量或滥用
  • ✅ 保护后端服务资源,避免因突发流量导致系统崩溃
  • ✅ 支持基于不同用户等级(如免费/付费)的差异化服务

常见的限流策略通常基于以下标识进行追踪:

  • 客户端 IP 地址
  • API Key(更推荐,便于业务管理)
  • OAuth Access Token

当客户端触发限流时,服务端可采取的措施有:

  • ❌ 直接拒绝:返回 HTTP 429 Too Many Requests
  • ⚠️ 排队等待:将请求放入队列,待窗口重置后处理(实现复杂,较少用)
  • 💰 允许但收费:超限请求按次计费(常见于云服务)

其中,直接拒绝是最简单粗暴且最常用的处理方式。

3. Bucket4j 限流库

3.1. 什么是 Bucket4j?

Bucket4j 是一个基于 令牌桶算法(Token-bucket Algorithm) 的 Java 限流库。其核心优势包括:

  • ✅ 线程安全,适用于单机或分布式环境
  • ✅ 支持 JCache(JSR107)规范,可无缝集成 Redis、Caffeine 等缓存实现分布式限流
  • ✅ 提供灵活的配置方式,支持多种填充策略

项目地址:https://github.com/vladimir-bukhtoyarov/bucket4j

3.2. 令牌桶算法原理

我们可以把“令牌桶”想象成一个水桶:

  • 桶容量(Capacity):最多能存放多少个令牌(即允许的最大请求数)
  • 令牌填充(Refill):以固定速率向桶中添加令牌(如每秒 10 个)
  • 请求处理:每次请求需从桶中获取一个令牌,获取成功则放行,否则拒绝

举个例子:若限流规则为 100 次/分钟,则:

  • 桶容量 = 100
  • 填充速率 = 每分钟补充 100 个令牌

假设某分钟内只处理了 70 次请求,桶中剩余 30 个令牌。下一分钟开始时,桶会被重新填满至 100,而不是在 30 的基础上补充。这种机制确保了每个时间窗口都有完整的额度可用。

⚠️ 注意:如果请求速度远高于填充速度,桶会迅速耗尽,后续请求将被拒绝,直到有新令牌生成。

4. Bucket4j 快速上手

4.1. Maven 依赖

首先引入核心依赖:

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

4.2. 核心概念

在使用前,先了解几个关键类:

  • ✅ **Bucket**:代表一个令牌桶,提供 tryConsume() 方法尝试消费令牌
  • ✅ **Bandwidth**:定义桶的限流规则,包括容量和填充策略
  • ✅ **Refill**:定义令牌的填充方式,如 intervally() 表示周期性填充
  • ✅ **ConsumptionProbe**:tryConsumeAndReturnRemaining() 返回的结果对象,包含剩余令牌数、等待时间等信息

4.3. 基础用法示例

场景1:10次/分钟限流

Refill refill = Refill.intervally(10, Duration.ofMinutes(1));
Bandwidth limit = Bandwidth.classic(10, refill);
Bucket bucket = Bucket.builder()
    .addLimit(limit)
    .build();

// 前10次请求成功
for (int i = 1; i <= 10; i++) {
    assertTrue(bucket.tryConsume(1));
}
// 第11次失败
assertFalse(bucket.tryConsume(1));

场景2:平滑填充(1次/2秒)

Bandwidth limit = Bandwidth.classic(1, Refill.intervally(1, Duration.ofSeconds(2)));
Bucket bucket = Bucket.builder()
    .addLimit(limit)
    .build();
assertTrue(bucket.tryConsume(1)); // 第一次成功

// 2秒后再次尝试
Executors.newScheduledThreadPool(1)
    .schedule(() -> assertTrue(bucket.tryConsume(1)), 2, TimeUnit.SECONDS);

场景3:多级限流(防突发流量)

既要限制 10次/分钟,又要防止前5秒内打满10次,可叠加多个 Bandwidth

Bucket bucket = Bucket.builder()
    .addLimit(Bandwidth.classic(10, Refill.intervally(10, Duration.ofMinutes(1))))
    .addLimit(Bandwidth.classic(5, Refill.intervally(5, Duration.ofSeconds(20))))
    .build();

// 前5次成功
for (int i = 1; i <= 5; i++) {
    assertTrue(bucket.tryConsume(1));
}
// 第6次失败(20秒窗口已满)
assertFalse(bucket.tryConsume(1));

5. 在 Spring 接口中应用 Bucket4j

5.1. 示例接口:面积计算器

我们先构建一个简单的 REST 接口:

@RestController
class AreaCalculationController {

    @PostMapping(value = "/api/v1/area/rectangle")
    public ResponseEntity<AreaV1> rectangle(@RequestBody RectangleDimensionsV1 dimensions) {
        return ResponseEntity.ok(new AreaV1("rectangle", dimensions.getLength() * dimensions.getWidth()));
    }
}

测试调用:

$ curl -X POST http://localhost:9001/api/v1/area/rectangle \
    -H "Content-Type: application/json" \
    -d '{ "length": 10, "width": 12 }'

{ "shape":"rectangle","area":120.0 }

5.2. 基础限流实现

为接口添加 20次/分钟 的硬性限制:

@RestController
class AreaCalculationController {

    private final Bucket bucket;

    public AreaCalculationController() {
        Bandwidth limit = Bandwidth.classic(20, Refill.greedy(20, Duration.ofMinutes(1)));
        this.bucket = Bucket.builder()
            .addLimit(limit)
            .build();
    }

    @PostMapping(value = "/api/v1/area/rectangle")
    public ResponseEntity<AreaV1> rectangle(@RequestBody RectangleDimensionsV1 dimensions) {
        if (bucket.tryConsume(1)) {
            return ResponseEntity.ok(new AreaV1("rectangle", dimensions.getLength() * dimensions.getWidth()));
        }
        return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).build();
    }
}

测试第21次请求:

$ curl -v -X POST http://localhost:9001/api/v1/area/rectangle \
    -H "Content-Type: application/json" \
    -d '{ "length": 10, "width": 12 }'

< HTTP/1.1 429

5.3. 基于 API Key 的分级限流

现实场景中,不同用户等级应有不同的限流策略。我们设计如下套餐:

套餐 限流规则
Free 20次/小时
Basic 40次/小时
Professional 100次/小时

1. 定义套餐枚举

enum PricingPlan {
    FREE {
        Bandwidth getLimit() {
            return Bandwidth.classic(20, Refill.intervally(20, Duration.ofHours(1)));
        }
    },
    BASIC {
        Bandwidth getLimit() {
            return Bandwidth.classic(40, Refill.intervally(40, Duration.ofHours(1)));
        }
    },
    PROFESSIONAL {
        Bandwidth getLimit() {
            return Bandwidth.classic(100, Refill.intervally(100, Duration.ofHours(1)));
        }
    };

    abstract Bandwidth getLimit();

    static PricingPlan resolvePlanFromApiKey(String apiKey) {
        if (apiKey == null || apiKey.isEmpty()) {
            return FREE;
        } else if (apiKey.startsWith("PX001-")) {
            return PROFESSIONAL;
        } else if (apiKey.startsWith("BX001-")) {
            return BASIC;
        }
        return FREE;
    }
}

2. 实现缓存服务

@Service
class PricingPlanService {

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

    public Bucket resolveBucket(String apiKey) {
        return cache.computeIfAbsent(apiKey, this::newBucket);
    }

    private Bucket newBucket(String apiKey) {
        PricingPlan pricingPlan = PricingPlan.resolvePlanFromApiKey(apiKey);
        return Bucket.builder()
            .addLimit(pricingPlan.getLimit())
            .build();
    }
}

3. 更新 Controller

@RestController
class AreaCalculationController {

    @Autowired
    private PricingPlanService pricingPlanService;

    @PostMapping(value = "/api/v1/area/rectangle")
    public ResponseEntity<AreaV1> rectangle(
        @RequestHeader(value = "X-api-key") String apiKey,
        @RequestBody RectangleDimensionsV1 dimensions) {

        Bucket bucket = pricingPlanService.resolveBucket(apiKey);
        ConsumptionProbe probe = bucket.tryConsumeAndReturnRemaining(1);
        
        if (probe.isConsumed()) {
            return ResponseEntity.ok()
                .header("X-Rate-Limit-Remaining", Long.toString(probe.getRemainingTokens()))
                .body(new AreaV1("rectangle", dimensions.getLength() * dimensions.getWidth()));
        }
        
        long waitForRefill = probe.getNanosToWaitForRefill() / 1_000_000_000;
        return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
            .header("X-Rate-Limit-Retry-After-Seconds", String.valueOf(waitForRefill))
            .build();
    }
}

返回头说明:

  • X-Rate-Limit-Remaining:当前窗口剩余请求数
  • X-Rate-Limit-Retry-After-Seconds:需等待多少秒才能重试

测试结果:

# 成功请求
$ curl -v -X POST http://localhost:9001/api/v1/area/rectangle \
    -H "Content-Type: application/json" -H "X-api-key:FX001-99999" \
    -d '{ "length": 10, "width": 12 }'

< HTTP/1.1 200
< X-Rate-Limit-Remaining: 11
{"shape":"rectangle","area":120.0}

# 被限流
$ curl -v -X POST http://localhost:9001/api/v1/area/rectangle \
    -H "Content-Type: application/json" -H "X-api-key:FX001-99999" \
    -d '{ "length": 10, "width": 12 }'

< HTTP/1.1 429
< X-Rate-Limit-Retry-After-Seconds: 583

5.4. 使用 Spring MVC 拦截器统一处理

为避免在每个接口中重复写限流逻辑,推荐使用 HandlerInterceptor

1. 创建拦截器

public class RateLimitInterceptor implements HandlerInterceptor {

    @Autowired
    private PricingPlanService pricingPlanService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 
      throws Exception {
        String apiKey = request.getHeader("X-api-key");
        if (apiKey == null || apiKey.isEmpty()) {
            response.sendError(HttpStatus.BAD_REQUEST.value(), "Missing Header: X-api-key");
            return false;
        }

        Bucket tokenBucket = pricingPlanService.resolveBucket(apiKey);
        ConsumptionProbe probe = tokenBucket.tryConsumeAndReturnRemaining(1);
        if (probe.isConsumed()) {
            response.addHeader("X-Rate-Limit-Remaining", String.valueOf(probe.getRemainingTokens()));
            return true;
        } else {
            long waitForRefill = probe.getNanosToWaitForRefill() / 1_000_000_000;
            response.addHeader("X-Rate-Limit-Retry-After-Seconds", String.valueOf(waitForRefill));
            response.sendError(HttpStatus.TOO_MANY_REQUESTS.value(),
              "You have exhausted your API Request Quota"); 
            return false;
        }
    }
}

2. 注册拦截器

@Configuration
public class Bucket4jRateLimitApp implements WebMvcConfigurer {
    
    @Autowired
    private RateLimitInterceptor interceptor;

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

新增三角形面积接口,无需额外限流代码:

@PostMapping(value = "/api/v1/area/triangle")
public ResponseEntity<AreaV1> triangle(@RequestBody TriangleDimensionsV1 dimensions) {
    return ResponseEntity.ok(new AreaV1("triangle", 0.5d * dimensions.getHeight() * dimensions.getBase()));
}

测试:

# 成功
$ curl -v -X POST http://localhost:9001/api/v1/area/triangle \
    -H "Content-Type: application/json" -H "X-api-key:FX001-99999" \
    -d '{ "height": 15, "base": 8 }'

< HTTP/1.1 200
< X-Rate-Limit-Remaining: 9
{"shape":"triangle","area":60.0}

# 被限流
$ curl -v -X POST http://localhost:9001/api/v1/area/triangle \
    -H "Content-Type: application/json" -H "X-api-key:FX001-99999" \
    -d '{ "height": 15, "base": 8 }'

< HTTP/1.1 429
< X-Rate-Limit-Retry-After-Seconds: 299
{ "status": 429, "error": "Too Many Requests", "message": "You have exhausted your API Request Quota" }

6. 使用 Bucket4j Spring Boot Starter

如果希望完全通过配置实现限流,可以使用官方推荐的自动装配模块。

6.1. 核心特性

  • ✅ 声明式配置,无需编写 Java 代码
  • ✅ 支持 SpEL 表达式动态提取限流 Key(如 header、IP)
  • ✅ 内置多种过滤策略

6.2. Maven 依赖

<dependency>
    <groupId>com.giffing.bucket4j.spring.boot.starter</groupId>
    <artifactId>bucket4j-spring-boot-starter</artifactId>
    <version>0.8.1</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>javax.cache</groupId>
    <artifactId>cache-api</artifactId>
</dependency>
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.8.2</version>
</dependency>
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>jcache</artifactId>
    <version>2.8.2</version>
</dependency>

⚠️ 别忘了在配置类上加上 @EnableCaching

6.3. 配置文件(YAML)

spring:
  cache:
    cache-names:
      - rate-limit-buckets
    caffeine:
      spec: maximumSize=100000,expireAfterAccess=3600s

bucket4j:
  enabled: true
  filters:
    - cache-name: rate-limit-buckets
      url: /api/v1/area.*
      strategy: first
      http-response-body: "{ \"status\": 429, \"error\": \"Too Many Requests\", \"message\": \"You have exhausted your API Request Quota\" }"
      rate-limits:
        - cache-key: "getHeader('X-api-key')"
          execute-condition: "getHeader('X-api-key').startsWith('PX001-')"
          bandwidths:
            - capacity: 100
              time: 1
              unit: hours
        - cache-key: "getHeader('X-api-key')"
          execute-condition: "getHeader('X-api-key').startsWith('BX001-')"
          bandwidths:
            - capacity: 40
              time: 1
              unit: hours
        - cache-key: "getHeader('X-api-key')"
          bandwidths:
            - capacity: 20
              time: 1
              unit: hours

配置说明:

  • strategy: first:匹配到第一个规则后即停止
  • execute-condition:使用 SpEL 判断是否应用该规则
  • cache-key:从请求头提取 API Key 作为缓存键

测试结果与手动实现一致,但代码量大幅减少。

7. 总结

本文系统介绍了在 Spring 生态中使用 Bucket4j 实现接口限流的多种方式:

  • ✅ 手动编码:灵活控制,适合复杂逻辑
  • ✅ 拦截器:解耦业务与限流,推荐生产使用
  • ✅ Spring Boot Starter:纯配置化,适合标准化场景

Bucket4j 凭借其高性能和灵活性,已成为 Java 领域限流方案的首选之一。建议根据项目复杂度选择合适的集成方式。

完整源码见 GitHub:https://github.com/eugenp/tutorials/tree/master/spring-boot-modules/spring-boot-libraries


原始标题:Rate Limiting a Spring API Using Bucket4j | Baeldung