在实际应用中,往往需要使用到一些限流算法,比如nginx的底层使用的是漏桶流算法,因此我们实际遇到的比较常见的有4种限流算法。
1.计数器限流算法
这种算法是限流中最简单的一种算法,是通过在单位时间内所允许的最大流量,通过计数来实现限流,内存消耗小。
具体实现如下:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import com.suntree.interfaces.MyRateLimiter;
public class CounterRateLimiter implements MyRateLimiter{
private final long permitsPerSecond; //每秒限制的请求数
private long timestamp=System.currentTimeMillis(); //上一次的时间
private int counter;
public CounterRateLimiter(long permitsPerSecond) {
this.permitsPerSecond=permitsPerSecond;
}
@Override
public synchronized boolean tryAcquire() {
long now=System.currentTimeMillis();
if(now-timestamp<=1000) {
counter++;
if(counter<=permitsPerSecond) {
return true;
}else {
return false;
}
}
counter=1;
timestamp=now;
return true;
}
public static void main(String[] args) {
CounterRateLimiter limiter=new CounterRateLimiter(10);
try {
Thread.sleep(900);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
long s1=System.currentTimeMillis();
for(int i=0;i<10;i++) {
boolean b=limiter.tryAcquire();
if(!b) {
System.err.println("请求超出限制");
}
}
long s2=System.currentTimeMillis();
System.err.println(s2-s1);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
for(int i=0;i<10;i++) {
boolean b=limiter.tryAcquire();
if(!b) {
System.err.println("请求超出限制");
}
}
}
}
计数器算法存在一个临界点问题,假设我每分钟最大允许100个请求,那么我在第59分钟来了100个请求,接着我在第60分钟临界点又有100请求,那相当于我一分钟之内有200个请求,超过了最大限制,因此第二种滑动窗口算法解决了这个问题。
2、滑动窗口限流算法
滑动窗口算法的原理就是将一个固定窗口变为滑动窗口,假设我每分钟允许100个请求,那么我就将这个每分钟又划分为多个小窗口,比如划分为6个窗口,每个窗口10秒钟,最后计算的是这些小窗口总的请求量如果不超过最大请求数,则放行,反之,则限制,这种算法可以解决临界点问题。
具体实现如下:
import java.util.HashMap;
import java.util.TreeMap;
import com.suntree.interfaces.MyRateLimiter;
public class SlidingWindowRateLimiter implements MyRateLimiter{
private final long windowLengthInMs=200L;
private int windowCount=5;
private int permitCount;
private int currId=0;
private int counter=0;
private long lastWindowTime=System.currentTimeMillis();
//private final HashMap<Integer, Integer> windowCounters;
private final int[] windowCounters;
public SlidingWindowRateLimiter(int permitCount) {
this.permitCount=permitCount;
windowCounters=new int[windowCount];
}
@Override
public synchronized boolean tryAcquire() {
long now=System.currentTimeMillis();
long currWindowTime=now-lastWindowTime;
if(currWindowTime>=windowLengthInMs) {
currId+=(currWindowTime/windowLengthInMs);
currId=currId%windowCount;
int newCurrId=currId;
/*
if(windowCounters.get(newCurrId)!=null) {
counter=counter-windowCounters.get(newCurrId);
}
windowCounters.put(newCurrId, 1);*/
counter=counter-windowCounters[newCurrId];
windowCounters[newCurrId]=1;
lastWindowTime=now;
}else {
windowCounters[currId]++;
//windowCounters.merge(currId, 1, Integer::sum);
}
counter++;
return counter<=permitCount;
}
public static void main(String[] args) {
SlidingWindowRateLimiter limiter=new SlidingWindowRateLimiter(10);
long s1=System.currentTimeMillis();
for(int i=0;i<10;i++) {
boolean b=limiter.tryAcquire();
if(!b) {
System.err.println("请求超出限制");
}
}
long s2=System.currentTimeMillis();
System.err.println(s2-s1);
try {
Thread.sleep(990);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
for(int i=0;i<10;i++) {
boolean b=limiter.tryAcquire();
if(!b) {
System.err.println("请求超出限制");
}
}
}
}
滑动窗口算法限流虽然解决了临界问题,但限流并不平滑,流量可能会被直接掐断,而漏桶流算法可以解决流量不平滑问题。
3.漏桶流算法
漏桶流算法是流量整形及速率限制经常使用的算法,突发流量可以直接被整形以提供稳定的流量,它的原理就是存在一个固定漏桶,以不固定的注水速率注水,但是它的出水速率是固定的,当水被注满,也就是达到桶的容量,那么多余的请求就会被拒绝。
实现如下:
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import com.suntree.interfaces.MyRateLimiter;
public class LeakyRateLimiter implements MyRateLimiter{
private final int capacity;
private final float permitsPerSecond;
private double restWater; //剩余水量
private long timestamp=System.currentTimeMillis();//上次的注水时间
public LeakyRateLimiter(int capacity,float permitsPerSecond) {
this.capacity=capacity;
this.permitsPerSecond=permitsPerSecond;
}
@Override
public synchronized boolean tryAcquire() {
long now=System.currentTimeMillis();
float timeGap=(now-timestamp)/1000f;
restWater= Math.max(0, restWater-(double)Math.round(timeGap*permitsPerSecond*100)/100);
timestamp=now;
if(restWater>capacity-1) {
return false;
}
if(restWater<capacity) {
restWater+=1;
return true;
}
return false;
}
public static void main(String[] args) {
ExecutorService singleExcutor=Executors.newSingleThreadExecutor();
ScheduledExecutorService scheduleExecutor=Executors.newSingleThreadScheduledExecutor();
LeakyRateLimiter limiter=new LeakyRateLimiter(20, 5);
Queue<Integer> queue=new LinkedList<>();
singleExcutor.execute(new Runnable() {
int count=0;
@Override
public void run() {
while(true) {
count++;
boolean flag=limiter.tryAcquire();
if(flag) {
queue.offer(count);
System.err.println("流量被放行");
}else {
System.err.println("流量被限制");
}
try {
Thread.sleep((long) (Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
scheduleExecutor.scheduleAtFixedRate(()->{
if(!queue.isEmpty()) {
System.err.println(queue.poll()+"被处理");
}
}, 0, 100, TimeUnit.MILLISECONDS);
}
}
在这里插入代码片
这种算法缺陷就是不能允许一定的突发流量的发生,因为不管注水有多快,出水都不变,在某些场景下对用户其实并不友好,因此,我们可以使用令牌桶算法解决这个问题。
4.令牌桶算法
令牌桶算法在实现限流的前提下,允许一定的突发流量的发生,它是流量整形和速率限制最常使用的算法,google的guava当中RateLimiter就是使用了令牌桶算法,原理是 存在一个固定大小的令牌桶,以源源不断的速率产生令牌,用户的请求必须拿到令牌才可以通过,当令牌的产生速率大于消耗速率,令牌就会逐渐装满整个令牌桶,那么它允许的最大突发流量也就是桶的容量。
具体实现如下:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import com.suntree.interfaces.MyRateLimiter;
public class TokenBucketRateLimiter implements MyRateLimiter{
private final int capacity;
private final int generatePerSecond;
private long lastTokenTime=System.currentTimeMillis(); //上一个令牌发放时间
private long currentTokens;
public TokenBucketRateLimiter(int capacity,int generatePerSecond) {
this.capacity=capacity;
this.generatePerSecond=generatePerSecond;
this.currentTokens=generatePerSecond;
}
@Override
public synchronized boolean tryAcquire() {
long now=System.currentTimeMillis();
if(now-lastTokenTime>=1000) {
long newPermitTokens=(now-lastTokenTime)/1000*generatePerSecond;
currentTokens=Math.min(currentTokens+newPermitTokens, capacity);
lastTokenTime=now;
}
if(currentTokens>0) {
currentTokens--;
return true;
}
return false;
}
public static void main(String[] args) {
TokenBucketRateLimiter limiter=new TokenBucketRateLimiter(100, 10);
ExecutorService singleExcutor=Executors.newSingleThreadExecutor();
singleExcutor.execute(new Runnable() {
int count=0;
@Override
public void run() {
while(true) {
count++;
boolean flag=limiter.tryAcquire();
if(flag) {
System.err.println(count+"请求被放行");
}else {
System.err.println(count+"请求被限制");
}
if(count>10) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}else {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
});
}
}
总结:
从限流的这几种算法来看,我们在可以根据自身的需要去选择。
大家可以尝试一下。