Spring Cloud Gateway 升级与 Bucket4j 限流实践

4 阅读6分钟

从 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 令牌桶库,核心优势:

能力RedisRateLimiterBucket4j
补充周期固定秒级任意 Duration(秒/分/时/天/周/月)
补充策略固定 GreedyGreedy / Interval / IntervallyAligned
Redis Key2 个(tokens + timestamp)1 个(序列化 Bucket 对象)
分布式支持Lua 脚本多后端(Redis / Hazelcast / Ignite 等)
多带宽配置不支持支持(但 Gateway 集成暂不支持)

Bucket4j 的 refill-period 参数直接支持 1d(一天),完美匹配日配额需求。


二、版本升级:改了什么?

2.1 核心版本变更

组件升级前升级后
Spring Boot3.4.13.5.9
Spring Cloud2024.0.02025.0.1
Spring Cloud Alibaba-2025.0.0.0
Spring Cloud Gateway4.1.x4.4.3
Gateway Starterspring-cloud-starter-gatewayspring-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);
    }
}

关键设计决策:

  1. 复用 Lettuce 连接:不额外创建 Redis 连接池,直接从 LettuceConnectionFactory 获取底层 RedisClusterClient,避免连接资源浪费
  2. CAS-Based Builder:使用 CAS(Compare-And-Swap)模式,每次令牌操作只需一次 Redis 交互,性能优于事务模式
  3. 自动过期:配置 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-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 集成的已知限制,需等待社区支持