现在很多网站都会添加反爬策略防止暴力破解或不间断爬取,同时为了不让系统因为短时间内大量并发而崩溃,都会添加一定的限流措施
常用限流算法
计数器算法
滑动窗口算法
漏桶算法
令牌桶算法
#####计数器算法
在固定窗口内对请求进行计数,然后与设置的最大请求数进行比较,如果超过了最大值,就进行限流。到达了一个固定时间窗口终点,将计数器清零。
比如设置每分钟最大请求100,那么在xx:xx:00都会清零计数器,随后每进来一个请求就将计数器加1,如果超过100,则拒绝请求。直到到达下一个时间窗口的开始。
特点:这种方式实现简单,但是无法临界时间问题,比如在01:00:00开始计数,在01:00:59突然收到90个请求,而在01:01:01又收到了90个请求。由于程序在01:01:00重置计数,导致在01:00:30-01:01:30这段时间虽然请求数超过了100,但也被允许了。
#####滑动窗口算法
基于计数器可能存在的时间临界问题,发明了改进版的滑动窗口算法。
如图,滑动窗口算法是将1分钟的时间分割成6份,每份10s。每过10s,指针就会向前移动一个10s。这样1分钟的时间内,每10s所指代的窗口都不一样。
这是如果块a和块b发送了大量请求,也能成功限制。因为此时的窗口已经滑动到了m窗口的位置,块a和块b已经保证在一个时间范围内了。
特点:滑动窗口虽然解决了部分时间临界问题,但是如果某个微小的时间内存在大量请求,也不能保证完全限制。
#####漏桶算法
设置一个用于装水的桶,桶下面有一个放水的洞。把请求比作水,水来了就先放到桶里,然后按照一定
的速率放出水。水龙头放水过快,桶里的水满了就会溢出。表现为请求就是多出的请求丢掉。
流入的请求速率是不确定,请求可以是任意速率流入桶中,流出的请求则是按照固定速率流出。因此也叫作流量“整形”,因为不管你流入有多快,流出都是固定速率。
特点:漏桶算法可以有效解决临界时间的问题,但是如果有大量请求可能会丢弃很多请求,无法应对突发请求
#####令牌桶算法
漏桶算法流入速度不稳定,流出速度是稳定的。
而令牌桶则时先获取令牌,然后再将令牌放入桶中,这样可以控制通过控制令牌产生的速度,而简介控制请求流入桶中的速度。
常用限流方案
1.前端限流
前端可以通过添加验证码(短信验证码或者手势验证码),增加暴力破解的难度。
某些按钮点击之后设置为禁用或者设置定时器延迟一段时间点击等防抖策略。
2.黑白名单
添加动态黑白名单,是大多数公司进行限流的常用手段,通过限制某段时间大量访问的IP地址防止恶意爬虫,但是由于许多用户使用的公网IP是相同的,所以可能会让一些正常用户中枪。
####3.网关限流
前端限流虽然可以在一定程序上解决某些问题,但是用户通过接口调用的方式访问就没有效果了,这时添加网关的限流方案就很有必要了。
nginx限流
nginx 提供两种限流方式,一是控制速率,二是控制并发连接数。
控制速率
ngx_http_limit_req_module
模块提供限制请求处理速率能力,使用了漏桶算法。控制单个IP的访问速率。
http{
limit_req_zone $binary_remote_addr zone=limit:10m rate=5r/s;
server /login {
limit_req zone=limit;
proxy_pass xxxx;
}
}
limit_req_zone
命令定义了一个zone,一般用于http块中
$binary_remote_add
中remote_add
是指IP地址,$binary_remote_addr
是指压缩后的IP地址,因为地址压缩后存放更省空间,加上它表示速率限制以IP为依据zone=limit:10m
表示压缩地址所占的空间限制在10m大小rate=10r/s
表示接收单个IP请求的速率是每秒10个,因为nginx是以毫秒计算的,所以可以看成是每100毫秒接收一个请求
limit_req
命令表示使用了http块中的zone,一般用于server中
zone=limit
命令表示使用名称为limit的zone
例如以下代码按照上面的配置,会被限制为每秒最高5个请求
@RequestMapping("/test1") public String test() { String ss = "test-1...,date:"+ DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()); System.out.println(ss); return "success"; }
limit_req
命令常和burst,delay命令一起使用。
http{
limit_req_zone $binary_remote_addr zone=limit:10m rate=10r/s;
server /login {
limit_req zone=limit burst=10;
proxy_pass xxxx;
}
}
burst=10
表示超出请求速率时,又发出了10个请求,那么这10个请求不会被拒绝,而是会放在队列里面,当队列满了,就拒绝请求。当前面的请求执行完后接着执行队列中的请求。所以可能会导致某些请求时间过长。
http{
limit_req_zone $binary_remote_addr zone=limit:10m rate=10r/s;
server /login {
limit_req zone=limit burst=10 nodelay;
proxy_pass xxxx;
}
}
nodelay
表示当超出请求速率时,入队列的10个请求立刻执行,不会延迟,同时标记队列的空间。其他请求全部拒绝,必须等待队列空间释放才能请求
控制并发数
ngx_http_limit_conn_module提供了限制并发连接数的能力
http {
limit_conn_zone $binary_remote_addr zone=my_conn:10m
limit_conn_zone $server_name zone=my_server:10m
server /login {
limit_conn my_conn 10;
limit_conn my_server 10;
proxy_pass xxxxx;
}
}
limit_conn_zone
命令同样定义了一个zone,一般用于http块
$binary_remote_add
中remote_add是指IP地址,$binary_remote_addr是指压缩后的IP地址,因为地址压缩后存放更省空间,加上它表示速率限制以IP为依据zone=my_conn:10m
表示压缩地址所占的空间限制在10m大小
limit_req
命令表示使用了http块中的zone,一般用于server中
my_conn 10
表示使用了名称为my_conn的zone块,并且最大连接数不超过10
例如以下代码,即使在发送100个请求,也会被限制为串行请求
@RequestMapping("/demo") public String test() throws InterruptedException { String ss = "start....date:" + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()); System.out.println(ss); TimeUnit.SECONDS.sleep(5); ss = "end.....date:" + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()); System.out.println(ss); return "success"; }
limit_req_zone
与limit_conn_zone
有什么不同?
limit_req_zone定义的是最大请求数,limit_conn_zone定义的是最大连接数。现在一个http连接一般都会定义keep-alive,也叫长连接存活时间,即在这个keep-alive周期内所有的http请求都会使用这一个连接。所以实现用limit_req_conn可以更好地控制限制请求。
gateway限流
gateway作为微服务网关,经常用于处理一些与业务相关的通用配置,比如鉴权,动态路由,自定义负载均衡,限流等。
gateway官方提供了RequestRateLimiterGatewayFilterFactory这个类,它使用了redis和lua脚本实现了令牌桶的限流方式 如何通过Gateway网关进行限流操作完整版代码
#bean配置 @Bean(value = "ipKeyResolver") KeyResolver ipKeyResolver() { return exchange -> { return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()); }; }
#application.yml配置 spring: cloud: gateway: routes: # 路由id,可以任意设置 - id: web-test uri: http://127.0.0.1:8100 predicates: - Path=/test/**,/demo/** filters: - name: RequestRateLimiter args: # 指定限流标志,与上面的bean对应 key-resolver: '#{@ipKeyResolver}' # 速率限流 redis-rate-limiter.replenishRate: 1 # 能容纳的并发流量总数 redis-rate-limiter.burstCapacity: 2
4.中间件限流
redis限流
redis也可以用来实现限流,例如防止商品超卖等。有以下几种实现方式
①基于Redis的setnx操作
我们可以通过setnx指令设置key的过期时间,比如限时活动。可以通过设置指定时间的key,当key过期后也就不能访问了,但是这种只能指定固定时间的key,比如当统计1-10秒的时候,无法统计2-11秒之内,而且还需要设置很多个key。
②基于Redis的zset操作
通过zset指令可以实现基于滑动窗口的限流算法。例如我们可以把所有的请求设置成同一个key,当请求过来的时候生成唯一的value值和当前时间作为score存进去。当新的请求来临时,先计算当前key中指定时间范围的score是否大于限流阈值,思路与延迟队列基本相同。if(redisTemplate.hasKey("limit")) { // intervalTime是限流的时间 Integer count = redisTemplate.opsForZSet().rangeByScore("limit", currentTime - intervalTime, currentTime).size(); if (count != null && count > 5) { return false; } } redisTemplate.opsForZSet().add("limit", UUID.randomUUID().toString(),currentTime); return true;
③基于Redis的令牌桶算法
基于redis的令牌桶算法使用的list数组类型,再通过定时任务定时向list中插入任务,模拟匀速生成令牌// 输出令牌 public Response limit(Long id){ Object result = redisTemplate.opsForList().leftPop("limit"); if(result == null){ return false; } return true; } // 以1S的速率定时向list中添加令牌 @Scheduled(fixedDelay = 1000, initialDelay = 0) public void tokenTask(){ redisTemplate.opsForList().rightPush("limit", UUID.randomUUID().toString()); }
5.程序限流
①guava包使用令牌桶的限流算法
它已经定义好了一系列的限流api供我们使用,首先引入包
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
guava提供实现限流的类是RateLimiter
RateLimiter提供了两种限流模式:
①普通的限流SmoothBursty。
②带预热的限流SmoothWarmingUp。即在指定预热期,允许放过的流量逐渐增加。预热期结束后,允许放过的流量就等于设定的限流值。这个目的是为了解决软件重启等情况,由于缓存等还没有初始化化,系统能够承受的流量比稳定运行后更小,防止在服务刚刚启动就被大流量打挂了。在预热时间内,生产令牌的速度只有设定的一半。
SimpleDateFormat dateFormat = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss");
// 使用普通限流器,0.5表示每秒生产令牌的速度
RateLimiter rateLimiter = RateLimiter.create(0.5);
// 使用带有预热的限流器,10表示预热时间
RateLimiter rateLimiter2 = RateLimiter.create(0.5, 10, TimeUnit.Second);
for (int i = 0; i < 100; i++) {
double aaa = rateLimiter.acquire();
System.out.println(aaa + "--" + dateFormat.format(new Date()));
}
查看RateLimiter使用详情 guava之限流RateLimiter
②使用锁来控制并发请求的速度,可以使用Semaphore 类来完成
Semaphore semaphore = new Semaphore(3);
CountDownLatch countDownLatch =new CountDownLatch(10);
Runnable runnable = () -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "---acquire");
TimeUnit.SECONDS.sleep(2);
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "---release");
semaphore.release();
countDownLatch.countDown();
}
};
for (int i = 0; i < 10; i++) {
new Thread(runnable).start();
}
countDownLatch.await();
除了以上几种限流方案外,也可以通过MQ完成限流功能。
阿里还推出了Sentinel限流熔断中间件,可以从请求速率,异常比例,访问控制等多个方面实现限流,通过控制台就可以实现了,非常方便。