令牌桶算法简介:
令牌桶算法是限流算法的常见实现 能够起到限流作用,允许一定程度的流量突发
令牌桶算法在限流组件中被广泛使用,如guava,sentinal等
令牌桶算法原理:
- 令牌桶按固定速率往桶里放入令牌,令牌桶满则丢弃
- 系统接收用户请求时,从桶里拿出令牌,如果能拿到就通过,否则拒绝用户请求
由于桶容量的设计,当有突发流量到达时,令牌可以马上被取走,读者学习漏桶算法后,可以对比理解为什么令牌桶可以处理一定程度的流量突发
令牌桶的初步设计分析及简单实现
本文以两个最小demo入门令牌桶,对于生产实践级别的令牌桶限流实践,除了令牌桶的算法实现,需要考虑限流粒度,降级方法等
常见的限流算法还有漏桶,滑动窗口等,本文不再赘述
令牌桶,顾名思义,需要有令牌,桶,同时还需要控制放入桶里的令牌速率,数量等
因此最简单的令牌桶应该有以下参数
- 桶容量
- 令牌放入速率
- 当前令牌数量
对于每秒固定放入令牌,主流的限流组件不是依赖于每秒去填充令牌,而是基于时间差去动态计算应该生成的令牌数量
我的理解是与其每秒都去放入令牌(依赖于定时器,有cpu损耗,无效填充等问题)
不如请求到达时再去补充令牌,只要保证请求到达时,他看到的令牌数量是准确的即可
同时,需要考虑并发情况,对于令牌的填充和获取,需要处理其并发,每秒都填充令牌的策略,需要为其设置锁
流量较低时,锁会影响填充效率 流量较高时,锁竞争过大,影响并发性能,以及容易引起上下文切换
因此本文demo采用时间差动态计算应该生成的令牌数量
最小demo实现
java
/**
* @author omenkk7
* @description 简单令牌桶实现
* @create 2025/10/8
*/
public class TokenBucketLimiter {
//桶容量
private long bucketCapacity;
//每秒放入多少个令牌
private long refillRate;
//当前令牌容量
private long tokenValue;
//上次取令牌时间
private long lastGetTokenTime;
public TokenBucketLimiter(long bucketCapacity,long refillRate){
this.bucketCapacity=bucketCapacity;
this.refillRate=refillRate;
this.tokenValue=bucketCapacity;
this.lastGetTokenTime=System.nanoTime();
}
//取出令牌
boolean tryGetToken(){
fillToken();
//先尝试放入令牌
if(tokenValue>0){
tokenValue--;
return true;
}
return false;
}
//填充令牌
void fillToken(){
long l = System.nanoTime();
long time=(l-lastGetTokenTime)/1_000_000_000L;
if(time>0){
long min=Math.min(bucketCapacity-tokenValue,time*refillRate);
tokenValue+=min;
lastGetTokenTime=l;
}
}
}
并发问题分析及优化
在上述实现中,没有考虑并发问题.tokenValue的修改不是原子操作,可能会导致并发问题,我们要对tokenValue的修改做出并发控制
如果并发量非常高,我们可以采用加悲观锁的形式 直接在tryGetToken()方法上加锁即可
如果并发量不是很高,可以采用cas自旋来解决并发问题,从而避免悲观锁带来的线程上下文切换等问题
代码demo
java
import java.util.concurrent.atomic.AtomicLong;
/**
* @author omenkk7
* @description 简单令牌桶实现
* @create 2025/10/8
*/
public class TokenBucketLimiter {
//桶容量
private long bucketCapacity;
//每秒放入多少个令牌
private long refillRate;
//当前令牌容量
private AtomicLong tokenValue;
//上次取令牌时间
private AtomicLong lastGetTokenTime;
public TokenBucketLimiter(long bucketCapacity, long refillRate) {
this.bucketCapacity = bucketCapacity;
this.refillRate = refillRate;
this.tokenValue=new AtomicLong(bucketCapacity);
this.lastGetTokenTime.set(System.nanoTime());
}
//取出令牌
boolean tryGetToken() {
fillToken();
//先尝试放入令牌
while (true){
long curValue = tokenValue.get();
if (curValue < 0) {
return false;
} else if(tokenValue.compareAndSet(curValue, curValue - 1)){
return true;
}
}
}
//填充令牌
void fillToken() {
long l = System.nanoTime();
long time = (l - lastGetTokenTime.get()) / 1_000_000_000L;
if (time < 0)return;
while (true) {
long curValue = tokenValue.get();
long adjustValue = Math.min(bucketCapacity - curValue, time * refillRate);
if(adjustValue<=0)return ;
//cas修改
if(tokenValue.compareAndSet(curValue,curValue+adjustValue))return;
//重试
}
}
}
通过cas自旋的无锁编程,相比悲观锁
并发情况下,自旋次数过多可能带来cpu损耗
减少了线程的上下文切换:悲观锁在竞争时会导致线程上下文切换
提高并发性:多线程可以同时对共享资源进行操作,提高了并发性
如上的AtomicLong就是采用了util.concurrent.atomic包下的原子类;
通过无锁编程提高了效率
总结
令牌桶算法作为限流算法的经典实现,能够起到限流作用,允许一定程度的流量突发
本文简单介绍了令牌桶算法的简单实现和基础原理
除此之外,还需要了解
- 限流粒度
- 降级策略
- 并发下的线程安全及优化