固定时间窗口算法
实现原理:
固定时间内限制访问次数,如1秒内限制请求100个,如果超出阈值就拒绝其他请求。如果单位时间结束,则进入下一轮计数。
临界值问题:
如果在上一秒最后100ms内请求发了100个请求,下一秒前100ms内请求了100个请求,相当于一秒内请求了200个请求,超过了阈值但是没有被限流
滑动窗口算法
滑动窗口是为了解决固定窗口计数存在问题而诞生的,滑动窗口是基于时间来划分窗口的。
实现原理
滑动窗口算法是基于固定时间窗口算法的变种,在这里我们把1秒的时间分割成多格,例如分割为5格,没过200ms向前移动一格,如下图所示
每格都有请求数,当请求超出当前格的数量就会被限流
个人认为这种方式也没有完全解决固定窗口算法的问题,只是通过拆分成多个时间窗口加强精确度的目的
漏桶算法
为了解决“突刺现象”,可以采用漏桶算法实现限流,当请求进来,不管流量多大,下面的速度始终保持不变
突刺现象
指在一定时间内的一小段时间内就用完了所有资源,后大部分时间中无资源可用。
比如在限流方法中的计算器算法,设置1s内的最大请求数为100,在前100ms已经永远了100个请求,则后面900ms将无法处理请求,这就是突刺现象。
不管客户端请求多么不稳定,通过漏桶算法限流,每10ms处理一次请求,因为处理速度是固定的,请求的数量是未知的,未来得及处理的请求存放在桶内,超出桶容量上限,新进来的请求就丢弃。
实现方案
通过队列存储请求,另外通过线程池从队列获取请求并执行,可以一次性获取多个并发执行,但是存在无法在短时间内处理突发流量。
令牌桶算法
令牌桶算法是对漏桶算法的一种改进,漏桶算法能限制请求调用速率,令牌桶算法能在限制调用的平均速度同时允许一定程度的突发调用。
令牌桶算法中存在一个桶用于存储固定数量的令牌,算法机制以固定速率往桶中存放令牌。每次请求调用需要先去获取令牌,只有拿到令牌才有机会继续执行请求,否则等待获取可用令牌、或者直接拒绝请求。
如果桶内令牌达到上限,就丢弃令牌,当桶内有大量可用令牌时,这是进来的请求可以直接拿到令牌执行,++比如设置QPS为100,容器初始化完成一秒后,桶内就有100个令牌了,等服务启动完成对外提供服务时,就可以抵挡瞬时的100个请求,由于令牌发放算法,会匀速处理后续请求++
令牌桶算法是漏斗算法的改进版,解决漏桶算法无法短时间内处理突发流量的问题,令牌桶算法由三个部分组成令牌流、数据流、令牌桶.
- 令牌流:流通令牌的管道,用于生成令牌的流通,存放令牌桶中
- 数据流:进入系统的数据流量
- 令牌桶:存放令牌的区域,可以理解成缓存区:令牌保存在这里用于使用
算法原理:
令牌桶算法会按固定速率生成令牌放入令牌桶内,访问进入系统是,需要从令牌桶内获取到令牌,有令牌的可以进入,没有的被抛弃。由于令牌是不间断生成的,当请求量小于令牌发放速度时,令牌桶可以达到桶容量上线,短时间内突发大量访问时,积累的令牌可以处理这个问题。当访问量持续大量流入时,由于令牌生成的速度是固定的,最后也会变成类似漏桶算法的固定流量处理。
令牌桶和漏桶对比
- 令牌桶
按照固定速率往桶内添加令牌,请求需要获判断令牌是否足够,无令牌时拒绝新的请求。
平均流入速率,在空闲时间可以积累令牌,所以可以处理许突发请求。 - 漏桶
按照固定速率流出请求,流入的请求数累达到桶容量后,拒绝新流入请求。
固定流出速率。
Guava
Google Guava工程包含了若干被Google的 Java项目广泛依赖 的核心库,例如:集合 [collections] 、缓存 [caching] 、原生类型支持 [primitives support]、并发库 [concurrency libraries] 、通用注解 [common annotations] 、字符串处理 [string processing] 、I/O 等等。
Guava官方文档-RateLimiter类RateLimiter使用的是一种叫令牌桶的流控算法,RateLimiter会按照一定的频率往桶里扔令牌,线程拿到令牌才能执行,比如你希望自己的应用程序QPS不要超过1000,那么RateLimiter设置1000的速率后,就会每秒往桶里扔1000个令牌,RateLimiter经常用于限制对一些物理资源或者逻辑资源的访问速率。。
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>29.0-jre</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);
}
}
}
Spring Cloud GateWay 令牌桶限流
Spring Cloud Gateway 基于内部过滤器工厂RequestRateLimiterGateWayFilterFactory实现。
RequestRateLimiterGateWayFilterFactory依赖于Redis,需要引入spring-boot-starter-data-redis-reactive依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
server:
port: 8080
spring:
cloud:
gateway:
routes:
- id: limit_route
uri: http://httpbin.org:80/get
predicates:
- After=2019-02-26T00:00:00+08:00[Asia/Shanghai]
filters:
- name: RequestRateLimiter
args:
key-resolver: '#{@userKeyResolver}' #用于限流的解析器的Bean对象名字,使用SpEL表达式根据#{@beanName}从Spring容器中获取Bean对象
redis-rate-limiter.replenishRate: 50 # 令牌桶每秒速率
redis-rate-limiter.burstCapacity: 300 # 令牌桶总容量
application:
name: gateway-limiter
redis:
host: localhost
port: 6379
database: 0
@Configuration
public class KeyResolverConfiguration {
/*** 接口限流: * 获取请求地址的uri作为限流key。 */
@Bean
public KeyResolver pathKeyResolver() {
return new KeyResolver() {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getPath().toString());
}
};
}
/*** 用户限流: * 获取请求用户id作为限流key。 */
@Bean
public KeyResolver userKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("userId"));
}
/**
* IP限流: * 获取请求用户ip作为限流key。
*/
@Bean
public KeyResolver hostAddrKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
}