在开发高并发系统时, 有三把利器来保护系统: 缓存, 降级和限流. 下面来看看限流量的一些算法:
1. 计数器法:
它是限流算法中最简单最容易的一种算法, 比如我们要求某一个接口, 1 分钟内的请求不能超过 10 次, 我们可以在开始时设置一个计数器,
每次请求, 该计数器 + 1; 如果该计数器的值大于 10 并且与第一次请求的时间间隔在 1 分钟内, 那么说明请求过多; 如果该请求与第一次请求
的时间间隔大于 1 分钟, 并且该计数器的值还在限流范围内, 那么重置该计数器. 具体代码如下:
- public class CounterDemo {
- public long timeStamp = getNowTime();
- public int reqCount = 0;
- public final int limit = 100; // 时间窗口内最大请求数
- public final long interval = 1000; // 时间窗口 ms
- public boolean grant() {
- long now = getNowTime();
- if (now < timeStamp + interval) {
- // 在时间窗口内
- reqCount++;
- // 判断当前时间窗口内是否超过最大请求控制数
- return reqCount <= limit;
- }
- else {
- timeStamp = now;
- // 超时后重置
- reqCount = 1;
- return true;
- }
- }
- }
不过, 以上代码有致命问题, 当遇到恶意请求, 在 0:59 时, 瞬间请求 100 次, 并且在 1:00 请求 100 次, 那么这个用户在 1 秒内请求了 200 次,
用户可以在重置节点突发请求, 而瞬间超过我们设置的速率限制, 用户可能通过算法漏洞击垮我们的应用. 如下图, 如何解决呢, 看下边的滑动窗口算法.
2. 滑动窗口算法:
在上图中, 整个红色矩形框是一个时间窗口, 在我们的例子中, 一个时间窗口就是 1 分钟, 然后我们将时间窗口进行划分, 如上图我们把滑动窗口
划分为 6 格, 所以每一格代表 10 秒, 每超过 10 秒, 我们的时间窗口就会向右滑动一格, 每一格都有自己独立的计数器, 例如: 一个请求在 0:35 到达,
那么 0:30 到 0:39 的计数器会 + 1, 那么滑动窗口是怎么解决临界点的问题呢? 如上图, 0:59 到达的 100 个请求会在灰色区域格子中, 而 1:00 到达的请求
会在红色格子中, 窗口会向右滑动一格, 那么此时间窗口内的总请求数共 200 个, 超过了限定的 100, 所以此时能够检测出来触发了限流.
回头看看计数器算法, 会发现, 其实计数器算法就是窗口滑动算法, 只不过计数器算法没有对时间窗口进行划分, 所以是一格.
由此可见, 当滑动窗口的格子划分越多, 限流的统计就会越精确.
3. 漏桶算法:
漏桶算法, 又称 leaky bucket , 如下图:
这个算法很简单. 首先, 我们有一个固定容量的桶, 有水进来, 也有水出去. 对于流进来的水, 我们无法预计共有多少水流进来, 也无法预计流水速度, 但
对于流出去的水来说, 这个桶可以固定水流的速率, 而且当桶满的时候, 多余的水会溢出来.
- public class LeakyDemo {
- public long timeStamp = getNowTime();
- public int capacity; // 桶的容量
- public int rate; // 水漏出的速度
- public int water; // 当前水量 (当前累积请求数)
- public boolean grant() {
- long now = getNowTime();
- water = max(0, water - (now - timeStamp) * rate); // 先执行漏水, 计算剩余水量
- timeStamp = now;
- if ((water + 1) < capacity) {
- // 尝试加水, 并且水还未满
- water += 1;
- return true;
- }
- else {
- // 水满, 拒绝加水
- return false;
- }
- }
- }
4. 令牌桶算法:
又称 token bucket, 如下图:
从上图中可以看出, 令牌算法有点复杂, 桶里存放着令牌 token. 桶一开始是空的, token 以固定的速率 r 往桶里面填充, 直到达到桶的容量, 多余的 token 会
被丢弃. 每当一个请求过来时, 就会尝试着移除一个 token, 如果没有 token, 请求无法通过.
- public class TokenBucketDemo {
- public long timeStamp = getNowTime();
- public int capacity; // 桶的容量
- public int rate; // 令牌放入速度
- public int tokens; // 当前令牌数量
- public boolean grant() {
- long now = getNowTime();
- // 先添加令牌
- tokens = min(capacity, tokens + (now - timeStamp) * rate);
- timeStamp = now;
- if (tokens < 1) {
- // 若不到 1 个令牌, 则拒绝
- return false;
- }
- else {
- // 还有令牌, 领取令牌
- tokens -= 1;
- return true;
- }
- }
- }
总结
计数器 VS 滑动窗口
计数器算法是最简单的算法, 可以看成是滑动窗口的低精度实现. 滑动窗口由于需要存储多份的计数器 (每一个格子存一份), 所以滑动窗口在实现上需要更多的存储空间. 也就是说, 如果滑动窗口的精度越高, 需要的存储空间就越大.
漏桶算法 VS 令牌桶算法
漏桶算法和令牌桶算法最明显的区别是令牌桶算法允许流量一定程度的突发. 因为默认的令牌桶算法, 取走 token 是不需要耗费时间的, 也就是说, 假设桶内有 100 个 token 时, 那么可以瞬间允许 100 个请求通过.
令牌桶算法由于实现简单, 且允许某些流量的突发, 对用户友好, 所以被业界采用地较多. 当然我们需要具体情况具体分析, 只有最合适的算法, 没有最优的算法.
- https://www.cnblogs.com/clds/p/5850070.html
- https://blog.csdn.net/ljj821061514/article/details/52512943
来源: http://www.bubuko.com/infodetail-3075114.html