1.学习准备
RateLimiter 是基于令牌桶算法实现的限流框架,在学习框架源码钱先复习令牌桶算法
令牌桶算法
- 有一个固定容量的桶存放令牌
- 桶初始化是空的,以固定速率向桶填充令牌,当达到容量时令牌背丢弃
- 当请求到来时,从桶中移除一个令牌
缺陷:令牌桶只控制令牌入桶速度,不控制令牌消耗速度,允许一定的突发流量被瞬间处理
再看看其他几个过载保护算法
计数器算法
- 设置一个计数器count, 接收一个请求就将计数器加一,同时记录当前时间
- 判断当前时间和上次统计时间是否为同一分钟,如果是判断count是否超出阈值,超出阈值禁止访问,如果不是同一分钟把Count重置,重新计数
缺陷:如果两次大量请求在一次时间临界点附近,会造成短时间内超过阈值现象,可能导致后端过载
滑动窗口算法
弥补了计数器算法的缺陷,把时间间隔划分成更小的粒度,当更小粒度的时间间隔过去后,把过去的时间间隔请求数减掉再补充一个空的时间间隔
- 一个时间窗口1分钟,滑动窗口分为10个格子,每个格子6秒
- 每过6秒,滑动窗口右移动一个格子
- 每个格子都有独立的计数器
- 如果时间窗口内所有的计数器之和超过限流阈值触发限流操作
漏桶算法
假设存在一个漏桶,往桶里加水的速率是任意的,但桶里往外流水的速率是固定的。
- 定义一个固定大小的队列,请求进来就入队代表加水,队列满了就拒绝请求
- 准备一个线程池按固定速率从队列中取数据
2.方法测试
写了一个测试类实验谷歌令牌桶限流器,让二十个线程准备就绪后获取令牌
{
public RateLimiter createRateLimiter(Integer permitsPerSecond){
return RateLimiter.create(permitsPerSecond);
}
public static void main(String[] args) {
RateLimiterTest rateLimiterTest = new RateLimiterTest();
//构建一个一秒生成20个令牌的桶
RateLimiter rateLimiter = rateLimiterTest.createRateLimiter(20);
//提前准备好20个令牌
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//准备一个栅栏让请求同时进入
CyclicBarrier barrier = new CyclicBarrier(20, () -> {
System.out.println("线程准备就绪");
});
for (int num = 0; num < 20; num ++ ){
Thread thread = new Thread(() -> {
try{
//20个线程准备就绪后放开
barrier.await();
//请求获取令牌
if(rateLimiter.tryAcquire()){
System.out.println("进来一个请求");
}else {
System.out.println("请求被限流");
}
}catch (BrokenBarrierException | InterruptedException be){
System.out.println("系统异常");
}
});
thread.start();
}
}
}
3.源码分析
static RateLimiter create(SleepingStopwatch stopwatch, double permitsPerSecond) {
RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */);
rateLimiter.setRate(permitsPerSecond);
return rateLimiter;
}
RateLimiter初始化函数里指定了具体的实现类,绑定一个计时器,设置了每秒生成令牌数permitsPerSecond, permitsPerSecond中初始化了一些关键信息。其中setRate方法比较关键,初始化了下一个令牌的生成微秒时间戳。
final void doSetRate(double permitsPerSecond, long nowMicros) {
resync(nowMicros);
double stableIntervalMicros = SECONDS.toMicros(1L) / permitsPerSecond;
this.stableIntervalMicros = stableIntervalMicros;
doSetRate(permitsPerSecond, stableIntervalMicros);
}
方法外部有一个同步锁和参数校验不多做赘述,方法会首先执行一个resync函数,这个函数主要初始化了当前桶内令牌数以及洗一个令牌的生成微秒时间戳
void resync(long nowMicros) {
// if nextFreeTicket is in the past, resync to now
// 初始化时nextFreeTicketMicros为默认值0,会进入判断
if (nowMicros > nextFreeTicketMicros) {
double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
//此时的maxPermits为0,因此当前时间桶内令牌数也是0
storedPermits = min(maxPermits, storedPermits + newPermits);
// nowMicros大于nextFreeTicketMicros可知当前已有令牌生成,重置nestFreeTicketMicros为当前微秒时间戳
nextFreeTicketMicros = nowMicros;
}
}
回到doSetRate方法,此时计算一秒内生成permitsPerSecond需要多少微妙,由stableIntervalMicros保存,最后实际的SmoothBursty.doSetRate方法,这个方法计算了桶内最大令牌数,因为桶时间默认为1秒所以最大令牌书也就等于permitsPerSecond, 且初始化时桶内令牌数storedPermits为0,SmoothBursty.doSetRate源码如下
void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
double oldMaxPermits = this.maxPermits;
maxPermits = maxBurstSeconds * permitsPerSecond;
if (oldMaxPermits == Double.POSITIVE_INFINITY) {
// if we don't special-case this, we would get storedPermits == NaN, below
storedPermits = maxPermits;
} else {
storedPermits =
(oldMaxPermits == 0.0)
? 0.0 // initial state
: storedPermits * maxPermits / oldMaxPermits;
}
}
整个初始化到这里结束,后面开始分析从桶中获取令牌的方法。
rateLimiter.tryAcquire()
这个方法获取到令牌返回true,还可选有等待时间的获取令牌方法,这里只追踪没有等待时间的获取令牌。
public boolean tryAcquire(int permits, long timeout, TimeUnit unit) {
//没有等待时间这里timeoutMicros为0
long timeoutMicros = max(unit.toMicros(timeout), 0);
//校验获取令牌数,这里permits为1
checkPermits(permits);
long microsToWait;
//一个同步锁避免并发干扰令牌生产时间
synchronized (mutex()) {
//拿到当前微妙时间戳
long nowMicros = stopwatch.readMicros();
//检查能否获取令牌,实际上里面就是nextFreeTicketMicros-timeoutMicros<=nowMicros,这里timeoutMicros为0
if (!canAcquire(nowMicros, timeoutMicros)) {
return false;
} else {
//拿到令牌后需要更新nextFreeTicketMicros时间
microsToWait = reserveAndGetWaitLength(permits, nowMicros);
}
}
stopwatch.sleepMicrosUninterruptibly(microsToWait);
return true;
}
这里有一个同步锁,因此获取令牌以及更新令牌生成时间都是单线程执行。
@Override
final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
//重新同步桶内令牌数
resync(nowMicros);
//获取上一次的最新令牌生成时间
long returnValue = nextFreeTicketMicros;
//获取令牌更新需要扣减的令牌数,如果桶内令牌为0则扣减为0不允许扣减
double storedPermitsToSpend = min(requiredPermits, this.storedPermits);
//使用待扣减令牌数减去可扣减令牌数求出差值
double freshPermits = requiredPermits - storedPermitsToSpend;
//用上面求出的差值计算出下次令牌生成时间,这里实际上是预扣减了,因此新令牌生成时间需要把预扣减的令牌生成使劲按补回来
long waitMicros =
storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
+ (long) (freshPermits * stableIntervalMicros);
//求出新令牌生成时间,除在限流初始化第一次限流开始或桶内令牌长期被消耗为0时,freshPermits都为0
this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros);
//扣减桶内令牌
this.storedPermits -= storedPermitsToSpend;
//返回下一次令牌生成时间
return returnValue;
}
上面这个方法就是获取令牌后,维护令牌生成时间和桶内令牌数的核心方法。可以看到谷歌的令牌桶算法使用了同步块和一些计算实现了令牌桶算法。我们项目中也使用了这个框架进行限流,但这个操作只能限制单节点请求流量,因此如果系统瓶颈是400我们有4个节点,那配置需要配置为100。后面还有分布式的限流框架,以后有时间再水一篇帖子。