在某些时候,我们需要对请求进来的流量进行控制,也就是我们常说的限流或削峰。
1. 为什么要限流
一个系统,面临大数据量,高并发的访问时,将出现无法提供服务,或请求响应超时等情况,严重的,在连锁反应下导致系统崩溃。这时候,对请求进行限流就很有必要了。通过限流,当请求达到一定的并发数或速率,就进行让请求等待、排队、降级、拒绝服务等,平缓进入系统的请求数峰值。
2. 常用的限流算法
常用的限流算法有两种,分别是漏桶算法,令牌桶算法。
-
漏桶算法
漏桶算法是将请求放入到漏桶中,桶中的请求按照一定的速率出去。突发大流量请求,经过漏桶算法控制后,将以恒定的速率进入网络。 -
令牌桶算法
固定容量的令牌桶可自行以恒定的速率产生令牌。如果令牌不被消耗,或者被消耗的速度小于产生的速度,桶内的令牌累积,直到把桶填满。后面再产生的令牌就会从桶中溢出抛弃。请求进来时,需要先从令牌桶中获取到令牌,方能被系统处理及响应。否则,被直接拒绝服务。
总结:
漏桶算法直接强制限制了请求的速率,无论多大的并发请求数,都会以恒定的速率出现。当一个系统的请求处理速率大于漏桶算法的限制速率时,面对突发的大流量,将会出现大量请求被拒绝,而系统利用率低的情况,在这个时候,漏桶算法就不合适了。此时,就要用令牌桶算法。令牌桶在容量固定,在请求量平缓且低于令牌生成速率(令牌生成速率及令牌桶容量要根据系统的处理能力设置)时,令牌桶是满的,面对突发的大流量,令牌桶容量大小的请求数能获取到令牌,降低流量峰值的同时,避免大量请求被丢弃。请求流量平缓进入系统,直到与令牌生成速率持平。
两种算法原理是不同的,但都能达到限流的目的,我们可以结合实际场景使用。比如,当调用的第三方系统处理能力有限,且没有相应的限流机制,这个时候,有突发大流量,为了第三方系统的稳定,就必须抛弃掉多余的流量。使用漏桶算法,刚好可以达到这个目的。相反,作为被调用方,在无法预测有多大的流量进入己方系统时,就可以使用令牌桶算法限流,即使在大流量请求到来,也能平缓过渡。
3. 限流实例
Google开源工具包Guava提供了限流工具类RateLimiter,该类基于令牌桶算法来完成限流,我们可以导入相应的包进行使用。
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>20.0</version>
</dependency>
3.1 调用代码:
3.2 限流工具类RateLimiter:
/**
* Acquires the given number of permits from this {@code RateLimiter} if it can be obtained
* without exceeding the specified {@code timeout}, or returns {@code false} immediately (without
* waiting) if the permits would not have been granted before the timeout expired.
* 如果没有超过延时时间,能够从令牌桶获取到指定数量的令牌就返回true;
* 或者在延时结束时还没有获取到令牌,就返回false(没有指定延时时间 timeout,就马上返回false)
* @param permits the number of permits to acquire
* @param timeout the maximum time to wait for the permits. Negative values are treated as zero.
* @param unit the time unit of the timeout argument
* @return {@code true} if the permits were acquired, {@code false} otherwise
* @throws IllegalArgumentException if the requested number of permits is negative or zero
*/
public boolean tryAcquire(int permits, long timeout, TimeUnit unit) {
long timeoutMicros = max(unit.toMicros(timeout), 0);
// 入参校验
checkPermits(permits);
long microsToWait;
// 同步块,解决并发获取令牌的情况
synchronized (mutex()) {
long nowMicros = stopwatch.readMicros();
// 马上能获取到令牌或者在延时时间内能获取到令牌 canAcquire(nowMicros, timeoutMicros)
if (!canAcquire(nowMicros, timeoutMicros)) {
return false;
} else {
// 预定下一张令牌,返回下一张令牌生成需等待的时间
microsToWait = reserveAndGetWaitLength(permits, nowMicros);
}
}
stopwatch.sleepMicrosUninterruptibly(microsToWait);
return true;
}