前言

在流量突增的场景下,为了保证后端服务在整体上一个稳定性,我们需要对请求进行限流,来避免系统崩溃。

不过限流会对少部分用户的请求直接进行拒绝或者延迟处理,影响这些用户的体验。

本文会介绍一些常见的限流算法,并在最后附上对分布式限流的一些思考。


计数器法

计数器算法,也成固定窗口法。可以控制在固定的时间窗口内,允许通过的最大的请求数。

例如,我们设定时间间隔窗口intervalWindow为1分钟,该窗口内的最大请求数max为100。

当第1个请求到来时,我们记录下当前窗口内的第一个请求的时间firstReqTime,那么之后的请求到来时,先判断是否进入下一个窗口。

如果进入,则直接重置firstReqTime为当前时间,该请求通过。如果没进入,再判断是否超过max。

demo如下(并没有考虑线程安全):

/*** @author qcy* @create 2021/12/04 18:20:30*/
public class CountLimiter {/*** 时间间隔窗口(单位:毫秒)*/private long intervalWindow;/*** 该窗口内的最大请求数*/private int max;/*** 当前窗口内的请求计数*/private int count;/*** 当前窗口内的第一个请求的时间*/private long firstReqTime = System.currentTimeMillis();public CountLimiter(int intervalWindow, int max) {this.intervalWindow = intervalWindow;this.max = max;}//省略get与set方法public boolean limit() {long now = System.currentTimeMillis();if (now > firstReqTime + intervalWindow) {//代表已经进入下一个窗口firstReqTime = now;count = 1;return true;}//还在当前的时间窗口内if (count + 1 <= max) {count++;return true;}return false;}}

计数器法,非常简单粗暴,以上demo只是单机式限流。

如果需要进行分布式限流,可以使用Redis。以接口名称作为key,max作为value,intervalWindow作为key过期时间。

当请求过来时,如果key不存在,则代表已经进入下一个窗口,value赋值为max-1,并允许请求通过。

如果key存在,则再判断value是否大于0。大于0则允许请求通过,否则进行限流。

使用Redis进行分布式限流,需要注意保证代码的原子性,可以直接使用lua脚本。

计数器法的缺点

该算法无法应对突发的流量,因为计数器法是固定窗口的。

例如第一个请求10:00:00到来,那么第一个时间窗口为10:00:00-10:01:00。之后在10:00:59时,突然来了99个请求,又在下一个时间窗口的10:01:01来了100个请求。

也就是说,在10:00:59-10:01:01的短短几秒内,共有199个请求到来,可能会瞬间压垮我们的应用。


滑动窗口法

滑动窗口法可以解决计数器在固定窗口法下无法应对突发流量的问题

固定窗口法是以第一个请求为窗口开始期,并向后截取intervalWindow长度,只有当窗口时间流逝完,才开辟新的窗口。

滑动窗口法以每一个请求为窗口结束期,向前截取intervalWindow长度,检查该范围内的请求总和,相当于会为每个请求开辟一个新窗口。

既然要知道前intervalWindow长度内到底有多少个请求,那么就要为每个放行的请求记录发生时间。

demo如下:

public class SlidingWindowLimiter {/*** 时间间隔窗口(单位:毫秒)*/private long intervalWindow;/*** 窗口内的最大请求数*/private int max;/*** 限流容器* 队列尾部保存最新通过的请求时间*/private LinkedList<Long> list = new LinkedList<>();public SlidingWindowLimiter(int intervalWindow, int max) {this.intervalWindow = intervalWindow;this.max = max;}//省略get与set方法public boolean limit() {long now = System.currentTimeMillis();//队列未满,说明当前窗口还可以接收请求if (list.size() < max) {list.addLast(now);return true;}//队列已满Long first = list.getFirst();if (now - first <= intervalWindow) {//说明新请求和队列中的请求还处于一个窗口内,触发了限流return false;}//说明新请求和队列中的请求不在通过窗口内list.removeFirst();list.addLast(now);return true;}
}

当然,也可以使用Redis的List或Zset实现吗,大致步骤和以上demo类似。

这里多说一句,限流中的滑动窗口法和TCP的滑动窗口其实很像。滑动窗口法是去主动限流,而TCP的滑动窗口则是接收方为了告诉发送方自己还能接受多少数据,是对发送方的“限流”。

滑动窗口法的缺点

在滑动窗口法中,因为要倒推窗口的开始期,所以需要记录每个请求的执行时间,会额外占用一些内存。

此外,在算法中会频繁地removeFirst与addLast,在选择错误的数据结构下(例如数组),可能会造成很大的移动开销。


漏桶法

水龙头可以通过松紧来控制出水的速率,下方有一个储蓄桶来保存当前的水。储蓄通底部有一个出口,内部的水会以恒定的速率从出口漏掉。

如果储蓄桶满了之后,再进来的水会全部溢出。只有当出水速率和漏水速率相同时,储蓄桶才会在不漏水的前提下达到最大的吞吐量。

我们把水比作请求,水龙头就是客户端。请求产生的速率肯定不是恒定的,但处理请求的速率是恒定的。当储蓄桶满了之后,请求产生的速率必须要和处理请求的速率一致。

demo如下:

public class LeakyBucketLimiter {/*** 上次请求到来的时间*/private long preTime = System.currentTimeMillis();/*** 漏水速率,n/s*/private int leakRate;/*** 储蓄桶容量*/private int capacity;/*** 当前水量*/private int water;public LeakyBucketLimiter(int leakRate, int capacity) {this.leakRate = leakRate;this.capacity = capacity;}//省略get与set方法public boolean limit() {long now = System.currentTimeMillis();//先漏水,计算剩余水量water = Math.max(0, water - (int) ((now - preTime) / 1000) * leakRate);preTime = now;//桶未满if (water + 1 <= capacity) {water++;return true;}return false;}
}

仔细一想,储蓄桶能够把不定速率的请求转化为恒定速率的请求,和消息队列一样,具有削峰填谷的作用。

其实整套装置和ScheduledThreadPoolExecutor线程池更像,将储蓄桶想象为具有延时特性的阻塞队列,超出队列容量的请求,将直接执行拒绝策略。

如果储蓄桶的容量为Integer.MAX_VALUE,流速为10/s,则可通过以下的代码来模拟漏桶:

        //最大任务数为Integer.MAX_VALUE,即储蓄桶容量ScheduledExecutorService pool = Executors.newScheduledThreadPool(30);//每隔0.1秒处理1个请求,即流速为10/spool.scheduleAtFixedRate(() -> System.out.println("处理请求"), 0, 100, TimeUnit.MILLISECONDS);

漏桶法的缺点

使用漏桶法去做限流,在业务平稳期其实已经够用了。但是在业务高峰期,我们又希望动态地去调整处理请求的速率,而不是一成不变的速率。

我们大可以动态地去改变参数leakRate的值,不过在计算剩余水量的时候,将会十分复杂。

因此,如果要考虑到对突发流量的控制,就不太推荐漏桶法了。


令牌桶法

首先有一个令牌桶,然后系统以一个恒定的速率向桶中放入令牌。当桶满时,会丢弃生成的令牌。

每有一个请求过来时,拿到令牌就可以执行,否则阻塞获取或者被直接丢弃。

一个简要的demo如下:

public class TokenBucketLimiter {/*** 上次请求到来的时间*/private long preTime = System.currentTimeMillis();/*** 放入令牌速率,n/s*/private int putRate;/*** 令牌桶容量*/private int capacity;/*** 当前令牌数*/private int bucket;public TokenBucketLimiter(int putRate, int capacity) {this.putRate = putRate;this.capacity = capacity;}//省略get与set方法public boolean limit() {long now = System.currentTimeMillis();//先放入令牌,再获取令牌bucket = Math.min(capacity, bucket + (int) ((now - preTime) / 1000) * putRate);preTime = now;if (bucket == 0) {return false;}bucket--;return true;}
}

看的出来,令牌桶和漏桶的原理有些相似。

漏桶是以一个恒定速率的出水,即处理请求的速率是恒定的。而令牌桶则是以一个恒定的速率往桶中放入令牌,在桶中令牌用完之前,并不限制处理请求的速率。

令牌桶的一个优势在于,可以允许短时间内的一次突发流量。但不会允许在短时间内的多次突发流量,因为令牌的填充也是需要时间的。

Guava中的RateLimiter

google的工具包Guava中的RateLimiter就是对令牌桶的实现,其包含了两种限流模式,位置处于SmoothRateLimiter的两个静态内部类中:

  • SmoothBursty,稳定模式,令牌生成的速率是恒定的,为默认模式。
  • SmoothWarmingUp,预热模式,逐渐提升令牌的生成速率到一固定值。

其中acquire方法支持阻塞式获取,tryAcquire支持获取不到就返回或者在指定时间内阻塞获取。

关于RateLimiter源码分析,后面应该会另起篇幅介绍。


分布式限流

以上的RateLimiter属于单机式限流,如果要进行分布式限流该怎么处理呢?

无非是将控制请求的阈值从单机中挪到统一的中间件上,例如Redis。

对于计数器法

如果要限制一天中对某个接口的调用次数,则可以使用接口的名称作为key,value作为预设的阈值,过期时间为24小时。请求到来时利用原子指令判断key是否存在,不存在则设置该key;存在则减1,再判断是否大于0。

对于滑动窗口法

单机中我们使用list,在分布式系统中,则可以使用Redis的有序集合zset,key为某个接口名称,value为处理请求的时间戳。请求到来时,先使用removeRangeByScore移除上一个时间窗口内的记录,接着使用size获取集合长度,若大于阈值,则进行限流。

对于漏桶法

阿里巴巴的开源分布式限流系统Sentienel,支持漏桶与令牌桶算法。

个人觉得非常有必要去了解一下Sentienel的整体架构,可以看这篇文章入门阿里巴巴开源限流系统 Sentinel 全解析

对于令牌桶法

可以在每个应用中起一个延时的线程池,定时生产令牌到Redis中,这种方案在水平扩展时可以同比例的扩大限流阈值,但性能不高。

当然也可以利用lua脚本,在lua脚本中直接将生产令牌与获取令牌的操作合在一起,即和上文的demo一样,先放入令牌再获取令牌。之后将脚本放在代码中,每个应用先判断Redis中是否存在该脚本,若不存在再加载该脚本,后续获取令牌时直接执行该脚本即可。

面试官:说说你了解几种限流算法,手写个demo?相关推荐

  1. 5种限流算法,7种限流方式,挡住突发流量?

    大家好啊,我是阿朗,最近工作中需要用到限流,这篇文章介绍常见的限流方式. 文章持续更新,可以关注公众号程序猿阿朗或访问未读代码博客. 本文 Github.com/niumoo/JavaNotes 已经 ...

  2. 5种限流算法,7种限流方式,挡住突发流量

    前言 最近几年,随着微服务的流行,服务和服务之间的依赖越来越强,调用关系越来越复杂,服务和服务之间的稳定性越来越重要.在遇到突发的请求量激增,恶意的用户访问,亦或请求频率过高给下游服务带来较大压力时, ...

  3. 限流算法-常见的4种限流算法

    首先我们先来看看什么是限流? 限流是指在系统面临高并发.大流量请求的情况下,限制新的流量对系统的访问,从而保证系统服务的安全性. 另一种解释:在计算机网络中,限流就是控制网络接口发送或接收请求的速率, ...

  4. 简单介绍4种限流算法!(固定窗口计数器算法、滑动窗口计数器算法、漏桶算法、令牌桶算法)...

    作者:架构小菜 链接:https://www.jianshu.com/p/7987bf427b5b 简单介绍 4 种非常好理解并且容易实现的限流算法! 一.固定窗口计数器算法 规定我们单位时间处理的请 ...

  5. 常见的几种限流算法代码实现(JAVA)

    最近在学习Sentinel组件需要了解限流算法相关的知识,正好在微信公众号上看到了一篇不错的文章,在此记录一下以下是原文链接. 年轻人,来手撸几种常见的限流算法! 限流算法接口 public inte ...

  6. 微服务限流及熔断一:四种限流算法(计数器算法、滑动窗口算法、令牌限流算法、漏桶限流算法)

    引言 本篇内容根据<spring cloud alibaba 微服务原理与实战>中内容摘取,希望和大家分享限流的思想,本篇不涉及代码层面的实现. 限流的目的 目的:通过限制并发访问数或者限 ...

  7. 面试官:说说RabbitMQ 消费端限流、TTL、死信队列

    欢迎关注方志朋的博客,回复"666"获面试宝典 1. 为什么要对消费端限流 假设一个场景,首先,我们 Rabbitmq 服务器积压了有上万条未处理的消息,我们随便打开一个消费者客户 ...

  8. 【169期】面试官问:说说为什么要限流,有哪些解决方案?

    点击上方"Java精选",选择"设为星标" 别问别人为什么,多问自己凭什么! 下方有惊喜留言必回,有问必答! 每天 08:35 更新文章,每天进步一点点... ...

  9. 一种限流算法-令牌桶算法

    业务背景 一般做接口限流主要是为了应对突发流量,避免突发流量拖垮服务.如下面一些场景就有可能发生突发流量 微博热搜 恶意刷单 恶意爬虫 促销活动 令牌桶算法 我们用一个桶来盛放令牌,假设桶的上限为10 ...

最新文章

  1. 2019 ACM - ICPC 西安邀请赛 B. Product (杜教筛) 简单数论(bushi)
  2. ISA SERVER日志存放SQL SERVER中
  3. CVPR 2018 VITAL:《VITAL: VIsual Tracking via Adversarial Learning》论文笔记
  4. 查询jsp servelet mysql_JSP + Servlet + JDBC + Mysql 实现增删改查 课程管理系统(示例代码)...
  5. STL源码剖析面试问题
  6. Guns 查询列表_入门试炼03
  7. HDU-2570-迷瘴
  8. visio中公式太小_串并联管道中调节阀的工作流量特性分析
  9. IIS错误信息--另一个程序正在使用此文件,进程无法访问!
  10. python随机森林变量重要性_随机森林如何评估特征重要性【机器学习面试题详解】...
  11. 互联网核心应用(搜索/推荐/广告)算法峰会
  12. 双边滤波方法原理与代码实践(附完整代码)
  13. dockerfile安装jenkins 并配置构建工具(node、npm、maven、git)
  14. java-net-php-python-jsp大麦公司网上拍卖商城计算机毕业设计程序
  15. 项目进度管理:制定进度计划
  16. Java的8种基本数据类型
  17. istio:灰度发布与AB测试
  18. Blender 制作刀光特效所用模型
  19. Linux下根据用户ID查询用户名
  20. 2022云计算真题:日志分析服务

热门文章

  1. 2022-2027年中国数码印花墨水行业市场全景评估及发展战略规划报告
  2. 华为交换机:本征VLAN
  3. 微服务:如何做好服务拆分?
  4. DES加密和解密工具,可以对字符串进行加密和解密操作
  5. 嵌入式Linux驱动开发(六)pinctrl和gpio子系统实验
  6. SpringBoot整合JPA+人大金仓(kingbase8)
  7. Java - 密码学
  8. BP神经网络算法基本原理,bp神经网络算法的优点
  9. linux 中prefetch文件夹,Prefetch是什么 Prefetch文件夹功能介绍
  10. 双功能螯合物修饰Cd-DTPA-ALP和DTPA-2NH碱性磷酸酶与双(3,5-二甲氧基-4-氨基二苯乙烯)