四种限流方法
1、计数器固定窗口算法(计数器)
规定单位时间处理的请求数量,比如规定一个接口一分钟只能访问10次。使用固定窗口计数器:给定一个变量counter来记录处理这个请求数量,当一分钟之内超过10次,全部拒绝。
等到1分钟后就讲counter回归为0,重新开始计数。
缺点:如果在59秒发10个请求,在1分一秒的时候再发10个请求,就会出现问题!!。
import java.util.concurrent.atomic.AtomicInteger; public class 计数器固定窗口算法 { private int windowSize;//窗口大小 private int limit;//窗口内限流大小 private AtomicInteger count;//当前计数器 public 计数器固定窗口算法(int windowSize, int limit) { this.windowSize = windowSize; this.limit = limit; count = new AtomicInteger(0); //开启一个线程达到窗口结束时清空count; new Thread(() -> { while (true) { count.set(0); try { Thread.sleep(windowSize); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } //请求到达后先调用本方法,若返回true,则请求通过,否则限流 public boolean tryAcuire() { int newCount = count.addAndGet(1); if (newCount > limit) { return false;//大于目前的阈值 } else { return true; } } public static void main(String[] args) throws InterruptedException { //测试 计数器固定窗口算法 counterTest = new 计数器固定窗口算法(100, 20); int count = 0; for (int i = 0; i < 50; i++) { if (counterTest.tryAcuire()) { count++; } } System.out.println("第一波50个请求通过" + count + "限流" + (50 - count)); Thread.sleep(1000); count=0; for (int i = 0; i < 50; i++) { if (counterTest.tryAcuire()) { count++; } } System.out.println("第二波50个请求通过" + count + "限流" + (50 - count)); } }
2、计数器滑动窗口算法
public class 计数器滑动窗口算法 { private int windowSize;//窗口大小,毫秒为单位 private int limit;//窗口内限流大小 private int splitNum;//切分小窗口的数目大小 private int[] counters;//当前小窗口的计数数组 private int index;//当前小窗口计数器的索引 private long startTime;//窗口开始时间 public 计数器滑动窗口算法(int windowSize, int limit, int splitNum) { this.windowSize = windowSize; this.limit = limit; this.splitNum = splitNum; counters = new int[splitNum]; startTime = System.currentTimeMillis(); } //请求达到后先调用本方法,若返回true,则请求通过,否则限流 public synchronized boolean tryAcquire() { long curTime = System.currentTimeMillis(); long windowsNum = Math.max(curTime - windowSize - startTime, 0) / (windowSize / splitNum); //计算滑动小窗口的数量 slideWindow(windowsNum); int count = 0; for (int i = 0; i < splitNum; i++) { count += counters[i]; } if (count >= limit) { return false; } else { counters[index]++; return true; } } public synchronized void slideWindow(long windowsNum) { if (windowsNum == 0) { return; } long slideNum = Math.min(windowsNum, splitNum); for (int i = 0; i < slideNum; i++) { index = (index + 1) % splitNum; counters[index] = 0; } startTime = startTime + windowsNum * (windowSize / splitNum);//更新滑动窗口时间 } public static void main(String[] args) throws InterruptedException { //每秒20个 int limit = 20; 计数器滑动窗口算法 counterSlideWindowLimiter = new 计数器滑动窗口算法(1000, limit, 20); int count = 0; // Thread.sleep(3000); //计数器滑动窗口算法模拟100组间隔30ms的50次请求 System.out.println("计数器滑动窗口算法测试开始..."); System.out.println("开始模拟100组间隔150ms的50次请求..."); int failCount = 0; for (int j = 0; j < 100; j++) { count = 0; for (int i = 0; i < 50; i++) { if (counterSlideWindowLimiter.tryAcquire()) { count++; } } Thread.sleep(150); //模拟50次请求,看多少能通过 for (int i = 0; i < 50; i++) { if (counterSlideWindowLimiter.tryAcquire()) { count++; } } if (count > limit) { System.out.println("时间窗口内放过的请求超过阈值,放过的请求数" + count + ",限流:" + limit); failCount++; } Thread.sleep((int) (Math.random() * 100)); } System.out.println("计数器滑动窗口算法测试结束,100组间隔150ms的50次请求模拟完成,限流失败组数: " + failCount); System.out.println("=========================================================================="); //计数器固定窗口算法模拟100组间隔30ms的50次请求 System.out.println("计数器固定窗口算法测试开始..."); //模拟100组间隔30ms的50次请求 计数器滑动窗口算法 counterLimiter = new 计数器滑动窗口算法(1000, 20, 20); System.out.println("开始模拟100组间隔150ms的50次请求..."); failCount = 0; for (int j = 0; j < 100; j++) { count = 0; for (int i = 0; i < 50; i++) { if (counterLimiter.tryAcquire()) { count++; } } Thread.sleep(150); //模拟50次请求,看多少能通过 for (int i = 0; i < 50; i++) { if (counterLimiter.tryAcquire()) { count++; } } if (count > limit) { System.out.println("时间窗口内放过的请求超过阈值,放过的请求数" + count + ",限流:" + limit); failCount++; } Thread.sleep((int) (Math.random() * 100)); } System.out.println("计数器固定窗口算法测试结束,100组间隔150ms的50次请求模拟完成,限流失败组数:" + failCount); } }
把时间以一定比例分片,把1分钟分为60个窗口。每隔1秒移动一次,每次窗口一秒只能处理不大于60(请求)/60(窗口)的请求,如果当前窗口的请求总和超过了限制数量不再处理其他请求。如果当前请求总和超过了限制的数量就不再处理请他请求。各种越大滑动窗口滚动越平滑。
缺点:滑动的时间复杂度是O(N),因为才用list来做数据结构,查询、修改需要逐个查询链表指针,效率比令牌通更低。滑动窗口需要存储更多的数据,存储N个数据,过期时间为S(S为一个窗口的计算周期),随着窗口细粒度越高,存储数据也越大。相当于是每个小周期就是个小窗口,这样就能避免计数器的毛刺现象
3、漏洞算法
import java.util.Date; import java.util.LinkedList; public class LeakyBucketLimiter { private int capacity; //漏斗容量 private int rate; //漏斗速率 private int left; //剩余容量 private LinkedList<Request> requestList; private LeakyBucketLimiter() { } public LeakyBucketLimiter(int capacity, int rate) { this.capacity = capacity; this.rate = rate; this.left = capacity; requestList = new LinkedList<>(); //开启一个定时线程,以固定的速率将漏斗中的请求流出,进行处理 new Thread(new Runnable() { @Override public void run() { while (true) { if (!requestList.isEmpty()) { Request request = requestList.removeFirst(); handleRequest(request); } try { Thread.sleep(1000 / rate); //睡眠 } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); } /** * 处理请求 * * @param request */ private void handleRequest(Request request) { request.setHandleTime(new Date()); System.out.println(request.getCode() + "号请求被处理,请求发起时间: " + request.getLaunchTime() + ",请求处理时间: " + request.getHandleTime() + ",处理耗时: " + (request.getHandleTime().getTime() - request.getLaunchTime().getTime() + "ms")); } private synchronized boolean tryAcquire(Request request) { if (left <= 0) { return false; } else { left--; requestList.addLast(request); return true; } } /** * 请求类,属性包含编号字符串,请求达到时间和请求处理时间 */ static class Request { private int code; private Date launchTime; private Date handleTime; private Request() { } public Request(int code, Date launchTime) { this.launchTime = launchTime; this.code = code; } public int getCode() { return code; } public void setCode(int code) { this.code = code; } public Date getLaunchTime() { return launchTime; } public void setLaunchTime(Date launchTime) { this.launchTime = launchTime; } public Date getHandleTime() { return handleTime; } public void setHandleTime(Date handleTime) { this.handleTime = handleTime; } } public static void main(String[] args) { LeakyBucketLimiter leakyBucketLimiter = new LeakyBucketLimiter(5, 2); for (int i = 1; i <= 10; i++) { Request request = new Request(i, new Date()); if (leakyBucketLimiter.tryAcquire(request)) { System.out.println(i + "号请求被接受"); } else { System.out.println(i + "号请求被拒绝"); } } } }
可以把请求的动作比作成注水到桶里中,处理请求的过程比喻成漏捅漏水。我们往桶中任意速率流入水,以一定的速率流出水。当水桶超过流量就丢弃。用一个队列来保存请求,然后定期从队列中拿出请求就可以。
缺点:导致其无法应对准点秒杀刚开始的流量洪峰。
4、令牌桶算法
import java.util.Date; public class TokenBucketLimiter { private int capacity; //令牌桶容量 private int rate; //令牌产生速率 private int tokenAmount; //令牌数量 public TokenBucketLimiter(int capacity, int rate) { this.capacity = capacity; this.rate = rate; tokenAmount = capacity; new Thread(new Runnable() { @Override public void run() { //以恒定的速率放令牌 while (true) { synchronized (this) { tokenAmount++; if (tokenAmount > capacity) { tokenAmount = capacity; } } try { Thread.sleep(1000 / rate); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); } public synchronized boolean tryAcquire(Request request) { if (tokenAmount > 0) { tokenAmount--; handleRequest(request); return true; } else { return false; } } /** * 处理请求 * * @param request */ private void handleRequest(Request request) { request.setHandleTime(new Date()); System.out.println(request.getCode() + "号请求被处理,请求发起时间: " + request.getLaunchTime() + ",请求处理时间: " + request.getHandleTime() + ",处理耗时: " + (request.getHandleTime().getTime() - request.getLaunchTime().getTime() + "ms")); } /** * 请求类,属性包含编号字符串,请求达到时间和请求处理时间 */ static class Request { private int code; private Date launchTime; private Date handleTime; private Request() { } public Request(int code, Date launchTime) { this.launchTime = launchTime; this.code = code; } public int getCode() { return code; } public void setCode(int code) { this.code = code; } public Date getLaunchTime() { return launchTime; } public void setLaunchTime(Date launchTime) { this.launchTime = launchTime; } public Date getHandleTime() { return handleTime; } public void setHandleTime(Date handleTime) { this.handleTime = handleTime; } } public static void main(String[] args) { TokenBucketLimiter tokenBucketLimiter = new TokenBucketLimiter(5, 2); for (int i = 1; i <= 10; i++) { Request request = new Request(i, new Date()); if (tokenBucketLimiter.tryAcquire(request)) { System.out.println(i + "号请求被接受"); }else { System.out.println(i + "号请求被拒绝"); } } } }
令牌桶可以说是生产者消费者模式,一个生产者以固定的速度生产令牌,如果桶满了就停止生产,每一个请求都是一个消费者,需要从桶里面拿到令牌才能进一步被处理。
总结:
1、两种漏洞算法能够强行限制数据的传输速率,而令牌通算法能够限制数据在平局传输外,还能在某种程度上应对突发流量。
2、漏捅的关键至于控制桶的大小,决定了系统能够容纳的等待请求。令牌通的关键在于控制“令牌生成的速率”,它决定了整个系统的流量速率。