Hello大家好,端午节到了,放松娱乐之余,也别忘了给自己充充电啊,这不,小慕今天就和大家一起学习学习高并发情况下的“限流策略”!
为啥要做流量控制呢?以下图为例,可以看出上游的A、B服务直接依赖了下游的基础服务C、D和E,对于A,B服务都依赖的基础服务D这种场景,服务A和B其实处于某种竞争关系,如果服务A的并发阈值设置过大,当流量高峰期来临,有可能直接拖垮基础服务D从而影响到服务B,即雪崩效应。
限流即流量控制,或者高大上一点,叫做流量整形,限流的目的是在遇到流量高峰期或者流量突增(流量尖刺)时,把流量速率限制在系统所能接受的合理范围之内,不至于让系统被高流量击垮。
我们常见的限流算法有:
-
计数器
-
漏桶算法
-
令牌桶算法
-
滑动窗口算法。
计数器算法
先看下图所示,我们把时间平均分成很多个时间间隔,比如我们以1分钟为一个时间周期,然后对每一个时间周期内部进行限流即可。
这是最简单的限流算法了,系统里面维护一个计数器,每来一个请求,计数器就加1,请求处理完成就减1,当计数器大于指定的阈值,就拒绝新的请求。是通过全局的总求数于设置的阈值来达到限流的目的。通常应用在池化技术上面比如:「数据库连接池、线程池」等中应用。
但是这种方式的话限流不是「平均速率」的。扛不住突增的流量,下面来说下这个问题。
如下图所示, 假如第一分钟前半部分时间没有产生请求,后半部分时间产生了很多请求,第二分钟的前半部分时间产生了很多的请求,而后半部分时间压根没有请求,那么就会导致第一分钟和第二分钟的全部请求都落在下面的红线上。而红线的时间长度也就一个时间周期罢了,却集齐了2倍的阈值,所以这就是个问题,明白了吧
代码如下:
/** * @ClassName : CounterLimitStream //类名 * @Description : 限流策略 //描述 * @Author : JavaAlliance //作者 * @Date: 2021-06-13 10:32 //时间 */public class CounterLimitStream { private int limtCount = 100;// 限制最大访问的容量 AtomicInteger atomicInteger = new AtomicInteger(0); // 每分钟 实际请求的数量 private long start = System.currentTimeMillis(); private int interval = 60*1000;// 间隔时间60秒 public boolean acquire() { long newTime = System.currentTimeMillis(); // 获取当前系统时间 // 判断是否是一个时间间隔周期 if (newTime > (start + interval)) { start = newTime; atomicInteger.set(0); //每过一个时间周期,计数器就重置为0 return true; } atomicInteger.incrementAndGet();// i++; return atomicInteger.get() <= limtCount; } static CounterLimitStream counterLimitStream = new CounterLimitStream(); public static void main(String[] args) { ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(); for (int i = 1; i < 100; i++) { final int tempI = i; newCachedThreadPool.execute(new Runnable() { public void run() { if (counterLimitStream.acquire()) { System.out.println("您没有被限流,可以正常访问 :" + tempI); } else { System.out.println("您已经被限流呢:" + tempI); } } }); } }}
滑动窗口算法
针对上面的计数器算法会在临界点存在瞬间大流量冲击的场景,滑动时间窗口计数器算法应运而生。它将时间窗口划分为更小的时间片断,每过一个时间片断,咱们的时间窗口就会往右滑动一格,每一个时间片断都有独立的计数器。咱们在计算整个时间窗口内的请求总数时会累加全部的时间片断内的计数器。时间窗口划分的越细,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。(PS:说白了就是为了确保在移动的过程中 ,下面的红色虚线框中的6个格子的总请求数小于阈值即可)
本文代码逻辑:新建一个本地缓存,以6s为一个时间窗口,每1s为一个时间片段,把时间做为缓存的key,原子类计数器做为缓存的value。每秒发送随机数量的请求,计算每一个时间片段的前6秒内的累加请求数量,超出阈值则限流。算法
@Slf4jpublic class SlideWindow { private LoadingCache<Long, AtomicLong> counter = CacheBuilder.newBuilder() .expireAfterWrite(10, TimeUnit.SECONDS) ////设置10s时间 对象没有被写访问则对象从内存中删除 .build(new CacheLoader<Long, AtomicLong>() { @Override public AtomicLong load(Long seconds) throws Exception { return new AtomicLong(0); } }); private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5); //限流阈值 private long limit = 15;
/** * 滑动时间窗口 * 每隔1s累加前6s内每1s的请求数量,判断是否超出限流阈值 */ public void slideWindow() { scheduledExecutorService.scheduleWithFixedDelay(() -> { try { long time = System.currentTimeMillis() / 1000; //每秒发送随机数量的请求 int reqs = (int) (Math.random() * 5) + 1; counter.get(time).addAndGet(reqs); long nums = 0; // 计算前6秒的累加请求数量 for (int i = 0; i < 6; i++) { nums += counter.get(time - i).get(); } log.info("time=" + time + ",nums=" + nums); if (nums > limit) { log.info("限流了,nums=" + nums); } } catch (Exception e) { log.error("slideWindow error", e); } finally { } }, 0, 1000, TimeUnit.MILLISECONDS); }}
漏桶算法
它有点像我们生活中用到的漏斗,液体倒进去以后,总是从下端的小口中以固定速率流出,漏桶算法也类似,不管突然流量有多大,漏桶都保证了流量以一定速率输出。但是如果你往漏斗中倒入水的速率超级快,速率远大于漏斗漏水的速率,那么就会导致水满则溢,溢出的水就只能丢弃了。 (PS:这里的水就相当于是“客户端发起的请求”,请求的人太多了,把漏斗都给溢满了,那就只能丢弃一部分了)
如下图:
代码实现:
/** * @ClassName : LeakyBucketLimitStream //类名 * @Description : 漏桶限流 //描述 * @Author : JavaAlliance //作者 * @Date: 2021-06-13 15:16 //时间 */public class LeakyBucketLimitStream { private Lock lock = new ReentrantLock(); // cas自旋锁 private volatile double remainWater = 0; //当前桶中的剩余的水数量 private volatile long lastTime = 0;//上次漏水的时间 private int capacity = 100; //桶容量大小 private int rate = 15;//漏水的速率
public boolean tryAcquire() { lock.lock(); try { long now = System.currentTimeMillis(); //计算这段时间,桶子漏掉了多少水 double outWater = (double) (now - lastTime) / 1000 * rate; if (outWater > 0) { lastTime = now; } //计算桶中剩余的水 remainWater = Math.max(0, remainWater - outWater); //如果桶没满,返回成功 if (remainWater <= capacity) { remainWater++; //本次请求要往桶里加一滴水 return true; } else { return false; } } finally { lock.unlock(); } }}
令牌桶算法
令牌桶算法是程序以固定速率向令牌桶中增加令牌,直到令牌桶满就停止往桶中添加新的令牌,请求到达时向令牌桶请求令牌,如获取到令牌则通过请求,否则触发限流策略
代码实现:
public class TokenBucketLimiter { // cas自旋锁 private Lock lock = new ReentrantLock(); //当前桶中令牌个数 private volatile double token = 0; //上次添加令牌的时间 private volatile long lastTime = 0; //桶容量大小 private int capacity = 0; //往令牌桶中放令牌的速率 private int rate = 15;
public boolean tryAcquire() { lock.lock(); try { Long now = System.currentTimeMillis(); //计算放入桶中的令牌数 double inToken = (double) (now - lastTime) / 1000 * rate; if (inToken > 0) { lastTime = now; } //计算桶中的令牌数 token = token + inToken; token = Math.min(token, capacity); //能否取到令牌 if (token - 1 >= 0) { token--; return true; } } finally { lock.unlock(); return false; } }}
好啦,本期的学习内容到此结束,充电完毕。我们下期再会!