1. 背景
1.1 为什么需要限流
- 大量正常用户高频访问导致服务器宕机
- 恶意用户高频访问导致服务器宕机
- 网页爬虫 ,对于这些情况我们需要对用户的访问进行限流访问
1.2 介绍
目前主流的限流算法:令牌、漏桶、滑动窗口。Nginx都实现了漏桶算法,Springcloud Gateway和Guava Ratelimiter实现了令牌桶,阿里的 Sentinel实现了滑动窗口。
如图所示,令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务,令牌桶算法通过发放令牌,根据令牌的rate频率做请求频率限制,容量限制等。
2. 源码
2.1 Guava RateLimiter如何实现令牌桶
令牌桶传统的思路是攒够了N个令牌后,再分配。但是Guava是先分配,然后将下次分配的时间移后。
举例说明:假设每秒可分配10个令牌,当前时间为2s,所以已存储了20个令牌,现在我要分配30个令牌,一种做法是等待1秒,在第3秒的时候攒够30个了,然后分配返回分配令牌;而Guava的做法是,直接返回,然后将下一个分配令牌的时间点定为第3秒。这两种情况都实现了3秒钟分配30个令牌的QPS需求。
2.1.1 如何申请令牌
public boolean tryAcquire(int permits, long timeout, TimeUnit unit) {
long timeoutMicros = max(unit.toMicros(timeout), 0);
checkPermits(permits);
long microsToWait;
synchronized (mutex()) {
// 秒表,也就是类似跑步用的秒表计数器,用来统计自开始掐秒表后过去了多少时间,可以重置秒表归0
long nowMicros = stopwatch.readMicros();
// 判断是否可以在timeoutMicros时间范围内获取令牌,公式:下次令牌可领取时间(相对时间)-超时时间>秒表时间
if (!canAcquire(nowMicros, timeoutMicros)) {
return false;
} else {
// 获取令牌,并返回需要等待的毫秒数
microsToWait = reserveAndGetWaitLength(permits, nowMicros);
}
}
// 先获取令牌,再等待
stopwatch.sleepMicrosUninterruptibly(microsToWait);
return true;
}
2.1.2 谁发令牌
申请的时候会每次重新同步,将秒表的时间与下次可领取令牌的时间进行对比
void resync(long nowMicros) {
// if nextFreeTicket is in the past, resync to now
if (nowMicros > nextFreeTicketMicros) {
// 计算该时间差里令牌的产生数量,coolDownIntervalMicros为每个令牌产生需要的时间
double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
// 这里是为了防止超出最大限制
storedPermits = min(maxPermits, storedPermits + newPermits);
nextFreeTicketMicros = nowMicros;
}
}
3. 实战
4. FAQ
4.1 Guava令牌的发放形式将下次分配的时间移后,那会不会无限超前消费?
4.2 从源码看,Guava 先获取令牌再进行等待,这跟先等待,再获取令牌有什么区别?
4.3 guava限流用的synchronized,这样性能是不是值得考究?