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

基于用户与transaction-id的Bucket4j限流方案咨询及可行性确认

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 16:10:55