限流 是对某一时间窗口内的请求数进行限制,保证系统的可用性和稳定性,防止因流量暴增而导致的系统运行慢或宕机。常用的限流算法有令牌桶和漏桶算法,Google的Guava中的RateLimiter使用令牌桶。
开发高并发系统,保护系统手段:缓存、降级和限流。
1、缓存:提升系统访问速度和增大系统处理容量。
2、降级:对非核心服务和页面进行降级,主动关闭部分功能,释放服务器资源,保证核心业务可用。
3、限流:对并发访问/请求限速、给出一个限流个数控制洪峰。
令牌桶和漏桶算法区别:
1、内容上:令牌桶算法是按固定速率生成令牌,请求能从桶中拿到令牌就执行,否则执行失败。漏桶算法是任务进桶速率不限,但是出桶的速率是固定的,超出桶大小的的任务丢弃,也就是执行失败。
2、突发流量适应性上:令牌桶是允许一次取出多个token,只要有令牌就可以处理任务,在桶容量上允许处理突发流量。令牌桶限制的是流入的速率且灵活。而漏桶算法出桶的速率固定,有突发流量也只能按流出的速率处理任务。漏桶算法是平滑流入的速率,限制流出速率。
图示:
1、按固定速率生成令牌。需判断桶满状态。
2、桶:存放token
3、request过来从桶里取token,取到token处理,否则丢弃返回失败。
(单机的限流,是JVM级别的限流,所有的令牌生成都是在内存中):
RateLimiter rateLimiter = RateLimiter.create(2);//每秒生成两个permits(token)
//用给定的吞吐量创建一个RateLimiter,通常是QPS。
//1次尝试获取1个permits,拿不到立即返回false(超时时间0s)
rateLimiter.tryAcquire()
//一次可以拿多个permits,是阻塞的直到一个permit可用。
rateLimiter.acquire(int nums)
与juc中的semaphore有区别,信号量获取acquire之后需要释放release。令牌不需要释放。
RateLimiter是一个抽象类,限流器有两个实现类:1、SmoothBursty;2、SmoothWarmingUp
SmoothBursty是以稳定的速度生成permit。SmoothWarmingUp是渐进的生成,最终达到最大值趋于稳定。
偿还机制:当前请求的债务(请求的令牌大于限流器存储的令牌数)由下一个请求来偿还(上个请求亏欠的令牌,下个请求需要等待亏欠令牌生产出来以后才能被授权)acquire多个token时生效。
RateLimiter中重要参数:
1、stableIntervalMircos //稳定生成令牌的时间间隔。
2、maxBurstSeconds //1秒生产的令牌。
3、maxPermits //最大存储令牌数。
4、nextFreeTicketMicros //下个请求可被授权令牌的时间(不管请求多少令牌),实现当前债务由下一个请求来偿还机制关键。
5、storedPermits //已存储的令牌,生产过剩的令牌存储小于等于maxPermits,是应对突发流量的请求的关键。
//从RateLimiter中获取一个permit,阻塞直到请求可以获得为止。
public double acquire(){
Return acquire(1);
}
//从RateLimiter中获取指定数量的permits,阻塞直到请求可以获得为止
public double acquire(int permits) {
//计算获得这些数量需等待时间
long microsToWait = reserve(permits);
//不可被打断的等待
stopwatch.sleepMicrosUninterruptibly(microsToWait);
//单位转换为秒
return 1.0 * microsToWait / SECONDS.toMicros(1L);
}
//预订给定数量的permits来使用,计算需要这些数量permits等待时间。
final long reserve(int permits) {
//校验负数
checkPermits(permits);
//抢占锁,这里的锁使用单例模式获得
synchronized (mutex()) {
//计算等待时间
return reserveAndGetWaitLength(permits, stopwatch.readMicros());
}
}
//具体计算等待时间的逻辑(继承上一次债务,并且透支本次所需要的所有permits)
//注意这里返回的是时间点
final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
// 同步时间轴
resync(nowMicros);
// 继承上次债务
long returnValue = nextFreeTicketMicros;
// 跟桶内存储量比,本次可以获取到的permit数量,如果存储的permit大于本次需要的permit数量则此处是0,否则是一个正数
double storedPermitsToSpend = min(requiredPermits, this.storedPermits);
// 还缺少的permits数量
double freshPermits = requiredPermits - storedPermitsToSpend;
// 计算需要等待的时间(微秒)
long waitMicros = storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend) + (long) (freshPermits * stableIntervalMicros);
// 继承上一次债务时间点+这次需要等待的时间,让下一次任务去等待
this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros);
// 减去本次消费的permit数
this.storedPermits -= storedPermitsToSpend;
// 本次只需要等待到上次欠债时间点即可
return returnValue;
}
参考:
1、https://www.cnblogs.com/cjsblog/p/9379516.html有令牌桶源码分析
2、https://blog.csdn.net/chen888999/article/details/82254694 有时序图
3、https://www.jianshu.com/p/226c7907905c 帮助以上做理解
最后强调:
1、单机的限流,是JVM级别的限流,所有的令牌生成都是在内存中
2、令牌桶可以应对一定程度突发流量,漏桶则不行。因为一次可取多个storedPermits,取出permits数量不受限。