1.固定窗口算法
分别使用代码,guava cache和redis进行实现
代码思想:
(1)定义时间窗口,限流阙值,记录的窗口时间,当前请求量四个参数
(2)逻辑处理:获取当前时间与记录的窗口时间比较,如果大于时间窗口,证明窗口已经过期,重新赋值:记录的窗口时间,当前的请求量。如果小于情况下,证明满足时间要求,判断是否已经达到限流阙值,达到拒绝,不打到通过并请求量+1.
guava cache代码思想:
(1)利用guava cache自动淘汰机制,设置最大容量,设置过期时间,同时load方法重新。
(2) 获取当前时间的值,如果+1之后比限流阙值大情况下,拒绝任务;比限流阙值小的情况下,通过任务
redis思想:
(1)利用String结构,setNx命令,设置key为限流名称,value代表流量个数,同时设置过期时间为时间窗口大小
(2)方法判断key是否存在,不存在情况下初始化key,value;存在情况下,通过key获取数量,如果数量+1比流量阙值大情况下,拒绝请求;否则进行+1操作。
代码如下:
/**
* 固定窗口限流算法
* 核心思想:在一个时间窗口内达到的并发量
*/
public class FixedWindows {
//数量限制
private static int limit = 100;
//时间窗口 1000ms
private static Long windowUnit = 1000L;
//时间
private static Long time = 0L;
//当前的请求数
private static int curRequest = 0;
/**
* 固定窗口函数,使用synchronized保证线程安全
* 1.获取当前时间与之前窗口时间比较,大的情况下证明过期了,重新初始化变量
* 2.小的情况下,判断请求数是否比限制数大,大拒绝,小同意
*/
public static synchronized void fixedWindows() {
Long curTime = System.currentTimeMillis();
//判断当前窗口是否过期
if(curTime-time>windowUnit) {
//过期处理
time = curTime;
curRequest=1;
System.out.println("1.正常通过请求");
return;
}
//在窗口内处理
curRequest++;
if(curRequest>limit) {
System.out.println("2.限流了,拒绝请求");
return;
}
System.out.println("3.正常通过请求");
}
//guavaCache实现限流
static LoadingCache<Long, AtomicInteger> cache = CacheBuilder.newBuilder()
.maximumSize(100)
.expireAfterAccess(1,TimeUnit.SECONDS)
.build(new CacheLoader<Long, AtomicInteger>() {
@Override
public AtomicInteger load(Long key) throws Exception {
AtomicInteger a =new AtomicInteger(0);
System.out.println(a.intValue());
return a;
}
});
public void fixedWindowsGuavaCache() throws ExecutionException {
//秒级
Long time = System.currentTimeMillis()/1000;
if(cache.get(time).incrementAndGet()>limit) {
System.out.println("拒绝任务");
}else{
System.out.println("接收任务");
}
}
@Autowired
RedisTemplate<String,Object> redisTemplate;
//redis实现固定窗口限流
//数据结构为String key为限流key名称,value代表次数,过期时间为固定窗口
//1.判断key是否存在,不存在情况下,进行初始key,value,并设置过期时间
//2.key存在情况下,获取key的数量,和限制的值进行比较,不满足情况下+1,满足拒绝任务
public void fixedWindowsRedis() {
//先判断key是否存在
if(redisTemplate.hasKey("limit")) {
int sum = (int) redisTemplate.opsForValue().get("limit");
if(sum+1>limit) {
//拒绝任务
System.out.println("拒绝任务");
}else{
//+1操作
redisTemplate.opsForValue().increment("limit",1);
System.out.println("正常接收任务");
}
}else{
//使用什么结构 String
redisTemplate.opsForValue().setIfAbsent("limit",1);
//1s
redisTemplate.expire("limit",1, TimeUnit.SECONDS);
System.out.println("正常接收任务");
}
}
public static void main(String[] args) {
for(int i=0;i<101;i++) {
new Thread(new Runnable() {
@Override
public void run() {
fixedWindows();
}
}).start();
}
}
}
优缺点分析:
优点:实现简单
缺点:临界值问题
2.滑动窗口实现
优点:解决了固定窗口的临界值问题。扩展能力强。
缺点:流量不够平滑,有可能第一个窗口就满足要求了,其他都不能接收请求了。
各种代码实现思路:
代码实现:
(1)定义几个参数,划分几个格子,限制的次数,存储每个格子的数据量TreeMap
(2)1.获取格子归属当前时间
2.获取所有符合条件格子的总数,不符合的情况下删除key,value。
3.判断总数和限制次数,大于情况下拒绝任务,否则接收任务,将计数器+1
guava cache实现思路:
1.设置guava cache的初始化参数,最大容量,最大并发量,过期时间,构造函数
2.获取guava cache的数量,从一个个时间窗口中获取到起初的时间窗口,累加,成功返回.
redis实现滑动窗口原理:
利用zset结构,k存储限流名称 value 存储次数 score 存储时间戳,过期时间这里可以比限制时间长,否则会有数据丢失情况。
1.判断key是否存在,不存在直接赋值
2.存在情况下,zrangByScore从time-时间间隔开始一直到time看看有多少数量。数量比限制值大的情况拒绝,否则通过。
缺点是zset会随着构建数据不断增长
代码实现:
public class SlidingWindows {
//代码实现
//单位时间的周期,10代表每10s一个周期,每分钟有6个格子
private static int sub_cycle = 10;
//每分钟限制多少流量
private static int limitMin = 100;
//每个格子计数器,使用TreeMap机构,为什么使用TreeMap结构,因为TreeMap有序,key代表格子的开始时间,value代表数量
private static TreeMap<Long,Integer> map = new TreeMap<>();
//1.获取当前时间
public static synchronized void slidingWindows() {
//获取在哪个格子里
Long time = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) / sub_cycle * sub_cycle;
//计算总数,treeMap各个格子加在一起的数量
int count = getSum(time);
//判断总数和limit谁大
if(count+1>limitMin) {
System.out.println("拒绝任务");
}else{
System.out.println("接收任务");
map.put(time,map.getOrDefault(time,0)+1);
}
}
public static int getSum(Long time) {
//计算窗口开始位置
long startTime = time - sub_cycle* (60/sub_cycle-1);
int count = 0;
//遍历存储的计数器
Iterator<Map.Entry<Long, Integer>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Long, Integer> entry = iterator.next();
// 删除无效过期的子窗口计数器
if (entry.getKey() < startTime) {
iterator.remove();
} else {
//累加当前窗口的所有计数器之和
count =count + entry.getValue();
}
}
return count;
}
//guava cache限流
static LoadingCache<Long, AtomicInteger> cache = CacheBuilder.newBuilder()
.maximumSize(100)
.expireAfterAccess(10, TimeUnit.SECONDS)
.build(new CacheLoader<Long, AtomicInteger>() {
@Override
public AtomicInteger load(Long key) throws Exception {
AtomicInteger a =new AtomicInteger(0);
System.out.println(a.intValue());
return a;
}
});
public static void slidingWindowsGuavaCache() throws ExecutionException {
Long time = System.currentTimeMillis()/(1000*sub_cycle)*sub_cycle;
//滑动窗口改造
int sum = cache.get(time).incrementAndGet();
for(int i=1;i<sub_cycle;i++) {
sum+=cache.get(time-sub_cycle*(60/sub_cycle-i)).intValue();
}
if(sum>limitMin) {
System.out.println("拒绝请求");
}else{
System.out.println("接收请求");
}
}
Long interVal = 60000L;
@Autowired
RedisTemplate redisTemplate;
/**
* redis滑动窗口限流
* 数据结构:zset key:限流名称 score:时间戳 value次数
*/
public void slidingWindowRedis() {
//获取时间窗口
Long time = new Date().getTime();
//判断key存在与否
if(redisTemplate.hasKey("limit")) {
int size =redisTemplate.opsForZSet().rangeByScore("limit",time-interVal,time).size();
if(size+1>limitMin) {
System.out.println("拒绝请求");
return;
}
}
redisTemplate.opsForZSet().add("limit",1,time);
}
public static void main(String[] args) {
for(int i=0;i<1000;i++) {
new Thread(new Runnable() {
@Override
public void run() {
slidingWindows();
}
}).start();
}
}
}
3.漏桶算法
优点:平滑处理流量,通过设置容量+速率可以动态控制流出
缺点:突发流量情况下,桶容量满了,就会丢失数据。
本地代码实现:
思想:设置桶容量,速率,当前桶容量,上次漏水时间戳
1.当放入水的时候,先进行漏水一次
漏水逻辑:获取当前时间戳,和上一次漏水时间戳计算出来应该流出多少。
将桶中容量减去流出水量,初始化当前桶容量,漏水时间。
2.漏水之后进行判断容量+此次漏水量和总容量进行比较,如果大于拒绝请求,否则接收请求,初始化容量
核心点:桶容量,速率
public class LeakyBucket {
private int capacity;//总容量
private Long rate;//此处是每秒流水速率
private Long curCap;//当前水量
private Long lastLeakTime;//上次漏水时间戳
//构造函数 初始化参数
public LeakyBucket(int capacity,Long rate) {
this.capacity = capacity;
this.rate = rate;
curCap = 0L;
lastLeakTime = System.currentTimeMillis();
}
/**
* 尝试放入桶中
* @param waterRequested
*/
public synchronized void tryConsume(Long waterRequested) {
//漏水
leak();
if(curCap+waterRequested>capacity) {
System.out.println("桶满了,拒绝请求");
return;
}
curCap+=waterRequested;
System.out.println("接收请求");
}
/**
* 漏水
* 根据当前时间和上次漏水时间戳计算出应该漏出的水量,然后更新桶中的水量和漏水时间戳等状态。
*/
public synchronized void leak() {
Long curTime = System.currentTimeMillis();
Long leakWater = (curTime-lastLeakTime)/1000*rate;
if(leakWater>0) {
curCap = Math.max(0L,curCap - leakWater);
lastLeakTime = curTime;
}
}
public static void main(String[] args) {
LeakyBucket leakyBucket =new LeakyBucket(100,2L);
for(int i=1;i<1000;i++) {
int finalI = i;
new Thread(new Runnable() {
@Override
public void run() {
leakyBucket.tryConsume((long) finalI);
}
}).start();
}
}
}
redis实现:
个人感觉:使用list结构,利用左出右进或者左进右出的特性;有些地方说也可以用zset结构,个人感觉漏桶使用zset不太合适,score和value应该存储什么内容,如何流出数据。个人感觉list更合适。
/**
* redis实现漏桶
* 1.桶容量设置
* 2.速率设置,
*/
public class LeakBucketRedis {
private int cap;//桶容量
private int rate;//速率
private Long lastLeakTime;//时间
@Autowired
RedisTemplate redisTemplate;
public LeakBucketRedis(int cap,int rate) {
this.cap = cap;
this.rate = rate;
this.lastLeakTime = System.currentTimeMillis();
}
//list实现 利用左出右进的思想
public void leakBucketRedisList() {
//如果没有初始化先进行初始化
if(redisTemplate.hasKey("limit")) {
redisTemplate.opsForList().rightPush("limit",1);
lastLeakTime = System.currentTimeMillis();
}else{
//判断先按照速率流出
Long curTime = System.currentTimeMillis();
int leakSum = (int) ((curTime-lastLeakTime)/1000*rate);
for(int i=0;i<leakSum;i++) {
redisTemplate.opsForList().leftPop("limit");
}
//计算当前结构的容量
Long count =redisTemplate.opsForList().size("limit");
if(count+1>cap) {
System.out.println("桶满了,不可以放入元素了");
return;
}
redisTemplate.opsForList().rightPush("limit",1);
}
}
}
4.令牌桶限流
优点:
-
稳定性高:令牌桶算法可以控制请求的处理速度,可以使系统的负载变得稳定。
-
精度高:令牌桶算法可以根据实际情况动态调整生成令牌的速率,可以实现较高精度的限流。
-
弹性好:令牌桶算法可以处理突发流量,可以在短时间内提供更多的处理能力,以处理突发流量。
缺点:
-
实现复杂:相对于固定窗口算法等其他限流算法,令牌桶算法的实现较为复杂。对短时请求难以处理:在短时间内有大量请求到来时,可能会导致令牌桶中的令牌被快速消耗完,从而限流。这种情况下,可以考虑使用漏桶算法。
-
时间精度要求高:令牌桶算法需要在固定的时间间隔内生成令牌,因此要求时间精度较高,如果系统时间不准确,可能会导致限流效果不理想。
令牌桶算法具有较高的稳定性和精度,但实现相对复杂,适用于对稳定性和精度要求较高的场景。
原理如下图所示:redis代码实现:
使用list结构,左出右进或者左进右出的思想放入令牌
public class TokenBucketRedis {
private final static String TOKEN_KEY = "TOKEN_KEY";
/**
* 令牌个数
*/
private int TOKEN_SIZE = 10;
@Autowired
RedisTemplate redisTemplate;
public synchronized void tokenBucket() {
//判断令牌个数,循环一直等着>0
int i=0;
while(redisTemplate.opsForList().size(TOKEN_SIZE)<0&&i<3) {
System.out.println("等待令牌生成");
i++;
}
if(i==3) {
System.out.println("重试3次未获得令牌,拒绝请求");
return;
}
//获取令牌
redisTemplate.opsForList().leftPop(TOKEN_KEY);
System.out.println("获取令牌成功,请求接收");
}
/**
* 动态生成令牌:
* fixedDelay代表每秒生成,控制方法执行的间隔时间,是以上一次方法执行完开始算起,如上一次方法执行阻塞住了,那么直到上一次执行完,并间隔给定的时间后,执行下一次
*/
@Scheduled(fixedDelay=1000)
public void takeToken() {
//令牌个数不满足情况下,加入令牌
Long size = redisTemplate.opsForList().size(TOKEN_SIZE);
for(Long i=size;i<TOKEN_SIZE;i++){
redisTemplate.opsForList().rightPush(TOKEN_KEY, UUID.randomUUID());
}
}
}
5.总结:
固定时间窗口:一个固定窗口的次数
优点:实现简单 缺点:临界值问题,流量不平滑
实现方式:1.定义一个时间窗口,一个限流数,通过当前时间和上次时间进行比较,如果大于时间阙值,证明已经过期;小于情况下,判断当前流量请求数和限流阙值,如果满足的情况下,通过请求,否则拒绝请求。 2.使用redis的string结构,key存储限流名称,value存储次数,过期时间设置为时间窗口,利用自动的过期时间进行实现固定窗口限流。
滑动窗口:将一个时间窗口等分几块,随着时间进行格子移动。
优点:解决了固定窗口临界值问题
缺点:处理不够平滑
实现方式:1.将一个时间窗口几等分,限流阙值,记录每个窗口的次数可以使用TreeMap结构,key存储时间窗口的起始位置,value存储当前格子的次数;请求来的时候,计算时间阙值以内所有的次数与限流阙值比较,大情况下拒绝请求,小的时候通过请求
2.使用redis的zset结构,利用zset结构,key存储限流名称,score存储时间戳,value存储次数或者id,利用score的排序性,获取当前时间-时间阙值的个数与限流阙值进行比较,如果大于等于说明已经满足了拒绝请求,否则通过请求,同时zadd数据
漏桶算法:一个桶,流出速率固定,请求进入的数量不确定。当桶满足了,拒绝请求,以固定速率处理请求,以保护当前系统。
优点:相对于滑动窗口更加平滑。
缺点:突发流量场景下处理不足。
实现:1.两个必须参数:桶容量,流出速率
2.流量进入之后,判断桶是否满足限流要求,满足进行限流。
不满足情况下,将请求加在桶中。
以固定速率处理桶里面数据
redis实现:使用list代表桶,左出右进或者左进右出;进入时候判断list的size和桶容量,满足要求拒绝请求,不满足请求将请求放入list中。以固定速率进行出队列。
令牌桶:一个桶中存储了一堆令牌,每个请求去获取令牌,获取到接收请求,获取不到拒绝请求。
优点:解决了突发流量
缺点:实现困难;时间精度高
实现:redis list结构实现,list代表令牌数量,请求获取list的数据,获取到出队列,获取不到代表没有令牌了,拒绝请求。