java实现限流的底层算法分析

在实际应用中,往往需要使用到一些限流算法,比如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();
						}
					}	
				}
				
			}
		});
	}
}

总结:
从限流的这几种算法来看,我们在可以根据自身的需要去选择。

大家可以尝试一下。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夜空下的星

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值