限流技术主要有以下几种
- 信号量
- 计数器
- 滑动窗口
- 漏桶算法
- 令牌桶算法
- 分布式限流
信号量:信号量实际上就是限制系统的并发量,来达到限流的目的。常见的用法是:创建Semaphore
,指定permit
的数量。在方法开始时,调用Semaphore.acquire()
或者Semaphore.tryAcquire()
来获取permit
,并在方法返回前,调用Semaphore.release()
来返还permit
。(线程方面)
计数器:
计数器的方案比较简单。比如限制1秒钟内请求数最多为10个,每当进来一个请求,则计数器+1。当计数器达到上限时,则触发限流。时间每经过1秒,则重置计数器。
滑动窗口:
滑动窗口本质上也是一种计数器,只不过它的粒度更细。比如限制qps为1000,设定窗口大小为10,则每个窗口的时间间隔为100ms。每次窗口滑动时,重置的是前1s至900ms之间内的计数,而不是完整的1s。
示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | public class RateLimiter { private LinkedList<Integer> deque = new LinkedList<>(); private int windowSize; private int windowIntervalMilliSeconds; private int currentWindowPermits;
private long updateTimeStamp; private int intervalMilliSeconds; private int maxPermits; private long storedPermits;
public RateLimiter(int maxPermits, int windowSize) { this(maxPermits, 1, windowSize); }
public RateLimiter(int maxPermits, int intervalSeconds, int windowSize) { this.maxPermits = maxPermits; this.intervalMilliSeconds = intervalSeconds * 1000; this.windowSize = windowSize; this.windowIntervalMilliSeconds = intervalMilliSeconds / windowSize; }
public synchronized Boolean acquire(int permits) { while (true) { long now = System.currentTimeMillis(); if (now < updateTimeStamp + windowIntervalMilliSeconds) { if (storedPermits + permits + currentWindowPermits <= maxPermits) { currentWindowPermits += permits; updateTimeStamp = now; return true; } else { return false; } } else { updateTimeStamp = now; deque.offerLast(currentWindowPermits); storedPermits += currentWindowPermits; currentWindowPermits = 0; while (deque.size() > windowSize) { storedPermits -= deque.removeFirst(); } } } } } |
滑动窗口仍然没有解决计数器的问题。
漏桶算法:
漏桶(Leaky Bucket)算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率。
令牌桶算法的原理是,系统以固定的速率往令牌桶中放入令牌;当请求进来时,则从桶中取走令牌;当桶中令牌为空时,触发限流。
令牌桶与漏桶相比,好处在于它支持突发流量。
Guava中的RateLimiter
提供了令牌桶算法的实现,我们可以直接使用。有关原理的说明请参考:Guava RateLimiter分析