前言
最近想对自己的秒杀系统添加一个QPS限流的功能,自己百度了一下,发现大家的大体思路是一样的,所以边学习边自己写了下来。因为刚开始写博客,是个小白,欢迎大家指出错误。感恩!
限流的算法
先来讲一下常见的限流的算法。
计数法
计数法就是通过定义一个count,在规定的时间内(eg,每分钟之内只能访问100次)每次有人访问接口的时候count++,直到达到限流的最大值100,之后的访问都拒绝,到下一分钟count重置为0;
- 下面是用java写的代码帮助大家理解一下这个思路
package com.xiaoxiao.current_limiting.basic;
import java.util.Date;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.springframework.stereotype.Service;
@Service
public class CountLimiting {
private long count = 0;
private long countLimit = 100; //最大访问数
private long period = 1000 * 1; //周期 1s
private long timeStamp = new Date().getTime();
Lock lock = new ReentrantLock();
//可以访问返回true
public boolean visit() {
long now = new Date().getTime(); //当前时间戳
if (timeStamp + period > now) { //如果now还在这个周期里
if (++count <= countLimit) { //访问+1 相加和判断分步写的话会导致非原子性,所以这里我就写在一起了
System.out.println(count);
return true;
} else {
count--; //因为多加了所以要减掉
return false;
}
} else {
if (timeStamp + period < now) {
try {
boolean res = lock.tryLock();
if (res) { //这里也是为了保证原子性 更新只能由一个线程更新
timeStamp = new Date().getTime();
count = 1;
return true;
} else {
return visit();//如果没有获得锁,重新请求
}
} finally {
lock.unlock();
}
} else {
count++;
return true;
}
}
}
}
- 也可以直接用redis,用然后设置过期时间,实现比较简单,redis是线程安全的,这里不细说
缺点
计数法有一个明显的缺点就是划分的间隔太大了,如果在前一分钟的59.5之后才访问,访问了100次;在后一分钟的第0.5秒内访问了100次。其实就相当于1s之内访问了200次。
滑动窗口算法
窗口算法其实就是计数法的改进版,因为计数法的粒度太大了,所以窗口法就是把计数法的粒度变小,把一个周期分为n个窗口,把时间分为n分,然后进行填充。
- java代码实现
package com.xiaoxiao.current_limiting.basic;
import java.util.Date;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class BucketLimiting {
private int[] count = new int[10]; //每个窗口的count
private int allCount = 0; //总的count
private long countLimit = 10000; //最大访问数
private long period = 1000; //周期 1s
private int bucket = 10;//窗口数
private int index = 0; //目前指向哪个窗口
private long timeStamp = new Date().getTime();
//可以访问返回true
Lock lock = new ReentrantLock();
public boolean visit() {
long now = new Date().getTime(); //当前时间戳
if (timeStamp + period / bucket > now) { //如果now还在这个桶里
if (++allCount <= countLimit) { //访问+1 相加和判断分步写的话会导致非原子性,所以这里我就写在一起了
count[index]++; //桶里的数量也要加一
return true;
} else {
allCount--; //因为多加了所以要减掉
return false;
}
} else {
if (timeStamp + period / bucket < now) {
try {
boolean res = lock.tryLock();
if (res) { //这里也是为了保证原子性 更新只能由一个线程更新
timeStamp = new Date().getTime();
index = (index + 1) % bucket; //放在哪个桶 循环利用的
allCount -= count[index] - 1; //减掉要覆盖的那个桶的数量
count[index] = 1;
return true;
} else {
return visit();//如果没有获得锁,重新请求
}
} finally {
lock.unlock();
}
} else {
count[index]++;
allCount++;
return true;
}
}
}
}
漏桶算法
有一个桶,里面可以放水,用什么速度放水我不管,但是漏水的速度会控制(请求接口的速度),如果水桶满了 ,那么水直接溢出(请求直接丢弃,不处理)。
- java代码实现
package com.xiaoxiao.current_limiting.basic;
public class LeakyLimiting {
private long water = 0;//一开始水桶里面没有水
private long rate = 100;//漏水的速度 每秒100个
private long capacity = 100;//水桶的容量是100
private long timestamp = System.currentTimeMillis();//上次访问的时间
public boolean visit() {
long now = System.currentTimeMillis();
water = Math.max(0L, water - (now - timestamp) / 1000 * rate);//当前剩余的水量 每次有访问的时候,更新水量
timestamp = now; //更新最后访问时间
if (++water <= capacity) {//水桶还放的下去,允许访问
return true;
} else {
water--;//丢弃这滴水
return false; //水满了不允许访问
}
}
}
令牌法
令牌法和漏桶算法很类似,其实是同一个意思。令牌法是往一个空桶里按一定的速率往里面放入令牌,如果桶满了,令牌就直接丢弃,每个请求要访问之前都要在桶里拿一个令牌,如果拿不到,就不允许访问
public class TokenLimiting {
private long tokenCount = 0;//令牌数
private long timestamp = System.currentTimeMillis();
private long capacity = 100;//桶的容量
private long rate = 100; //速度
public boolean visit() {
long now = System.currentTimeMillis();
tokenCount = Math.min(tokenCount + (now - timestamp) / 1000 * 100, capacity);//桶里的令牌数
timestamp = now;
if (--tokenCount >= 0) {
return true;
} else {
tokenCount++;
return false;
}
}
}
Guava插件的令牌法
Guava的RateLimiter使用的就是令牌桶算法,按照固定的频率往桶里放入令牌,每次响应都要取到令牌才能响应,获取有2种方式:一种是直接返回失败,一种是阻塞到拿到令牌为止
package com.xiaoxiao.current_limiting.basic;
import com.google.common.util.concurrent.RateLimiter;
public class GuavaLimiting {
public boolean visit() {
RateLimiter rateLimiter = RateLimiter.create(100);//每秒100个令牌
if (rateLimiter.tryAcquire()) { //尝试获取令牌 无延迟 获取不到返回false
return true;
} else {
return false;
}
}
}