限流--4种经典限流算法讲解--单机限流和分布式限流的实现

news/2024/5/19 4:25:01
为什么需要限流

系统的维护使用是需要成本的,用户可能使用科技疯狂刷量,消耗系统资源,出现额外的经济开销
问题:

  1. 控制成本=>限制用户的调用次数
  2. 用户在短时间内疯狂使用,导致服务器资源被占满,其他用户无法使用=>限流

那么限流阈值多大合适?比如限制单个用户在每秒只能使用1次。

限流的算法

推荐阅读:https://juejin.cn/post/6967742960540581918

1)固定窗口限流
单位时间内允许部分操作
1小时只允许10个用户操作

优点:最简单
缺点:可能出现流量突刺
比如:前59分钟没有1个操作,第59分钟来了10个操作;第1小时又来了10个操作。相当于2分钟内执行了20个操作,服务器仍然有高峰危险。

2)滑动窗口限流
单位时间内允许部分操作,但是这个单位时间是滑动的,需要指定一个滑动单位。
滑动窗口与固定窗口相比,将一个时间段又划分成了几个更小的切片。随着时间的前进,滑动窗口不断向前加载切片,向后遗弃切片,但是切片的总长度是固定的,滑动窗口保证了自己时间段内所有的访问不会超过阈值。

优点:能够解决流量突刺问题,第59分钟和第1小时分为了两个切片,但是属于一个时间段,更多的操作会被拒绝
缺点:实现相对复杂,限流效果和滑动单位有关,滑动单位越小,限流效果越好,但往往很难选取到一个特别合适的滑动单位。

3)漏桶限流(推荐)
固定的速率处理请求(漏水),当请求桶满了后,拒绝请求。
每秒处理10个请求,同的容量是10,每0.1秒固定处理一次请求,如果1秒来了10个请求;都可以处理完,但如果1秒内来了11个请求,最后那个请求就会溢出桶,被拒绝。

优点:能够一定程度上应对流量突刺,能够以固定速率处理请求,保证服务器的安全
缺点:没有办法迅速处理一批请求,只能一个一个按顺序来处理(固定速率的缺点)

4)令牌桶限流(推荐)
管理员先生成一批令牌,每秒生成10个令牌;当用户要操作前,先去拿到一个令牌,有令牌的人就有资格执行操作、能同时执行操作;拿不到令牌就等着

优点:能够并发处理同时的请求,并发性能会更高
需要考虑的问题:还是存在时间单位选取的问题

想看这些算法的具体实现的话,可以参考这篇文章:4种经典限流算法讲解
在实际开发中,我们一般调用第三方库,无需关注这些算法的具体实现,只需要理解上边我说的这些算法的思想就行

限流粒度
  1. 针对某个方法限流,即单位时间内最多允许同时XX个操作使用这个方法
  2. 针对某个用户限流,比如单个用户单位时间内最多执行XX次操作
  3. 针对某个用户X方法限流,比如单个用户单位时间内最多执行XX次这个方法
限流的实现
1)本地限流(单机限流)

每个服务器单独限流,一般适用于单体项目,就是项目只有一个服务器

在Java中,使用Guava库实现单机限流:

Guava库提供了RateLimiter类,它使用令牌桶算法来控制请求的速率。

  1. 添加依赖:首先需要在项目的pom.xml文件中添加Guava库的依赖。
<dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>30.1-jre</version> <!-- 请使用最新版本 -->
</dependency>
  1. 创建RateLimiter实例:创建一个RateLimiter实例,设置每秒可以处理的请求数。
import com.google.common.util.concurrent.RateLimiter;RateLimiter rateLimiter = RateLimiter.create(5.0); // 每秒5个请求
  1. 限流操作:在需要限流的方法调用前,调用rateLimiter.acquire()来获取一个令牌。
public void doSomething() {if (rateLimiter.tryAcquire()) {// 执行操作} else {// 限流逻辑,如等待或返回错误}
}

4.**如何在第3点中的else里写等待逻辑:**可以实现几种不同的等待逻辑:

  1. 主动等待:使用 Thread.sleep() 或其他同步等待方法等待一段时间后再次尝试获取令牌。
  2. 被动等待:实现一个循环,不断尝试获取令牌,直到成功为止。
  3. 随机等待:使用随机等待时间以避免多个请求同时触发。
  4. 指数退避:等待时间逐渐增加,直到成功获取令牌。
  5. 返回错误:如果等待时间过长,可以选择返回错误,避免无限期的等待。

以下是几种等待逻辑的示例代码:

主动等待

解释public void doSomething() {if (rateLimiter.tryAcquire()) {// 执行操作} else {try {// 等待一段时间后再次尝试,例如等待500毫秒Thread.sleep(500);} catch (InterruptedException e) {Thread.currentThread().interrupt();// 可以选择返回错误或继续执行}// 再次尝试获取令牌if (rateLimiter.tryAcquire()) {// 执行操作} else {// 如果再次获取失败,可以选择返回错误或继续等待}}
}

被动等待(不断尝试)

解释public void doSomething() {while (!rateLimiter.tryAcquire()) {// 可以选择在这里实现一个短暂的等待,如Thread.sleep(10),以避免CPU占用过高}// 执行操作
}

随机等待

解释import java.util.Random;public void doSomething() {Random random = new Random();while (!rateLimiter.tryAcquire()) {try {// 随机等待一段时间,例如1到500毫秒之间int waitTime = random.nextInt(500);Thread.sleep(waitTime);} catch (InterruptedException e) {Thread.currentThread().interrupt();// 可以选择返回错误或继续执行}}// 执行操作
}

指数退避

复制解释public void doSomething() {int retries = 0;while (!rateLimiter.tryAcquire()) {try {// 指数退避等待时间Thread.sleep((int) (Math.pow(2, retries) * 100));} catch (InterruptedException e) {Thread.currentThread().interrupt();// 可以选择返回错误或继续执行}retries++;if (retries > MAX_RETRIES) {// 超过最大重试次数,可以选择返回错误break;}}// 执行操作
}

在实际应用中,选择哪种等待逻辑取决于你的具体需求和场景。例如,如果你希望避免重试对系统造成额外压力,可以选择随机等待或指数退避。如果你希望确保请求最终能够被处理,可以选择被动等待。如果你希望快速失败,可以选择返回错误。

2)分布式限流(多机限流)

如果项目有多个服务器,比如微服务,那么建议使用分布式限流。

  1. 把用户的使用频率等数据放到一个集中的存储进行统计,比如Redis,这样无论用户的请求落到了哪台服务器,都以集中的数据存储内的数据为准(Redisson - 是一个操作Redis的工具库)
  2. 在网关集中进行限流和统计(比如Sentinel、Spring Cloud Gateway)
import org.redisson.Redisson;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;public static void main(String[] args){//创建RedissonClientRedissonClient redisson = Redisson.create();//获取限流器RRateLimiter semaphore = redisson.getRateLimiter("mySemaphore");//尝试获取许可证boolean result = semaphore.tryAcquire();if(result){//处理请求}else{//超过流量限制,需要做何处理}}
Redisson限流实现

Redisson内置了一个限流工具类,可以借助Redis来存储统计。
官方仓库:https://github.com/redisson/redisson
看不懂方法的参数含义怎么办?

  1. 看官方文档
  2. 下载源码

image.png
点进任意一个包里的代码,然后点击下载源代码即可自动下载
image.png

步骤:

  1. 安装Redis
  2. 引入Redisson代码包:
        <dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.21.3</version></dependency>
  1. 创建RedissonConfig配置块,用于初始化RedissonClient对象单例
spring:redis:host: 127.0.0.1port: 6379database: 0
@Configuration
@ConfigurationProperties(prefix = "spring.redis")
@Data
public class RedissonConfig {private Integer database;private String host;private Integer port;private String password;@Beanpublic RedissonClient getRedissonClient() {Config config = new Config();config.useSingleServer().setDatabase(database).setAddress("redis://" + host + ":" + port).setPassword(password);RedissonClient redissonClient = Redisson.create(config);return redissonClient;}
}
  1. 编写RedisLimiterManager

什么是Manager?提供了通用的能力,可以放到任何一个项目里

/*** 专门提供RedisLimiter限流基础服务(提供了通用的能力)*/
@Service
public class RedisLimiterManager {@Resourceprivate RedissonClient redissonClient;/*** 限流操作* @param key 区分不同的限流器,比如不同的用户id应该分别统计*/public void doRateLimit(String key) {//创建一个名称为key的限流器,每秒最多访问2次RRateLimiter rateLimiter = redissonClient.getRateLimiter(key);rateLimiter.trySetRate(RateType.OVERALL, 2, 1, RateIntervalUnit.SECONDS);//每当一个操作来了之后,请求一个令牌boolean canOp = rateLimiter.tryAcquire(1);if (!canOp) {throw new BusinessException(ErrorCode.TOO_MANY_REQUEST);}}
}
  1. 单元测试:
@SpringBootTest
class RedisLimiterManagerTest {@Resourceprivate RedisLimiterManager redisLimiterManager;@Testvoid doRateLimit() throws InterruptedException {String userId = "1";for (int i = 0; i < 2; i++) {redisLimiterManager.doRateLimit(userId);System.out.println("成功");}Thread.sleep(2000);for (int i = 0; i < 5; i++) {redisLimiterManager.doRateLimit(userId);System.out.println("成功");}}
}
  1. 应用到要限流的方法中,比如智能分析接口:
//必须登录
User loginUser = userService.getLoginUser(request);
//限流判断,每个用户一个限流器
redisLimiterManager.doRateLimit("genChartByAi_" + loginUser.getId());

http://www.mrgr.cn/p/78422346

相关文章

初学React基础

最近准备跟着黑马React学一下React&#xff0c;扩充一下技术面&#xff0c;打算还是以一边学习一边记笔记为主&#xff0c;进行学习&#xff01; 1. React介绍 1.1. React是什么&#xff1f; React是由FaceBook现在称&#xff08;Meta&#xff09;开发的开源 JavaScript 库&a…

硬件21、接线端子XH2.54、2.54排针排母、2510接插件、PH2.0、町洋接线端子5.08、ISP接口JTAG插座

XH2.54端子的间距为2.54毫米&#xff0c;2.54排针排母的间距也是2.54mm&#xff0c;2510接插件也是2.54、而PH2.0端子的间距为2.0毫米&#xff0c;町洋接线端子插针间的距离是5.08mm&#xff0c;ISP接口JTAG插座针脚的间距一般也是2.54mm XH2.54 针脚间距为2.54mm 插头 接线…

如何将 redis 快速部署为 docker 容器?

部署 Redis 作为 Docker 容器是一种快速、灵活且可重复使用的方式&#xff0c;特别适合开发、测试和部署环境。本文将详细介绍如何将 Redis 部署为 Docker 容器&#xff0c;包括 Docker 安装、Redis 容器配置、数据持久化、网络设置等方面。 步骤 1&#xff1a;安装 Docker 首…

WDS+MDT网络启动自动部署windows(十四)如何调试自定义任务脚本

简介: 为了使MDT在系统安装时执行自定义操作,可以写vbs脚本或wsf脚本。 调试这些脚本确实是很有难度的事,我在上一篇设置计算机描述的实验中,尝试了两天,才完成测试,为什么呢?因为每次测试都用模拟机安装一遍windows,最近一个月,真的超过了几百遍重装windows。 中断(…

OceanBase 助力同方智慧能源,打造安全可靠、高性能的能源数据架构

本文作者&#xff1a;丁泽斌&#xff0c;同方智慧能源数据库工程师 业务背景 作为同方股份有限公司旗下的领军企业&#xff0c;同方智慧能源集团矢志成为全球领先的综合智慧能源解决方案提供商。凭借中核集团和清华大学的科技实力&#xff0c;专注于向建筑、交通、工业、北方供…

Windows查找JDK的安装路径

如果很久之前安装了JDK&#xff0c;或者在别人的电脑上&#xff0c;想要快速指导JDK 的安装路径&#xff0c;可以通过啥方式指导JDK的安装路径是在哪里呢&#xff1f; 一、确认是否安装了JDK 首先我们打开命令行&#xff0c;如果输入 java -version 如果显示这种&#xff0c;…

二维泊松方程(Neumann+Direchliet边界条件)有限元Matlab编程求解|程序源码+说明文本

专栏导读 作者简介&#xff1a;工学博士&#xff0c;高级工程师&#xff0c;专注于工业软件算法研究本文已收录于专栏&#xff1a;《有限元编程从入门到精通》本专栏旨在提供 1.以案例的形式讲解各类有限元问题的程序实现&#xff0c;并提供所有案例完整源码&#xff1b;2.单元…

ruoyi漏洞总结

若依识别 黑若依 :icon hash"-1231872293 绿若依 :icon hash"706913071” body" 请通过前端地址访 " body" 认证失败&#xff0c;无法访问系统资源 " 如果页面访问显示不正常&#xff0c;可添加默认访问路径尝试是否显示正常 /login?redi…

OpenFeign修改HttpClient为Apache HttpClient 5

OpenFeign中http client 如果不做特殊配置&#xff0c;OpenFeign默认使用JDK自带的HttpURLConnection发送HTTP请求&#xff0c; 由于默认HttpURLConnection没有连接池、性能和效率比较低。所以修改为Apache HttpClient 5。 总结为两步&#xff1a; 加依赖改yml 具体操作请往…

开关门机关

根物体创建动画 子物体录制动画 ctrl6&#xff1a;调用动画窗口 添加关键帧&#xff1a;输入添加关键帧到第几帧&#xff0c;然后点击录制&#xff0c;最后在该物体的面板上修改其位置等&#xff0c;记得添加完要结束录制 搞个父物体是为了让动画的可移植性变高 设置触发器方…

这是一个简单的照明材料网站,后续还会更新

1、首页效果图 代码 <!DOCTYPE html> <html><head><meta charset"utf-8" /><title>爱德照明网站首页</title><style>/*外部样式*/charset "utf-8";*{margin: 0;padding: 0;box-sizing: border-box;}a{text-dec…

事件标志组

什么是事件标志组&#xff1f; 事件标志位&#xff1a;表明某个事件是否发生&#xff0c;联想&#xff1a;全局变量 flag。通常按位表示&#xff0c;每一个位表示一个事件&#xff08;高8位不算&#xff09;事件标志组是一组事件标志位的集合&#xff0c; 可以简单的理解事件标…

docker原理

Docker原理 在前面我们学习了Docker&#xff0c;接下来我们探究一下Docker的底层技术原理 Linux 命名空间&#xff08;namespace&#xff09;、控制组&#xff08;cgroups&#xff09;和 联合文件系统&#xff08;UnionFS&#xff09; 三大技术支撑了目前 Docker 的实现&…

【多级缓存】多级缓存OpenResty,Canal,nginx本地缓存

多级缓存 安装OpenRestyOpenResty入门OpenResty获取请求参数OpenResty向tomcat服务器发送请求 在nginx与tomcat端之间添加redis缓存Redis本地缓存缓存同步缓存同步策略基于Canal的异步通知安装Canal Canal客户端 安装OpenResty OpenResty是一个基于 Nginx的高性能 Web 平台&am…

路由器的构成

一、路由器简介 路由器是互联网中的关键设备&#xff1a; 连接不同的网络路由器是多个输入端口和多个输出端口的专用计算机&#xff0c;其任务是转发分组&#xff08;转发给下一跳路由器&#xff09;下一跳路由器也按照这种方法处理分组&#xff0c;直到该分组到达终点为止 …

Redisson 分布式锁和同步器

系列文章目录 文章目录 系列文章目录前言前言 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站,这篇文章男女通用,看懂了就去分享给你的码吧。 redisson 是基于redis的扩展库,使得redis除了应用于缓存以外,还能做队列…

mysql 远程无法连接

mysql 远程无法连接,解决办法:https://blog.csdn.net/Dontla/article/details/133213538

Vue工程化开发和脚手架Vue CLI

目录 一、介绍 二、使用步骤 1. 全局安装&#xff08;一次&#xff09; 2.查看Vue版本 3.创建项目架子&#xff08;项目名不能使用中文&#xff09; 4.启动项目 一、介绍 Vue CLI是Vue官方提供的一个全局命令工具。可以帮助我们快速创建一个开发的Vue项目的标准化基础架子…

深入学习Linux内核页框回收

目录 算法 1.选择目标页 2.PFRA设计 3.反向映射 3.1.匿名页的反向映射 3.2.try_to_unmap_anon()函数 3.3.try_to_unmap_one()函数 映射页的反向映射 优先搜索树 try_to_unmap_file()函数 PFRA实现 最近最少使用(LRU)链表 在LRU链表之间移动页 mark_page_accessed(…

《动手学深度学习》V2(00-10)

文章目录 一、学习目标二、环境搭建三、数据操作1、张量介绍2、运算符介绍3、广播介绍4、索引和切片5、节省内存6、课后练习实现 :fire: 四、数据预处理1、读取数据集2、处理缺失数据3、课后练习实现 :fire:①第一步&#xff1a;造数据②第二步 筛选遍历缺失值③第三步 统计降序…