简介

bucket4j

bucket4j是基于令牌桶算法的Java限流库, 主页在https://github.com/vladimir-bukhtoyarov/bucket4j。 它主要用在3种场景:
a,限制比较重工作的速率。
b,将限流作为定时器,例如有些场景限制你对服务提供方的调用速度,因此使用限流器作为定时器,定时按照约定速率调用服务提供方。
c,限制对API访问速率。

令牌桶是一种限速算法,与之相对的是漏桶

  • 令牌限速

当进行任务的操作时,消耗一定的令牌,后台以一定的速率生产令牌。

在没有令牌的情况下,就阻塞任务,或者拒绝服务。

令牌的生产速率,代表了大部分情况下的平均流速。

  • 桶限峰值

的作用就是存储令牌,消耗的令牌都是从中获取。

桶的作用是用来限制流速的峰值,当桶中有额外令牌的时候,实际的流速就会高于限定的令牌生产速率。

假设令牌生产速率为v,桶大小为b,处理时间为t,则实际流量速度为V=v+b/t

  • 额外消耗

为了保证功能的完整,后台必须保证令牌生产,而且是持续服务,不能中断。

同时,为了功能的正确作用,当桶满了以后,后续生产的令牌会溢出,不会存储到桶内部。

使用

基本使用

  1. 消费
public static void main(String[] args) {
        Bandwidth limit = Bandwidth.simple(10, Duration.ofSeconds(1));
        Bucket bucket = Bucket4j.builder().addLimit(limit).build();
        if(bucket.tryConsume(1)){
            System.out.println("do something");
        }else{
            System.out.println("do nothing");
        }
    }



 

Bandwidth:带宽,也就是每秒能够通过的流量,自动维护令牌生产。

Bucket:桶,不论状态,或是令牌的消费,bucket是我们操作的入口。

tryConsume:尝试消费n个令牌,返回布尔值,表示能够消费或者不能够消费,给我们判断依据。

为了简单理解可以尝试一下如下代码。

public static void main(String[] args) {
        Bandwidth limit = Bandwidth.simple(1, Duration.ofSeconds(1));
        Bucket bucket = Bucket4j.builder().addLimit(limit).build();
        while(true){
            if(bucket.tryConsume(1)){
                System.out.println(new SimpleDateFormat("HH:mm:ss").format(new Date()));
            }else{
                try{
                    System.out.println("waiting...");
                    Thread.sleep(200);
                }catch (Exception e){
                }
            }
        }
    }



 

  1. 阻塞
public static void main(String[] args) throws InterruptedException {
        Bandwidth limit = Bandwidth.simple(1, Duration.ofSeconds(1));
        Bucket bucket = Bucket4j.builder().addLimit(limit).build();
        while(true){
            bucket.asScheduler().consume(1);
            System.out.println(new SimpleDateFormat("HH:mm:ss").format(new Date()));
        }
    }



 

运行一下就能够了解。

asScheduler会进行阻塞,直到获取令牌才进行后续语句的执行。

  1. 探针
public static void main(String[] args) throws InterruptedException {
        Bandwidth limit = Bandwidth.simple(5, Duration.ofSeconds(1));
        Bucket bucket = Bucket4j.builder().addLimit(limit).build();
        while(true){
            ConsumptionProbe probe = bucket.tryConsumeAndReturnRemaining(1);
            if(probe.isConsumed()){
                System.out.println(new SimpleDateFormat("HH:mm:ss").format(new Date())
                        +"\t剩余令牌: "+ probe.getRemainingTokens());
            }else{
                System.out.println("waiting...");
                Thread.sleep(200);
            }
        }
    }



 

tryConsumeAndReturnRemaining:获取探针

isConsumed:判断是否能消耗

getRemainingTokens:查询剩余令牌数量

  1. 桶控制
public static void main(String[] args) throws InterruptedException {
        long bucketSize = 9;
        Refill filler = Refill.greedy(2, Duration.ofSeconds(1));
        Bandwidth limit = Bandwidth.classic(bucketSize, filler);
        Bucket bucket = Bucket4j.builder().addLimit(limit).build();
        while(true){
            ConsumptionProbe probe = bucket.tryConsumeAndReturnRemaining(1);
            if(probe.isConsumed()){
                System.out.println(new SimpleDateFormat("HH:mm:ss").format(new Date())
                        +"\t剩余令牌: "+ probe.getRemainingTokens());
            }else{
                Thread.sleep(2000);
            }
        }
    }



 

Refiller:填充速度

bucketSize:桶容量

classic:用Refiller创建Bandwidth

可以观察到初始容量有10个,休眠两秒,每次消耗刚好四个,验证Refill功能。

稍微深入

  1. 初始化令牌数量
public static void main(String[] args) throws InterruptedException {
        long bucketSize = 9;
        Refill filler = Refill.greedy(2, Duration.ofSeconds(1));
        Bandwidth limit = Bandwidth.classic(bucketSize, filler).withInitialTokens(5);
        Bucket bucket = Bucket4j.builder().addLimit(limit).build();
        while(true){
            ConsumptionProbe probe = bucket.tryConsumeAndReturnRemaining(1);
            if(probe.isConsumed()){
                System.out.println(new SimpleDateFormat("HH:mm:ss").format(new Date())
                        +"\t剩余令牌: "+ probe.getRemainingTokens());
            }else{
                Thread.sleep(2000);
            }
        }
    }

 

 

withInitialTokens:桶初始化令牌数。

一般情况下,桶容量初始化时默认是满的,可以设置初始化时桶内的令牌数。

  1. 杜绝贪婪
public static void main(String[] args) throws InterruptedException {
        long bucketSize = 10;
        Refill filler = Refill.greedy(10, Duration.ofSeconds(1));
        Bandwidth limit = Bandwidth.classic(bucketSize, filler).withInitialTokens(0);
        Bucket bucket = Bucket4j.builder()
                .addLimit(limit)
                .build();
        while(true){
            if(bucket.tryConsume(1)){
                System.out.println(new SimpleDateFormat("HH:mm:ss:SSS").format(new Date()));
            }
        }
    }

 

 

运行一下,就能够大致了解Refill的贪婪了。

它总是那么急,我们的10/s,施行下去结果变成了1/ms

如果我们必须是瞬间生成10个,这就违背我们的意愿了。

Refill filler = Refill.intervally(10, Duration.ofSeconds(1));

这样,我们的意愿就不会因为贪婪而有所扭曲了。

  1. 手动添加
bucket.addTokens(500);

 

不等创建,自己手动添加,适用于回滚。

public static void main(String[] args) throws InterruptedException { long bucketSize = 10; Refill filler = Refill.intervally(10, Duration.ofSeconds(1)); Bandwidth limit = Bandwidth.classic(bucketSize, filler).withInitialTokens(0); Bucket bucket = Bucket4j.builder() .addLimit(limit) .build(); bucket.addTokens(500); while(true){ if(bucket.tryConsume(1)){ try{ throw new Exception("create a Exception"); } catch (Exception e) { bucket.addTokens(1); } } } }

  1. 时钟
  1. 粒度

从上面的贪婪,可以发现,Bucket4j的时钟是以毫秒进行衡量的,如果想要微秒之类的更细粒度操控,需要自己设置

// 毫秒 Bucket4j.builder().withMillisecondPrecision().build; // 微秒 Bucket4j.builder().withNanosecondPrecision().build()

withMillisecondPrecision:毫秒

withNanosecondPrecision:微秒

担心系统时钟不准,可以采用自己的时钟。

Bucket4j.builder().withCustomTimePrecision(new MyClock()).build()

withCustomTimePrecision:指定时钟

  1. 配置热替换
BucketConfiguration config = Bucket4j.configurationBuilder()
    .addLimit(Bandwidth.simple(1, Duration.ofSeconds(2)))
    .build();
bucket.replaceConfiguration(config);

时机到了,就可以更换配置。

业务繁忙的时候令牌带宽可以适当的放宽,没有请求的时候也可以适当的减缓令牌生成。

毕竟生成令牌也会占用资源不是。

  1. 波动令牌
Bucket bucket = Bucket4j.builder()
       .addLimit(Bandwidth.simple(1000, Duration.ofMinutes(1)))
       .addLimit(Bandwidth.simple(50, Duration.ofSeconds(1)))
       .build();

当添加多个BandWidth,都会生效。

考虑总体的满足,在情况允许的情况下会尽量满足全部的要求。

如例子,如果满足第二个条件,第一个条件必定会被打破,但是也不是一直不会满足第二个条件。

因此,在整体上不超过带宽,但是允许不全部占用的情况。

令牌的生成也不必时刻都是火力全开,为了满足全部的限制,有时候不得不消极怠工

这种波动,打破了平均生产的境况,从而允许动态速率的生成令牌。