一、概述
当系统的访问量增大的时候,一般会往这么几个方向上去优化:缓存,扩容,降级,限流。这里将聊一聊限流,限流其实可以看做是服务降级的一种实现。当请求超过系统的负载后,多余的请求做降级处理,怎么降级呢?就是限流,直接快速失败,不进行业务处理。现实生活中也有这种思想,比如高速公路车流量很大的时候,会将高速公路入口关闭,或者每隔一段时间放行一定的车辆,防止大量车辆进入给交通带来更大的压力,这就是一种限流的思想。
二、固定窗口
原理
固定窗口限流是限制在一段时间内的请求次数,当这一段时间过去之后,下一个一段时间才可以请求,但同样限制了下一个时间段内的请求次数。比如,限制1分钟访问10次,假设当前时间是9:00:00,那么在9:01:00之间允许10个请求,在9:01:00 ~ 9:02:00允许10个请求,依次类推,每一个一分钟的时间窗口内允许10次请求,可以看到这个时间窗口是一段一段。
实现
固定窗口的实现比较简单,借助Redis,以限流接口为key,设置1分钟的过期时间,执行Redis的incr方法,如果返回值大于限流次数,则快速失败,否则放行。
问题
这会带来突刺的问题,比如在9:01:59的时候突然请求了10次,在9:02:00也请求了10次,那么实际上在这两秒内的请求达到了20。在上一个限流窗口的尾部和下一个限流窗口的开头,容易出现突刺,导致局部时间范围内qps偏高。如下图所示。
三、滑动窗口
原理
上面的固定窗口容易出现突刺的问题,滑动窗口可以限制在任意的一段时间内,qps不超过预设值。假设有一个n秒的滑动窗口,它可以在时间轴上任意滑动,且保持滑动窗口内的请求数量不超过预设值x。
实现
借助Redis的zset,可以以限流接口为key,value为一个唯一值,score为时间戳。每次进入限流方法时,进行滑动窗口的整理,根据时间,也就是score,移除已经不在窗口范围内的元素,然后判断大小,如果窗口内元素的个数没有超过限制,那么允许访问,否则限流。参考代码如下。
package wanghang.tools.limit;
import wanghang.tools.util.RedisUtil;
import java.time.LocalTime;
import java.util.Random;
/**
* createTime 2020/11/22 16:43
* 滑动窗口限流
* 限制在x秒内能执行n次操作
*
* @author wanghang
* @version v1.0
*/
public class SlideWindowLimiter{
/**
* 滑动窗口大小,n毫秒的窗口
*/
private int slideWindowSize;
/**
* 滑动窗口内允许的操作数
*/
private int actionLimit;
public SlideWindowLimiter(int slideWindowSize, int actionLimit) {
this.slideWindowSize = slideWindowSize;
this.actionLimit = actionLimit;
}
/**
* 比如限制一个用户在30秒内只能评论10次。如果出现并发,这个实现是不可靠的。
* 在redis中创建一个zset,key为uid + "comment",即每个用户在评论这个操作下对应一个zset,value为该用户评论这个行为标识,
* 可以是用户id+行为+时间戳。
* score是时间戳。表示当前评论时间。
* 注意zset需要设置一个大于30秒的过期时间,长时间无评论操作,可视为用户下线,释放存储空间。
* @param key key
* @return 是否允许操作
*/
public synchronized boolean isAllowed(String key) {
long ts = System.currentTimeMillis();
makeSpace(key, ts);
if (size(key) < actionLimit) {
recordAction(key, ts);
return true;
}
return false;
}
/**
* 记录用户行为
*
* @param key key
* @param ts ts
*/
private void recordAction(String key, long ts)