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