简介
bucket4j
bucket4j是基于令牌桶算法的Java限流库, 主页在https://github.com/vladimir-bukhtoyarov/bucket4j。 它主要用在3种场景:
a,限制比较重工作的速率。
b,将限流作为定时器,例如有些场景限制你对服务提供方的调用速度,因此使用限流器作为定时器,定时按照约定速率调用服务提供方。
c,限制对API访问速率。
令牌桶
是一种限速算法,与之相对的是漏桶
。
- 令牌限速
当进行任务的操作时,消耗一定的
令牌
,后台以一定的速率生产
令牌。在没有令牌的情况下,就阻塞任务,或者拒绝服务。
令牌的生产速率,代表了大部分情况下的平均流速。
- 桶限峰值
桶
的作用就是存储令牌,消耗的令牌都是从桶
中获取。桶的作用是用来限制流速的峰值,当桶中有额外令牌的时候,实际的流速就会高于限定的令牌生产速率。
假设令牌生产速率为
v
,桶大小为b
,处理时间为t
,则实际流量速度为V=v+b/t
。
- 额外消耗
为了保证功能的完整,后台必须保证
令牌生产
,而且是持续服务,不能中断。同时,为了
桶
功能的正确作用,当桶满了以后,后续生产的令牌会溢出
,不会存储到桶内部。
使用
基本使用
- 消费
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){
}
}
}
}
- 阻塞
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
会进行阻塞,直到获取令牌才进行后续语句的执行。
- 探针
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
:查询剩余令牌数量
- 桶控制
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
功能。
稍微深入
- 初始化令牌数量
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
:桶初始化令牌数。一般情况下,桶容量初始化时默认是满的,可以设置初始化时桶内的令牌数。
- 杜绝贪婪
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));
这样,我们的意愿就不会因为
贪婪
而有所扭曲了。
- 手动添加
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); } } } }
- 时钟
- 粒度
从上面的
贪婪
,可以发现,Bucket4j
的时钟是以毫秒进行衡量的,如果想要微秒
之类的更细粒度操控,需要自己设置// 毫秒 Bucket4j.builder().withMillisecondPrecision().build; // 微秒 Bucket4j.builder().withNanosecondPrecision().build()
withMillisecondPrecision
:毫秒
withNanosecondPrecision
:微秒担心系统时钟不准,可以采用自己的时钟。
Bucket4j.builder().withCustomTimePrecision(new MyClock()).build()
withCustomTimePrecision
:指定时钟
- 配置热替换
BucketConfiguration config = Bucket4j.configurationBuilder()
.addLimit(Bandwidth.simple(1, Duration.ofSeconds(2)))
.build();
bucket.replaceConfiguration(config);
时机到了,就可以更换配置。
业务繁忙的时候令牌带宽可以适当的放宽,没有请求的时候也可以适当的减缓令牌生成。
毕竟生成令牌也会占用资源不是。
- 波动令牌
Bucket bucket = Bucket4j.builder()
.addLimit(Bandwidth.simple(1000, Duration.ofMinutes(1)))
.addLimit(Bandwidth.simple(50, Duration.ofSeconds(1)))
.build();
当添加多个
BandWidth
,都会生效。考虑总体的满足,在情况允许的情况下会尽量满足全部的要求。
如例子,如果满足第二个条件,第一个条件必定会被打破,但是也不是一直不会满足第二个条件。
因此,在整体上不超过
带宽
,但是允许不全部占用
的情况。令牌的生成也不必时刻都是
火力全开
,为了满足全部的限制,有时候不得不消极怠工
。这种波动,打破了
平均
生产的境况,从而允许动态速率
的生成令牌。