Spring Cloud Gateway 升级与 Bucket4j 限流实践

从 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 依赖变更

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 连接集成:

java 复制代码
@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);
    }
}

关键设计决策:

  1. 复用 Lettuce 连接 :不额外创建 Redis 连接池,直接从 LettuceConnectionFactory 获取底层 RedisClusterClient,避免连接资源浪费
  2. CAS-Based Builder:使用 CAS(Compare-And-Swap)模式,每次令牌操作只需一次 Redis 交互,性能优于事务模式
  3. 自动过期:配置 10 秒过期策略,令牌耗尽后 Bucket 对象自动清理,减少 Redis 内存占用

3.2 KeyResolver:限流维度的核心

我们实现了两种 KeyResolver,分别对应不同的限流粒度:

PathUserIdKeyResolver(精细化限流)

java 复制代码
// Key 格式: {routerId.path.userId}
// 示例: {route-A./api/v1/asset/query.12345}
// 效果: 每个用户对每个接口独立计算配额

UserIdKeyResolver(合并限流)

java 复制代码
// Key 格式: {routerId.userId}
// 示例: {route-A.12345}
// 效果: 同一路由下多个接口共享配额

Redis Cluster 兼容 :Key 使用 {} Hash Tag 包裹,确保同一用户的不同限流 Key 落在 Redis Cluster 的同一 Slot,避免跨 Slot 操作。

3.3 429 限流通知过滤器

当触发 429 限流时,通过 GlobalFilter 捕获并发送告警通知:

java 复制代码
@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 路由配置示例

yaml 复制代码
# 日配额 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

四、双限流器并行架构

升级后,网关同时使用两种限流器,各司其职:

css 复制代码
请求 → Gateway
  │
  ├─ redisRateLimiter(秒级并发)
  │    └─ 适用:页面初始化、配置类接口
  │    └─ 配置:replenishRate=10, burstCapacity=20
  │
  └─ Bucket4jRateLimiter(日配额)
       ├─ A组:100 次/天
       ├─ B组:50 次/天
       └─ C组:30 次/天

为什么不全切 Bucket4j?

redisRateLimiter 的秒级限流在防止突发冲击(如脚本刷接口)上反应更快,秒级窗口内就能拦截。而 Bucket4j 的日配额更侧重于长期用量控制。两种机制互补,形成"短并发 + 长配额"的双重防护。


五、已知限制:多带宽(Multi-Bandwidth)不支持

什么是多带宽?

Bucket4j 核心库支持在一个 Bucket 中配置多个 Bandwidth(带宽),例如同时配置:

java 复制代码
// 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 参数接收限流器实例,其配置模型是单组参数

yaml 复制代码
bucket4j-rate-limiter.capacity: 50
bucket4j-rate-limiter.refill-tokens: 50
bucket4j-rate-limiter.refill-period: 1d

bucket4j-spring-boot-starterBucket4jRateLimiter 实现只解析一组 capacity / refill-tokens / refill-period 参数,没有提供多 Bandwidth 的配置入口

目前的应对策略

多带宽无法通过路由拆分来绕过------多个路由各自独立计算配额,无法在同一个 Bucket 内同时生效"分钟级 + 天级"的复合约束。

因此我们放弃了周限额的业务需求,仅实现日限额 。对秒级并发敏感的接口则通过 redisRateLimiter 补充防护,两种限流器各管各的维度,互不重叠。

如果未来 bucket4j-spring-boot-starter 支持多带宽配置,可以在一个路由上同时配置短周期 + 长周期的复合限流策略,届时再补齐周限额需求。


六、总结

这次升级的核心动机不是"追求新版本",而是业务需求驱动 :日配额限流是产品侧提出的硬性需求,RedisRateLimiter 在架构上无法满足。

改动总结:

  1. 版本升级是 Bucket4j 集成的前置条件,非为升级而升级
  2. 双限流器并行:秒级并发(RedisRateLimiter)+ 日配额(Bucket4j)互补
  3. Key 设计兼顾精细化控制和 Redis Cluster 兼容
  4. 429 告警通过 GlobalFilter 实时通知运维
  5. 多带宽是目前 Bucket4j Gateway 集成的已知限制,需等待社区支持

相关推荐
Flittly6 分钟前
【AgentScope Java新手村系列】(14)人机交互
java·spring boot·spring
RainCity10 分钟前
Java Swing 自定义组件库分享(十二)
java·笔记·后端
吃饱了得干活16 小时前
Spring Cloud Gateway 微服务网关:路由、断言、过滤器
java·spring cloud
lwx5728018 小时前
探秘InnoDB:搞懂它的内存、线程、磁盘与日志刷盘策略
java·后端
Flynt19 小时前
从Spring Boot 4.0升到4.1,我在Maven和gRPC上栽了跟头
java·spring boot·后端
plainGeekDev20 小时前
Activity 间传值 → Navigation 参数
android·java·kotlin
plainGeekDev20 小时前
onActivityResult → ActivityResult API
android·java·kotlin
Sunia20 小时前
《AgentX 专栏》10-生产部署:3台2C4G云服务器把企业级Agent真正跑起来的完整方案
java·架构
ZhengEnCi21 小时前
J7A-高级Java工程师面试三道灵魂拷问-深度广度与工程素养的终极检验
java·后端
狼爷2 天前
吃透 Java Function 接口,搞定 99% 的 Stream 场景
java·函数式编程