高可用之限流-04-fixed window 固定窗口
限流系列
开源组件 rate-limit: 限流
高可用之限流-01-入门介绍
高可用之限流-02-如何设计限流框架
高可用之限流-03-Semaphore 信号量做限流
高可用之限流-04-fixed window 固定窗口
高可用之限流-05-slide window 滑动窗口
高可用之限流-06-slide window 滑动窗口 sentinel 源码
高可用之限流-07-token bucket 令牌桶算法
高可用之限流 08-leaky bucket漏桶算法
高可用之限流 09-guava RateLimiter 入门使用简介 & 源码分析
固定窗口
计数器的方案比较简单。比如限制1秒钟内请求数最多为10个,每当进来一个请求,则计数器+1。
当计数器达到上限时,则触发限流。
时间每经过1秒,则重置计数器。
核心代码
重置计数器可以通过 loop 循环,也可以通过 CountDownLatch 达到类似的效果。
此处演示一下 CountDownLatch 的实现版本
public class LimitFixedWindow extends LimitAdaptor {/*** 日志* @since 0.0.4*/private static final Log LOG = LogFactory.getLog(LimitFixedWindow.class);/*** 上下文* @since 0.0.4*/private final ILimitContext context;/*** 计数器* @since 0.0.4*/private AtomicInteger counter = new AtomicInteger(0);/*** 限制状态的工具** 避免不同线程的 notify+wait 报错问题** @since 0.0.4*/private CountDownLatch latch = new CountDownLatch(1);/*** 构造器* @param context 上下文* @since 0.0.4*/public LimitFixedWindow(ILimitContext context) {this.context = context;// 定时将 count 清零。final long interval = context.interval();final TimeUnit timeUnit = context.timeUnit();// 任务调度ExecutorServiceUtil.singleSchedule(new Runnable() {@Overridepublic void run() {initCounter();}}, interval, timeUnit);}@Overridepublic synchronized void acquire() {// 超过阈值,则进行等待if (counter.get() >= this.context.count()) {try {LOG.debug("[Limit] fixed count need wait for notify.");latch.await();LOG.debug("[Limit] fixed count need wait end ");} catch (InterruptedException e) {Thread.currentThread().interrupt();LOG.error("[Limit] fixed count is interrupt", e);}}// 结束int value = this.counter.incrementAndGet();this.latch = new CountDownLatch(1);LOG.debug("[Limit] fixed count is " + value);}/*** 初始化计数器* @since 0.0.4*/private void initCounter() {LOG.debug("[Limit] fixed count init counter start");// 通知可以继续执行(这里不能无脑 notify)会卡主if(this.counter.get() >= this.context.count()) {this.counter = new AtomicInteger(0);LOG.debug("[Limit] fixed count notify all start");latch.countDown();LOG.debug("[Limit] fixed count notify all end");} else {this.counter = new AtomicInteger(0);}}}
测试验证
public class LimitFixedWindowTest {private static final Log log = LogFactory.getLog(LimitFixedWindowTest.class);/*** 1S 内最多运行 1 次* @since 0.0.5*/private static final ILimit LIMIT = LimitBs.newInstance().interval(1).count(1).limit(LimitFixedWindow.class).build();static class LimitRunnable implements Runnable {@Overridepublic void run() {for(int i = 0; i < 3; i++) {LIMIT.acquire();log.info("{}-{}", Thread.currentThread().getName(), i);}}}public static void main(String[] args) {new Thread(new LimitRunnable()).start();new Thread(new LimitRunnable()).start();}}
- 日志
13:57:54.845 [Thread-2] INFO com.github.houbb.rate.limit.test.core.fixed.LimitFixedWindowTest - Thread-2-0
13:57:55.825 [Thread-3] INFO com.github.houbb.rate.limit.test.core.fixed.LimitFixedWindowTest - Thread-3-0
13:57:56.826 [Thread-2] INFO com.github.houbb.rate.limit.test.core.fixed.LimitFixedWindowTest - Thread-2-1
13:57:57.824 [Thread-3] INFO com.github.houbb.rate.limit.test.core.fixed.LimitFixedWindowTest - Thread-3-1
13:57:58.826 [Thread-2] INFO com.github.houbb.rate.limit.test.core.fixed.LimitFixedWindowTest - Thread-2-2
13:57:59.826 [Thread-3] INFO com.github.houbb.rate.limit.test.core.fixed.LimitFixedWindowTest - Thread-3-2
存在的不足
这种简单的实现存在的一个问题,就是在两个周期的临界点的位置,可能会存在请求超过阈值的情况。比如有恶意攻击的人在一个周期即将结束的时刻,发起了等于阈值的请求(假设之前的请求数为0),并且在下一个周期开始的时刻也发起等于阈值个请求。
则相当于在这接近一秒的时间内系统受到了2倍阈值的冲击,有可能导致系统挂掉。
下一节将讲述滑动窗口,解决这个问题。
参考资料
限流技术总结
基于循环数组实现的带滑动窗口的计数器限流算法