guava限流器RateLimiter使用简介(Springboot实现)

在大型分布式系统中,限流是一种重要的防护机制,可以帮助我们控制流量并减轻系统的负担。Google的Guava库提供了一种方便的限流器实现,可以帮助我们轻松地实现限流功能。本文将介绍Guava中限流器的基本概念和使用方法。

一、什么是限流器?

限流器是一种控制系统流量的机制,可以帮助我们在高负载情况下保护系统免受过载和崩溃的风险。通过限制进入系统的请求速率,我们可以避免服务器过载和资源耗尽的问题。

常用的限流方式和场景有:

  1. 限制总并发数(比如数据库连接池、线程池)
  2. 限制瞬时并发数(如nginx的limitconn模块,用来限制瞬时并发连接数,Java的Semaphore也可以实现)
  3. 限制时间窗口内的平均速率(如Guava的RateLimiter、nginx的limitreq模块,限制每秒的平均速率)
  4. 其他:比如如限制远程接口调用速率、限制MQ的消费速率。另外还可以根据网络连接数、网络流量、CPU或内存负载等来限流。

二、Guava中限流器的实现原理

Guava中的限流器实现基于令牌桶算法,它是一种流量控制算法,可以帮助我们平滑地限制请求速率。限流器将请求速率限制为指定的速率,以确保系统能够在高负载情况下保持稳定。
Guava中的限流器使用一个桶来保存令牌,桶中的令牌代表可用的请求数量。每当一个请求到达时,限流器会尝试从桶中取出一个令牌,如果桶中没有可用的令牌,则该请求将被阻塞,直到有足够的令牌可用为止。当令牌被用尽时,限流器将根据配置的速率自动添加新的令牌到桶中,以确保系统能够继续处理请求。

guava单机限流RateLimiter简介

RateLimiter是基于令牌桶算法实现的一个限流组件,其代码看起来很简单,一共就两个类:抽象父类 RateLimiter和实际的 SmoothRateLimiter。其中 SmoothWarmingUPSmmothBurstySmoothRateLimiter的两个内部类。但实际真的要看懂也需要花点时间的,这里其实主要就是算法上的考虑不好看懂。

RateLimiter提供了两种限流模式:

  1. 普通的限流SmoothBursty
  2. 带预热的限流。即在指定预热期,允许放过的流量逐渐增加。预热期结束后,允许放过的流量就等于设定的限流值。这个目的是为了解决软件重启等情况,由于缓存等还没有初始化化、jvm还是解释执行等,能够承受的流量比稳定运行后更小,防止在服务刚刚启动就被大流量打挂了,所以RateLimiter提供了一个预热器。

使用方式:

当然也提供了非阻塞的tryAcquire()方法。

RateLimiter属性简介

  1. stableIntervalMicros
令牌产生的稳定速率,只是这里的速率是转换成了两个令牌生产之间的时间间隔(毫秒)。之所以是稳定速率,是因为SmoothWarmUp,在预热阶段产生令牌的速率会低于这个值。RateLimiter初始化的时候,传入的permitsPerSecond表示的是每秒产生的产生的令牌个数,也就是说令牌的生产速率的时间单位就给固定了,那么stableIntervalMicros = 1s/permitsPerSecond。
Ps:速率(单位时间生产个数) = 时间段内总个数/时间长度 = 时间内长度/生产两个令牌的时间间隔 可以来表示生效速率。反过来使用两个令牌生产间隔其实也就可以表示速率。
  1. maxPermits
令牌桶的容量,即令牌桶中最大的令牌数。对于无预热的限流器, maxPermits = 1s/stableIntervalMicros。之所以要这么计算,是因为初始化RateLimiter时,传入的permitsPerSecond是个double,用小数来表达限流窗口不是1s的情况。
而对于有预热的限流器,预热期间,就是1s/stableIntervalMicros的一半。预热结束就是1s/stableIntervalMicros。
所以这个maxPermits的最大值,其实就是初始化RateLimiter的时候设置的限流阈值做了整数转换。
  1. storedPermits
令牌桶中当前拥有令牌的个数。
  1. nextFreeTicketMicros

下次无需等待就能直接获取token的时间。它的值的计算包含两部分:

  1. 按照正常速率生产令牌,下一次能够直接获得令牌的。比如令牌的生产速率是每秒100个,如果在T1时刻将令牌桶token清空了,那么下次无需等待就能获得令牌的时间就是T1之后的100ms处。

  1. 预支的令牌生产的时间。RateLimiter为了支持一定的流量突发,当一次调用acquire()的时候,如果当前令牌桶中没有足够的令牌,也不会阻塞当前acquire()请求,而是直接返回,然后将预支的那些令牌的生产时间累加到nextFreeTicketMicros,然后下次调用acquire()的时候就会阻塞更久。
比如:当前令牌桶中的令牌数storedPermits=2,但是acquire(5)的时候不会立马阻塞,而是将超支的3个令牌的生产时间转义到下次调用acquire()的时候。
即nextFreeTicketMicros += 3*100ms。
RateLimiter的设计哲学: 它允许瞬间的流量波峰超过QPS,但瞬间过后的请求将会等待较长的时间来缓解上次的波峰,以使得平均的QPS等于预定值

这4个参数是SmoothBursty和SmoothWarmUp共有的,且维护逻辑也都是一样的。

SmoothBursty自己的属性

maxBurstSeconds:这个是影响maxPermits,在计算maxPermits的时候,实际是maxBurstSeconds * permitsPerSecond,如果maxBurstSeconds大于1,那其实就允许令牌桶中多余初始化RateLimiter时指定的阈值,以应对一定的突发流量。在guava 30.1版本中,这个值还是写死的1.0.

SmoothWarmUp自己的属性:

  1. warmupPeriodMicros
预热期时间长度,这个是初始化RateLimiter传入的。
  1. thresholdPermits:预热期内令牌桶内最大的令牌数。
其值=0.5 * warmupPeriodMicros/stableIntervalMicros
  1. coldFactor:
预热期令牌生产速率的减缓因子。正常情况下,令牌的生产间隔就是stableIntervalMicros = 1s/permitsPerSecond,而在预热期,令牌的生产速率=coldFactor * stableIntervalMicros。
在guava 30.1版本中,这个值还是写死的3.0。
所以,对于限流阈值设置成1s内100个,那么stableIntervalMicros=100ms,但是在预热期令牌生产速率=3*100ms=300ms.
  1. slope
其值=(stableIntervalMicros * coldFactor - stableIntervalMicros) / (maxPermits - thresholdPermits)

三、如何使用Guava中的限流器?

在Guava中使用限流器非常简单,只需要按照以下步骤即可:

1.创建一个限流器对象

Guava中的限流器对象是通过RateLimiter类实现的。您可以使用RateLimiter.create(double permitsPerSecond)方法创建一个限流器对象,其中参数permitsPerSecond是每秒允许的请求数。

例如,以下代码创建一个每秒允许10个请求的限流器对象:

RateLimiter limiter =RateLimiter.create(10);

2.获取许可证

当您的应用程序需要处理一个请求时,可以使用RateLimiter.acquire()方法获取许可证。这个方法将会阻塞,直到有足够的令牌可用。如果限流器没有可用的令牌,则该方法将阻塞,直到有足够的令牌可用。

例如,以下代码获取一个许可证并处理请求:

limiter.acquire();
handleRequest();

3.尝试获取许可证

如果您希望尝试获取许可证而不是阻塞,则可以使用RateLimiter.tryAcquire()方法。这个方法将尝试获取许可证,如果限流器没有可用的令牌,则该方法将返回false,否则将返回true。

例如,以下代码尝试获取一个许可证:

if (limiter.tryAcquire()) {handleRequest();
} else {handleRateLimitedRequest();
}

4.调整速率

您还可以使用RateLimiter.setRate(double permitsPerSecond)方法动态地调整限流器的速率。这个方法可以帮助您根据系统负载和流量需求动态调整请求速率。

例如,以下代码动态调整限流器的速率:

limiter.setRate(20);

5.处理多个请求

如果您需要处理多个请求,则可以使用RateLimiter.acquire(int permits)方法获取多个许可证。这个方法将会阻塞,直到有足够的令牌可用。

例如,以下代码获取5个许可证并处理5个请求:

limiter.acquire(5);
handleRequest1();
handleRequest2();
handleRequest3();
handleRequest4();
handleRequest5();

四、Guava限流器的注意事项

在使用Guava限流器时,需要注意以下几点:

  1. 限流器的速率应该根据系统负载和流量需求进行动态调整。
  2. 如果您需要处理多个请求,请确保您的限流器对象能够处理足够的请求数量。
  3. 在高负载情况下,限流器可能会导致请求超时或错误,请确保您的应用程序能够处理这些情况。
  4. 限流器的速率应该设置为适当的值,以确保系统能够处理所有的请求。

五、总结

Guava中的限流器提供了一种简单而有效的方法来控制系统流量,以保护系统免受过载和崩溃的风险。使用Guava限流器,您可以轻松地实现限流功能,并根据系统负载和流量需求动态调整请求速率。在实现分布式系统时,使用限流器是一种重要的防护机制,可以帮助我们确保系统的稳定性和可靠性。

六、SpringBoot使用限流器示例

1、pom文件添加依赖

<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1-jre</version>
</dependency>

2、给接口加上限流逻辑

@Slf4j
@RestController
@RequestMapping("/limit")
public class LimitController {/*** 限流策略 :1秒钟2个请求*/private final RateLimiter limiter = RateLimiter.create(2.0);private DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");@GetMapping("/test1")public String testLimiter() {// 500毫秒内,没拿到令牌,就直接进入服务降级boolean tryAcquire = limiter.tryAcquire(500, TimeUnit.MILLISECONDS);if (!tryAcquire) {log.warn("进入服务降级,时间{}", LocalDateTime.now().format(dtf));return "当前排队人数较多,请稍后再试!";}log.info("获取令牌成功,时间{}", LocalDateTime.now().format(dtf));return "请求成功";}
}

以上用到了RateLimiter的2个核心方法:create()、tryAcquire(),以下为详细说明

  • acquire() 获取一个令牌, 改方法会阻塞直到获取到这一个令牌, 返回值为获取到这个令牌花费的时间
  • acquire(int permits) 获取指定数量的令牌, 该方法也会阻塞, 返回值为获取到这 N 个令牌花费的时间
  • tryAcquire() 判断时候能获取到令牌, 如果不能获取立即返回 false
  • tryAcquire(int permits) 获取指定数量的令牌, 如果不能获取立即返回 false
  • tryAcquire(long timeout, TimeUnit unit) 判断能否在指定时间内获取到令牌, 如果不能获取立即返回 false
  • tryAcquire(int permits, long timeout, TimeUnit unit) 同上

3、体验效果

通过访问测试地址:http://127.0.0.1:8080/limit/test1,反复刷新并观察后端日志

WARN  LimitController:35 - 进入服务降级,时间2021-09-25 21:39:37
WARN  LimitController:35 - 进入服务降级,时间2021-09-25 21:39:37
INFO  LimitController:39 - 获取令牌成功,时间2021-09-25 21:39:37
WARN  LimitController:35 - 进入服务降级,时间2021-09-25 21:39:37
WARN  LimitController:35 - 进入服务降级,时间2021-09-25 21:39:37
INFO  LimitController:39 - 获取令牌成功,时间2021-09-25 21:39:37WARN  LimitController:35 - 进入服务降级,时间2021-09-25 21:39:38
INFO  LimitController:39 - 获取令牌成功,时间2021-09-25 21:39:38
WARN  LimitController:35 - 进入服务降级,时间2021-09-25 21:39:38
INFO  LimitController:39 - 获取令牌成功,时间2021-09-25 21:39:38

从以上日志可以看出,1秒钟内只有2次成功,其他都失败降级了,说明已经成功给接口加上了限流功能。

七、SpringBoot基于AOP实现接口限流

1、加入AOP依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2、自定义限流注解

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface Limit {/*** 资源的key,唯一* 作用:不同的接口,不同的流量控制*/String key() default "";/*** 最多的访问限制次数*/double permitsPerSecond () ;/*** 获取令牌最大等待时间*/long timeout();/*** 获取令牌最大等待时间,单位(例:分钟/秒/毫秒) 默认:毫秒*/TimeUnit timeunit() default TimeUnit.MILLISECONDS;/*** 得不到令牌的提示语*/String msg() default "系统繁忙,请稍后再试.";
}

3、使用AOP切面拦截限流注解

@Slf4j
@Aspect
@Component
public class LimitAop {/*** 不同的接口,不同的流量控制* map的key为 Limiter.key*/private final Map<String, RateLimiter> limitMap = Maps.newConcurrentMap();@Around("@annotation(com.jianzh5.blog.limit.Limit)")public Object around(ProceedingJoinPoint joinPoint) throws Throwable{MethodSignature signature = (MethodSignature) joinPoint.getSignature();Method method = signature.getMethod();//拿limit的注解Limit limit = method.getAnnotation(Limit.class);if (limit != null) {//key作用:不同的接口,不同的流量控制String key=limit.key();RateLimiter rateLimiter = null;//验证缓存是否有命中keyif (!limitMap.containsKey(key)) {// 创建令牌桶rateLimiter = RateLimiter.create(limit.permitsPerSecond());limitMap.put(key, rateLimiter);log.info("新建了令牌桶={},容量={}",key,limit.permitsPerSecond());}rateLimiter = limitMap.get(key);// 拿令牌boolean acquire = rateLimiter.tryAcquire(limit.timeout(), limit.timeunit());// 拿不到命令,直接返回异常提示if (!acquire) {log.debug("令牌桶={},获取令牌失败",key);this.responseFail(limit.msg());return null;}}return joinPoint.proceed();}/*** 直接向前端抛出异常* @param msg 提示信息*/private void responseFail(String msg)  {HttpServletResponse response=((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();ResultData<Object> resultData = ResultData.fail(ReturnCode.LIMIT_ERROR.getCode(), msg);WebUtils.writeJson(response,resultData);}
}

4、给需要限流的接口加上注解

@Slf4j
@RestController
@RequestMapping("/limit")
public class LimitController {@GetMapping("/test2")@Limit(key = "limit2", permitsPerSecond = 1, timeout = 500, timeunit = TimeUnit.MILLISECONDS,msg = "当前排队人数较多,请稍后再试!")public String limit2() {log.info("令牌桶limit2获取令牌成功");return "ok";}@GetMapping("/test3")@Limit(key = "limit3", permitsPerSecond = 2, timeout = 500, timeunit = TimeUnit.MILLISECONDS,msg = "系统繁忙,请稍后再试!")public String limit3() {log.info("令牌桶limit3获取令牌成功");return "ok";}
}

5、体验效果

通过访问测试地址:http://127.0.0.1:8080/limit/test2,反复刷新并观察输出结果:

正常响应时:

{"status":100,"message":"操作成功","data":"ok","timestamp":1632579377104}

触发限流时:

{"status":2001,"message":"系统繁忙,请稍后再试!","data":null,"timestamp":1632579332177}

通过观察得之,基于自定义注解同样实现了接口限流的效果。

6、小结

一般在系统上线时我们通过对系统压测可以评估出系统的性能阈值,然后给接口加上合理的限流参数,防止出现大流量请求时直接压垮系统。今天我们介绍了几种常见的限流算法(重点关注令牌桶算法),基于Guava工具类实现了接口限流并利用AOP完成了对限流代码的优化。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/28480.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

实现小程序商城首页【源码公开】

效果图 页面源码 <view class"index-container"><view class"header"><!--搜索框【仅样式&#xff0c;不做处理】 start--><van-search bindtap"clickSearch" disabled shape"round" background"#9c7bf0&q…

GPT与人类:人工智能是否能够真正复制人类语言?

人类语言是一种复杂的系统&#xff0c;它不仅包含着无数单词和语法规则&#xff0c;更重要的是具有丰富的含义和上下文。这些语言特征涉及到常识、文化、情感和经验等方面&#xff0c;是人类在长期进化和文明发展中所积累起来的丰富知识和经验的体现。然而&#xff0c;人工智能…

Java对日志文件进行加密

最近碰到了一个新的需求&#xff0c;生产环境中Java程序部署的服务器会定期清理数据&#xff0c;需要将保存在程序所在服务器上的日志文件挂载到网盘上&#xff0c;但又不想让用户看到日志文件中的信息&#xff0c;因此需要对日志文件中的内容进行加密。 这里&#xff0c;并不是…

2023牛客暑期多校训练营1(D/H/J/K)

目录 D.Chocolate H.Matches J.Roulette K.Subdivision D.Chocolate 思路&#xff1a;当n1且m1时候先手必输&#xff0c;然后1*k&#xff08;k>2&#xff09;的情况下后手必输&#xff0c;因为先手可以选到只剩下一个格子。而在其它情况里先手第一步可以先选(1,1)的格子…

ARM 架构是什么?

ARM&#xff08;Advanced RISC Machines&#xff09;架构是一种处理器架构&#xff0c;它是一种精简指令集计算机&#xff08;RISC&#xff09;架构。ARM架构最初由ARM Holdings&#xff08;现在是SoftBank Group的子公司&#xff09;开发&#xff0c;并在1980年代末和1990年代…

sort部分

sort主要针对文件内容的操作&#xff0c;对文件内容进行匹配或者过滤&#xff0c;排序 grep 过滤 针对文本内容进行过滤&#xff0c;也就是查找 -i&#xff1a;忽略大小写默认的&#xff0c;可以不加 -n&#xff1a;显示匹配的行号 -c&#xff1a;只统计匹配的行数 &#…

Python 列表 sort()函数使用详解

「作者主页」&#xff1a;士别三日wyx 「作者简介」&#xff1a;CSDN top100、阿里云博客专家、华为云享专家、网络安全领域优质创作者 「推荐专栏」&#xff1a;小白零基础《Python入门到精通》 sort函数使用详解 1、升序降序2、sort()和sorted()的区别3、切片排序4、指定排序…

地面点云提取:Autoware预处理ray_ground_filter节点解析 + 解决ray_ground_filter无输出的问题

文章目录 一、解决Autoware的ray_ground_filter节点无点云输出的问题二、ray_ground_filter节点代码分析2.1.监听bask_link和velodyne之间的TF2.2 裁切过高点云2.3 消除雷达近身反射点的影响2.4 角度和距离微分&#xff08;转换到柱坐标&#xff09;2.5 地面判断(核心部分) 一、…

Python numpy求均值、保留几位小数

import numpy as nplist_test [0.21, 0.32]print(f{np.mean(list_test):.2f}) #保留两位小数 print(f{np.mean(list_test):.3f}) #保留三位小数

网络虚拟化相关的Linux接口介绍

Linux拥有丰富的网络虚拟化功能&#xff0c;能被虚拟机&#xff0c;容器还有云网络使用。在这篇文章中&#xff0c;我会给出所有通用网络虚拟化接口的简要介绍。没有代码分析&#xff0c;只有简短的接口介绍和在Linux上的使用操作。这系列接口都可以使用ip link命令实现。 这篇…

Linux进程控制(一)---进程创建和终止(写时拷贝,exit与_exit等)

目录 进程创建 fork()函数 子进程如何继承父进程的数据 1.创建时拷贝分离 2.写时拷贝★ 进程终止 进程终止时&#xff0c;操作系统做了什么&#xff1f; 进程终止的常见方式 代码运行完毕&#xff0c;结果正确 退出码★ 代码运行完毕&#xff0c;结果不正确 代码异…

Echarts柱状图横向滚动,如何实现从后往前滚动

Echarts柱状图横向滚动&#xff0c;如何实现从后往前滚动 设置开始和结束的横坐标&#xff0c;设置产生横向滚动条