从 Spring Cloud Gateway 4.1.x 升级到 4.4.3,引入 Bucket4j 实现日配额限流,解决内置 RedisRateLimiter 无法支持天级限流的痛点。
一、背景:为什么需要升级?
1.1 原有方案:RedisRateLimiter 的局限
Spring Cloud Gateway 内置的 RedisRateLimiter 基于令牌桶算法,通过 Lua 脚本在 Redis 中实现秒级并发控制,配置参数为 replenishRate(每秒补充令牌数)和 burstCapacity(桶容量)。
这个方案在控制瞬时并发上表现优秀,但存在一个关键限制:它的补充周期固定为秒级,无法配置更长的周期。
我们在网关中面对的核心需求是:
- 日配额控制:限制单个用户每天调用敏感接口的总次数(如每天 300 次),而非仅控制每秒的并发数
- 多级限流:不同接口组需要不同的配额策略(A组 100 次/天、B组 50 次/天、C组 30 次/天)
- 精细化维度:按"路由 + 路径 + 用户 ID"或"路由 + 用户 ID"两个维度限流
这些需求在 RedisRateLimiter 的秒级模型下根本无法实现。
1.2 为什么选 Bucket4j?
Bucket4j 是一个成熟的 Java 令牌桶库,核心优势:
| 能力 | RedisRateLimiter | Bucket4j |
|---|---|---|
| 补充周期 | 固定秒级 | 任意 Duration(秒/分/时/天/周/月) |
| 补充策略 | 固定 Greedy | Greedy / Interval / IntervallyAligned |
| Redis Key | 2 个(tokens + timestamp) | 1 个(序列化 Bucket 对象) |
| 分布式支持 | Lua 脚本 | 多后端(Redis / Hazelcast / Ignite 等) |
| 多带宽配置 | 不支持 | 支持(但 Gateway 集成暂不支持) |
Bucket4j 的 refill-period 参数直接支持 1d(一天),完美匹配日配额需求。
二、版本升级:改了什么?
2.1 核心版本变更
| 组件 | 升级前 | 升级后 |
|---|---|---|
| Spring Boot | 3.4.1 | 3.5.9 |
| Spring Cloud | 2024.0.0 | 2025.0.1 |
| Spring Cloud Alibaba | - | 2025.0.0.0 |
| Spring Cloud Gateway | 4.1.x | 4.4.3 |
| Gateway Starter | spring-cloud-starter-gateway | spring-cloud-starter-gateway-server-webflux |
2.2 升级原因
原因一:Gateway Starter 更名
Spring Cloud Gateway 从 4.2.x 开始,将 spring-cloud-starter-gateway 重命名为 spring-cloud-starter-gateway-server-webflux。如果不升级,无法使用 Bucket4j 的 Spring Boot Starter 集成。
原因二:Bucket4j Spring Boot Starter 兼容性
bucket4j-spring-boot-starter 0.13.0 要求 Spring Boot 3.5.x + Spring Cloud Gateway 4.3+ 以上版本。旧版本的 Gateway 内部 API 不兼容。
原因三:引入 properties-migrator
Spring Boot 3.5.x 部分配置属性发生了变更(如 spring.cloud.gateway 下一些属性重命名),引入 spring-boot-properties-migrator 可以在启动时自动报告废弃/迁移的配置项,避免运行时踩坑。
2.3 升级改动清单
pom.xml 依赖变更
<!-- 版本升级 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.9</version> <!-- 原 3.4.1 -->
</parent>
<properties>
<spring-cloud.version>2025.0.1</spring-cloud.version> <!-- 原 2024.0.0 -->
<spring-cloud-alibaba.version>2025.0.0.0</spring-cloud-alibaba.version>
</properties>
<!-- Gateway Starter 更名 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway-server-webflux</artifactId>
<!-- 原 spring-cloud-starter-gateway -->
</dependency>
<!-- 新增:属性迁移助手 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-properties-migrator</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 新增:Bucket4j 分布式限流 -->
<dependency>
<groupId>com.bucket4j</groupId>
<artifactId>bucket4j_jdk17-lettuce</artifactId>
<version>8.14.0</version>
</dependency>
<dependency>
<groupId>com.giffing.bucket4j.spring.boot.starter</groupId>
<artifactId>bucket4j-spring-boot-starter</artifactId>
<version>0.13.0</version>
</dependency>
配置文件适配
Spring Boot 3.5 + Spring Cloud 2025 的部分配置属性路径有调整,需要根据 properties-migrator 的启动日志逐一修改。
三、Bucket4j 集成:代码实现
3.1 限流配置类:RateLimitConfiguration
核心是将 Bucket4j 与 Gateway 已有的 Redis Lettuce 连接集成:
@Configuration
public class RateLimitConfiguration {
private static final String KEY_PREFIX = "request_bucket_limiter.";
@Bean
public AsyncProxyManager<String> stringKeyAsyncProxyManager(
LettuceConnectionFactory connectionFactory) {
// 复用 Gateway 已有的 Redis Cluster 连接
RedisClusterClient client = (RedisClusterClient) connectionFactory.getNativeClient();
StatefulRedisClusterConnection<String, byte[]> connection =
client.connect(RedisCodec.of(StringCodec.UTF8, ByteArrayCodec.INSTANCE));
return Bucket4jLettuce.casBasedBuilder(connection.async())
// 令牌桶过期策略:10秒后自动清理
.expirationAfterWrite(ExpirationAfterWriteStrategy
.basedOnTimeForRefillingBucketUpToMax(Duration.ofSeconds(10)))
.requestTimeout(timeout)
.build()
.asAsync()
// 所有 Key 自动添加前缀
.withMapper(key -> KEY_PREFIX + key);
}
}
关键设计决策:
- 复用 Lettuce 连接:不额外创建 Redis 连接池,直接从
LettuceConnectionFactory获取底层RedisClusterClient,避免连接资源浪费 - CAS-Based Builder:使用 CAS(Compare-And-Swap)模式,每次令牌操作只需一次 Redis 交互,性能优于事务模式
- 自动过期:配置 10 秒过期策略,令牌耗尽后 Bucket 对象自动清理,减少 Redis 内存占用
3.2 KeyResolver:限流维度的核心
我们实现了两种 KeyResolver,分别对应不同的限流粒度:
PathUserIdKeyResolver(精细化限流)
// Key 格式: {routerId.path.userId}
// 示例: {route-A./api/v1/asset/query.12345}
// 效果: 每个用户对每个接口独立计算配额
UserIdKeyResolver(合并限流)
// Key 格式: {routerId.userId}
// 示例: {route-A.12345}
// 效果: 同一路由下多个接口共享配额
Redis Cluster 兼容:Key 使用 {} Hash Tag 包裹,确保同一用户的不同限流 Key 落在 Redis Cluster 的同一 Slot,避免跨 Slot 操作。
3.3 429 限流通知过滤器
当触发 429 限流时,通过 GlobalFilter 捕获并发送告警通知:
@Component
public class RateLimitEagleGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange)
.doFinally(signalType -> {
HttpStatusCode statusCode = exchange.getResponse().getStatusCode();
if (Objects.equals(statusCode, HttpStatus.TOO_MANY_REQUESTS)) {
sendNotificationAsync(exchange);
}
});
}
// 优先级最高,确保包裹所有业务过滤器
@Override
public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; }
}
设计要点:
- 使用
doFinally确保在任何响应路径下都能触发通知 - 设置
HIGHEST_PRECEDENCE优先级,作为最外层过滤器包裹所有业务逻辑 - 异步发送通知,不阻塞请求响应
3.4 路由配置示例
# 日配额 500 次
- id: route-bucket4j-standard-500
uri: lb://backend-service
predicates:
- Path=/api/v1/chart/**
- Path=/api/v1/hold/**
filters:
- name: RequestRateLimiter
args:
key-resolver: "#{@pathUserIdKeyResolver}"
rate-limiter: "#{@bucket4jRateLimiter}"
bucket4j-rate-limiter.requested-tokens: 1
bucket4j-rate-limiter.capacity: 50
bucket4j-rate-limiter.refill-tokens: 50
bucket4j-rate-limiter.refill-period: 1d
bucket4j-rate-limiter.refill-type: INTERVALLY_ALIGNED
四、双限流器并行架构
升级后,网关同时使用两种限流器,各司其职:
请求 → Gateway
│
├─ redisRateLimiter(秒级并发)
│ └─ 适用:页面初始化、配置类接口
│ └─ 配置:replenishRate=10, burstCapacity=20
│
└─ Bucket4jRateLimiter(日配额)
├─ A组:100 次/天
├─ B组:50 次/天
└─ C组:30 次/天
为什么不全切 Bucket4j?
redisRateLimiter 的秒级限流在防止突发冲击(如脚本刷接口)上反应更快,秒级窗口内就能拦截。而 Bucket4j 的日配额更侧重于长期用量控制。两种机制互补,形成"短并发 + 长配额"的双重防护。
五、已知限制:多带宽(Multi-Bandwidth)不支持
什么是多带宽?
Bucket4j 核心库支持在一个 Bucket 中配置多个 Bandwidth(带宽),例如同时配置:
// Bucket4j 核心库原生支持
Bucket bucket = Bucket.builder()
.addLimit(Bandwidth.builder().capacity(10).refillGreedy(10, Duration.ofMinutes(1)).build()) // 每分钟10次
.addLimit(Bandwidth.builder().capacity(1000).refillGreedy(1000, Duration.ofDays(1)).build()) // 每天1000次
.build();
这样可以实现"每分钟最多 10 次,每天最多 1000 次"的复合限流策略。
为什么不支持?
Spring Cloud Gateway 的 RequestRateLimiter 过滤器通过 rate-limiter 参数接收限流器实例,其配置模型是单组参数:
bucket4j-rate-limiter.capacity: 50
bucket4j-rate-limiter.refill-tokens: 50
bucket4j-rate-limiter.refill-period: 1d
bucket4j-spring-boot-starter 的 Bucket4jRateLimiter 实现只解析一组 capacity / refill-tokens / refill-period 参数,没有提供多 Bandwidth 的配置入口。
目前的应对策略
多带宽无法通过路由拆分来绕过——多个路由各自独立计算配额,无法在同一个 Bucket 内同时生效"分钟级 + 天级"的复合约束。
因此我们放弃了周限额的业务需求,仅实现日限额。对秒级并发敏感的接口则通过 redisRateLimiter 补充防护,两种限流器各管各的维度,互不重叠。
如果未来 bucket4j-spring-boot-starter 支持多带宽配置,可以在一个路由上同时配置短周期 + 长周期的复合限流策略,届时再补齐周限额需求。
六、总结
这次升级的核心动机不是"追求新版本",而是业务需求驱动:日配额限流是产品侧提出的硬性需求,RedisRateLimiter 在架构上无法满足。
改动总结:
- 版本升级是 Bucket4j 集成的前置条件,非为升级而升级
- 双限流器并行:秒级并发(RedisRateLimiter)+ 日配额(Bucket4j)互补
- Key 设计兼顾精细化控制和 Redis Cluster 兼容
- 429 告警通过 GlobalFilter 实时通知运维
- 多带宽是目前 Bucket4j Gateway 集成的已知限制,需等待社区支持