常见限流算法的种类
- 计数器算法(固定窗口)
- 滑动窗口算法
- 漏桶算法
- 令牌桶算法
固定窗口算法
- 实现原理:
- 通过维护一段时间内的计数值,每当一个请求通过时就加1;当计数值超过预先设置的阈值时,就拒绝单位时间内的其他请求。如果单位时间已经结束就将计数值清零,开启下一轮计数。
- 弊端:
- 临界值问题。会发生在临界时短时间内处理高数量请求的问题。
- 假如我们设定1秒内允许通过的请求阈值是100,如果有用户在时间窗口的最后几毫秒发送100个请求,紧接着又在下一个时间窗口开始时发送100个请求,那么这个用户其实在1秒内就请求了200次,虽然超过阈值但不会限流。这就是临界值问题。
滑动窗口算法
滑动窗口算法是为了解决固定窗口算法的临界值的问题而诞生的,滑动窗口是基于是时间来划分窗口的。
- 实现原理:
- 滑动窗口算法是固定窗口算法的一个变种,开设我们还是设定1秒内允许通过200个请求,但是我们这里需要把1秒分成多个格子,假设是5个格子,每个格子就是200ms,每过200ms就会将窗口向前滑动一个格子。最终我们统计请求数的时候是将当前窗口的值进行累加,用得到的请求数判断是否需要限流。所以格数越多,统计越精确。
漏桶算法
- 实现原理:
- 为了消除“突刺现象”,采用漏桶算法进行限流。当请求进来的时候放到漏斗,然后从下端小口匀速流出。不管上面的流量多大,下面流出的速度始终保持不变。桶的容量是有上限的,如果桶满了,新进来的请求就会被丢弃。
- 算法实现:
- 可以准备一个队列作为漏桶,用来保存请求。另外通过一个线程池定期从队列中获取请求并执行,可以一次性获取多个并发执行。
- 弊端:无法应对短时间内的突发流量。
令牌桶算法
令牌桶是对漏桶算法的一种改进,漏桶算法能够限制调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还可以允许一定程度的突发调用。
- 实现原理:
- 在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定数量往桶中放令牌。每次调用需要先获取令牌,只有拿到令牌,才能继续执行。否则等待可用的令牌,或者直接拒绝。如果桶中的令牌达到上限,就丢弃令牌。所以存这种情况,如果桶中一直有大量的可用令牌,这时进来的请求就可以直接拿到令牌执行。比如设置QPS为100,限流器初始化完成1秒后,桶中就有100个令牌。这时服务还没有完全启动好,等启动完成对外提供服务时,该限流器可以抵挡顺势而为100个请求。所以只有当桶中没有令牌时,请求才会等待,最后以一定速率执行。
- 令牌桶相关概念
- 令牌流:流过令牌的管道。用于生成令牌放入令牌桶中;
- 数据流:进入系统的数据流量;
- 令牌桶:保存令牌的区域,可以理解为一个缓冲区:令牌保存在这里用于使用。
- 算法实现:
- 令牌桶会按照一定的速率生成令牌放入桶中。当访问要进入系统时,需要从令牌桶获取令牌,有令牌则可以进入,没有令牌则被抛弃。由于令牌桶的令牌是源源不断生成的,当访问量小时可以留存令牌到达令牌桶的上限。这样当段时间的突发访问量来时,积累的令牌数可以处理这个问题。当访问持续大流量流入时,由于生成令牌的速率是固定的。最后也就变成了类似漏桶算法的固定流量处理。
漏桶和令牌桶的对比
- 令牌桶允许一定程度的突发流量;漏桶主要是平滑流出速率;
- 令牌桶限制的是平均流入速率,允许一定的突发请求;漏桶限制的是常量流出速率,即流出速率是一个固定常量值
- 令牌桶是固定速率的令牌添加,而漏桶是固定速率的请求流出;
Guava的RateLimiter的令牌桶流控算法
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>29.0-jre</version>
<!-- or, for Android: -->
<version>29.0-android</version>
</dependency>
/*** 模拟RateLimiter限流 */
public class TestRateLimiter {
public static void main(String[] args) {
//0.5代表一秒最多多少个
RateLimiter rateLimiter = RateLimiter.create(0.5);
List<Runnable> tasks = new ArrayList<Runnable>();
for (int i = 0; i < 10; i++) {
tasks.add(new UserRequest(i));
}
ExecutorService threadPool = Executors.newCachedThreadPool();
for (Runnable runnable : tasks) {
System.out.println("等待时间:" + rateLimiter.acquire());
threadPool.execute(runnable);
}
}
private static class UserRequest implements Runnable {
private int id;
public UserRequest(int id) {
this.id = id;
}
public void run() {
System.out.println("userQuestID:"+id);
}
}
}