Spring Boot结合PostgreSQL使用Bucket4j实现用户限流的疑问
在无API网关且暂无添加计划的Spring Boot应用中,需针对特定接口按用户实现限流,具体场景如下:
- 通过JWT令牌获取用户名;
- 限制每个用户每日60次请求(阈值存储在数据库且可修改),需使用HandlerInterceptor;
- 将用户限流状态保存至PostgreSQL数据库(可在新请求时读取评估);
- 归档前一日状态信息(每日创建新状态)。
最初考虑使用resilience4j,后发现其不支持服务端;尝试Repose Rate limit也不符合需求,最终选定Bucket4j。但网上教程多雷同,官方文档缺乏详细说明,自行尝试编写了RateLimitingService、DataSourceConfig代码,并创建了buckets表,存在以下疑问:
- state字段应存储什么?已知令牌桶算法通常存储包含剩余令牌数、最后操作时间的哈希值;
- 若表仅包含BigInt类型id和state字段,如何标识用户?能否添加user_id字段及实现方式;
- 使用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

