You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot结合PostgreSQL使用Bucket4j实现用户限流的疑问

问题描述

在无API网关且暂无添加计划的Spring Boot应用中,需针对特定接口按用户实现限流,具体场景如下:

  • 通过JWT令牌获取用户名;
  • 限制每个用户每日60次请求(阈值存储在数据库且可修改),需使用HandlerInterceptor;
  • 将用户限流状态保存至PostgreSQL数据库(可在新请求时读取评估);
  • 归档前一日状态信息(每日创建新状态)。

最初考虑使用resilience4j,后发现其不支持服务端;尝试Repose Rate limit也不符合需求,最终选定Bucket4j。但网上教程多雷同,官方文档缺乏详细说明,自行尝试编写了RateLimitingService、DataSourceConfig代码,并创建了buckets表,存在以下疑问:

  1. state字段应存储什么?已知令牌桶算法通常存储包含剩余令牌数、最后操作时间的哈希值;
  2. 若表仅包含BigInt类型id和state字段,如何标识用户?能否添加user_id字段及实现方式;
  3. 使用Bucket4j是否属于过度设计?是否应自行实现限流(但担心重复造轮子)。

尝试代码

RateLimitingService代码

@Service
@RequiredArgsConstructor
public class RateLimitingService {
    private final DataSource dsService;

    private final Map<UUID, Bucket> bucketCache = new ConcurrentHashMap<UUID, Bucket>();
    private final UserPlanMappingRepository userPlanMappingRepository;

    public Bucket resolveBucket(final UUID userId) {
        Bucket t = bucketCache.computeIfAbsent(userId, this::newBucket);
        return t;
    }

    public void deleteIfExists(final UUID userId) {
        bucketCache.remove(userId);
    }

    private Bucket newBucket(UUID userId) {

        final var plan = userPlanMappingRepository.findByUserIdAndIsActive(userId, true).get().getPlan();
        final Integer limitPerHour = plan.getLimitPerHour();

        Long key = 1L;
        PostgreSQLadvisoryLockBasedProxyManager proxyManager = new PostgreSQLadvisoryLockBasedProxyManager(new SQLProxyConfiguration(dsService));

        BucketConfiguration bucketConfiguration = BucketConfiguration.builder()
                .addLimit(Bandwidth.classic(limitPerHour, Refill.intervally(limitPerHour, Duration.ofHours(1))))
                .build();

        return proxyManager.builder().build(key, bucketConfiguration);

    }
}

DataSourceConfig代码

@Configuration
@AllArgsConstructor
public class DataSourceConfig {

    Environment env;

    @Bean(name = "dsService")
    @Primary
    public DataSource createDataSourceService() {

        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName(env.getProperty("spring.jpa.database-platform"));
        dataSource.setUrl(env.getProperty("spring.datasource.url"));
        dataSource.setUsername(env.getProperty("spring.datasource.username"));
        dataSource.setPassword(env.getProperty("spring.datasource.password"));

        return dataSource;
    }
    
}

建表SQL

CREATE TABLE IF NOT EXISTS buckets (
  id BIGINT PRIMARY KEY,
  state BYTEA
);

解决方案

1. state字段存储内容

Bucket4j的state字段是令牌桶的序列化二进制数据,包含令牌桶核心状态:剩余令牌数量、最后一次令牌补充的时间戳、令牌 refill 的规则元数据等。它不是哈希值,而是Bucket4j内部用Java序列化(或自定义序列化器)生成的字节流,用于在分布式环境或重启后恢复令牌桶状态。

你不需要手动解析或修改这个字段,Bucket4j会自动负责序列化和反序列化。如果需要自定义序列化方式,可以配置SQLProxyConfiguration指定序列化器,比如用Jackson代替默认的Java序列化。

2. 添加user_id标识用户及实现方式

原表结构无法区分用户,必须添加user_id字段(建议用UUID或与用户表一致的ID类型),同时调整表的主键设计以适配每日归档需求:

修改后建表SQL

CREATE TABLE IF NOT EXISTS buckets (
  id BIGSERIAL PRIMARY KEY,
  user_id UUID NOT NULL, -- 关联用户ID
  state BYTEA NOT NULL,
  created_date DATE NOT NULL DEFAULT CURRENT_DATE, -- 标记状态所属日期,用于归档
  CONSTRAINT uk_buckets_user_date UNIQUE (user_id, created_date) -- 确保同一用户每日仅一条状态记录
);

代码调整

改用JdbcBucketManager适配自定义表结构,同时结合日期实现每日令牌桶重置:

private Bucket newBucket(UUID userId) {
    LocalDate today = LocalDate.now();
    // 初始化JDBC桶管理器,指定自定义表和列名
    JdbcBucketManager bucketManager = JdbcBucketManager.builder()
            .withDataSource(dsService)
            .withTableName("buckets")
            .withUserKeyColumn("user_id")
            .withCreationTimestampColumn("created_date")
            .build();

    // 获取用户有效限流计划
    var plan = userPlanMappingRepository.findByUserIdAndIsActive(userId, true)
            .orElseThrow(() -> new RuntimeException("用户无有效限流配置"));
    
    // 配置每日60次的限流规则
    BucketConfiguration bucketConfiguration = BucketConfiguration.builder()
            .addLimit(Bandwidth.classic(plan.getDailyLimit(), Refill.intervally(plan.getDailyLimit(), Duration.ofDays(1))))
            .build();

    // 获取当日用户的令牌桶,不存在则自动创建
    return bucketManager.getBucket(
            BucketKey.of(userId, today),
            () -> bucketConfiguration
    );
}

原代码中PostgreSQLadvisoryLockBasedProxyManager适合基于PG advisory锁的分布式场景,但自定义表结构更推荐JdbcBucketManager,它支持自定义列名和表结构,更灵活适配归档需求。

3. Bucket4j是否过度设计?

不是过度设计,反而能帮你避免重复造轮子:

  • 令牌桶算法的细节(比如令牌补充的精度、并发场景下的线程安全、状态持久化/恢复)都需要仔细处理,自行实现容易出现边界问题(比如并发请求下的令牌数计算错误、状态持久化的一致性问题);
  • Bucket4j支持分布式场景、自定义持久化(JDBC/Redis等)、动态调整限流规则,刚好匹配你的需求(阈值存数据库可修改、每日归档);
  • 如果你自行实现,后续需要扩展(比如添加不同用户组的限流规则、分布式部署)会非常麻烦,而Bucket4j已经提供了成熟的扩展能力。

如果担心复杂度,可以简化使用:比如先用本地缓存+JDBC持久化的方式,后续按需扩展分布式锁或Redis存储。


内容的提问来源于stack exchange,提问作者SpaceSloth

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 01:01:13