基于用户与transaction-id的Bucket4j限流方案咨询及可行性确认
核心逻辑拆解
需求2本质是双层嵌套限流规则,Bucket4j的多令牌桶(Bandwidth)+ 自定义分组键特性可以完美覆盖:
- 规则A:同一用户+同一
transaction-id→ 复用需求1的「每分钟最多15次请求」 - 规则B:同一用户+不同
transaction-id→ 每小时最多允许使用5个新的transaction-id(仅首次调用新transaction-id时消耗该配额)
分步实现
1. 定义限流带宽规则
先声明两个独立的带宽配置,对应上述两个规则:
// 规则A:用户+transaction-id维度,每分钟15次请求 Bandwidth perTxPerMinute = Bandwidth.classic(15, Refill.intervally(15, Duration.ofMinutes(1))); // 规则B:用户维度,每小时最多5个新transaction-id Bandwidth perUserNewTxPerHour = Bandwidth.classic(5, Refill.intervally(5, Duration.ofHours(1)));
2. 维护双层Bucket缓存
用线程安全的哈希表存储两组Bucket,分别对应不同的限流维度:
// 缓存:键为「用户名:transaction-id」,值为对应Bucket(处理规则A) private final ConcurrentHashMap<String, Bucket> userTxBuckets = new ConcurrentHashMap<>(); // 缓存:键为「用户名」,值为对应Bucket(处理规则B) private final ConcurrentHashMap<String, Bucket> userNewTxBuckets = new ConcurrentHashMap<>();
3. 接口限流逻辑实现
在目标@GetMapping接口中,按顺序执行限流检查:
@GetMapping("/target-api") public ResponseEntity<?> handleRequest( @RequestParam("username") String username, @RequestParam("transaction-id") String transactionId ) { String userKey = username; String userTxKey = String.format("%s:%s", username, transactionId); // 检查规则B:首次使用新transaction-id时消耗用户配额 if (!userTxBuckets.containsKey(userTxKey)) { // 获取或创建用户维度的Bucket Bucket userNewTxBucket = userNewTxBuckets.computeIfAbsent(userKey, k -> Bucket.builder().addLimit(perUserNewTxPerHour).build() ); // 配额不足则直接拦截 if (!userNewTxBucket.tryConsume(1)) { return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS) .body("Blocked: 每小时最多使用5个不同的transaction-id"); } // 创建该transaction-id对应的Bucket并存入缓存 userTxBuckets.putIfAbsent(userTxKey, Bucket.builder().addLimit(perTxPerMinute).build() ); } // 检查规则A:同一transaction-id的每分钟限流 Bucket txBucket = userTxBuckets.get(userTxKey); if (!txBucket.tryConsume(1)) { return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS) .body("Blocked: 该transaction-id每分钟最多15次请求"); } // 执行业务逻辑 return ResponseEntity.ok("OK"); }
4. 可选:缓存过期优化
为避免长期闲置的Bucket占用内存,可改用Guava Cache替代ConcurrentHashMap,自动清理过期条目:
// 用户+transaction-id维度的缓存,1小时未使用自动清理 private final LoadingCache<String, Bucket> userTxBuckets = CacheBuilder.newBuilder() .expireAfterAccess(1, TimeUnit.HOURS) .build(new CacheLoader<>() { @Override public Bucket load(String key) { return Bucket.builder().addLimit(perTxPerMinute).build(); } }); // 用户维度的缓存,1小时未使用自动清理 private final LoadingCache<String, Bucket> userNewTxBuckets = CacheBuilder.newBuilder() .expireAfterAccess(1, TimeUnit.HOURS) .build(new CacheLoader<>() { @Override public Bucket load(String key) { return Bucket.builder().addLimit(perUserNewTxPerHour).build(); } });
示例场景验证
针对你给出的调用序列:
invokeAPI("ey12==", "t1") OK → 首次用t1,消耗用户Bucket1个令牌,t1的Bucket消耗1个
invokeAPI("ey12==", "t2") OK → 首次用t2,消耗用户Bucket1个令牌,t2的Bucket消耗1个
invokeAPI("ey12==", "t1") OK → t1已存在,仅消耗t1的Bucket1个
invokeAPI("ey12==", "t1") OK → 同上
invokeAPI("ey12==", "t3") OK → 首次用t3,消耗用户Bucket1个令牌,t3的Bucket消耗1个
invokeAPI("ey12==", "t1") OK → 同上
invokeAPI("ey12==", "t4") OK → 首次用t4,消耗用户Bucket1个令牌,t4的Bucket消耗1个
invokeAPI("ey12==", "t5") OK → 首次用t5,消耗用户Bucket最后1个令牌,t5的Bucket消耗1个
invokeAPI("ey12==", "t6") Blocked → 用户Bucket无令牌可消耗,直接拦截
完全符合预期规则。
内容的提问来源于stack exchange,提问作者SoT

