大家好,我是飘渺。SpringBoot老鸟系列的文章已经写了四篇,每篇的阅读反响都还不错,那今天继续给大家带来老鸟系列的第五篇,来聊聊在SpringBoot项目中如何对接口进行限流,有哪些常见的限流算法,如何优雅的进行限流(基于AOP)。

首先就让我们来看看为什么需要对接口进行限流?

为什么要进行限流?

因为互联网系统通常都要面对大并发大流量的请求,在突发情况下(最常见的场景就是秒杀、抢购),瞬时大流量会直接将系统打垮,无法对外提供服务。那为了防止出现这种情况最常见的解决方案之一就是限流,当请求达到一定的并发数或速率,就进行等待、排队、降级、拒绝服务等。

例如,12306购票系统,在面对高并发的情况下,就是采用了限流。 在流量高峰期间经常会出现提示语;“当前排队人数较多,请稍后再试!”

什么是限流?有哪些限流算法?

限流是对某一时间窗口内的请求数进行限制,保持系统的可用性和稳定性,防止因流量暴增而导致的系统运行缓慢或宕机。

常见的限流算法有三种:

1. 计数器限流

计数器限流算法是最为简单粗暴的解决方案,主要用来限制总并发数,比如数据库连接池大小、线程池大小、接口访问并发数等都是使用计数器算法。

如:使用 AomicInteger 来进行统计当前正在并发执行的次数,如果超过域值就直接拒绝请求,提示系统繁忙。

2. 漏桶算法

漏桶算法思路很简单,我们把水比作是请求,漏桶比作是系统处理能力极限,水先进入到漏桶里,漏桶里的水按一定速率流出,当流出的速率小于流入的速率时,由于漏桶容量有限,后续进入的水直接溢出(拒绝请求),以此实现限流。

3. 令牌桶算法

令牌桶算法的原理也比较简单,我们可以理解成医院的挂号看病,只有拿到号以后才可以进行诊病。

系统会维护一个令牌(token)桶,以一个恒定的速度往桶里放入令牌(token),这时如果有请求进来想要被处理,则需要先从桶里获取一个令牌(token),当桶里没有令牌(token)可取时,则该请求将被拒绝服务。令牌桶算法通过控制桶的容量、发放令牌的速率,来达到对请求的限制。

基于Guava工具类实现限流

Google开源工具包Guava提供了限流工具类RateLimiter,该类基于令牌桶算法实现流量限制,使用十分方便,而且十分高效,实现步骤如下:

第一步:引入guava依赖包

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

第二步:给接口加上限流逻辑

@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) 同上

第三步:体验效果

通过访问测试地址: 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次成功,其他都失败降级了,说明我们已经成功给接口加上了限流功能。

当然了,我们在实际开发中并不能直接这样用。至于原因嘛,你想呀,你每个接口都需要手动给其加上tryAcquire(),业务代码和限流代码混在一起,而且明显违背了DRY原则,代码冗余,重复劳动。代码评审时肯定会被老鸟们给嘲笑一番,啥破玩意儿!

所以,我们这里需要想办法将其优化 - 借助自定义注解+AOP实现接口限流。

基于AOP实现接口限流

基于AOP的实现方式也非常简单,实现过程如下:

第一步:加入AOP依赖

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

第二步:自定义限流注解

@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 "系统繁忙,请稍后再试.";
}

第三步:使用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);}
}

第四步:给需要限流的接口加上注解

@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";}
}

第五步:体验效果

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

正常响应时:

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

触发限流时:

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

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

小结

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

在完成优化后业务代码和限流代码解耦,开发人员只要一个注解,不用关心限流的实现逻辑,而且减少了代码冗余大大提高了代码可读性,代码评审时谁还能再笑话你?

好了,今天的文章到此就结束了,最后,我是飘渺Jam,一名写代码的架构师,做架构的程序员,期待您的转发与关注,当然也欢迎通过下方二维码添加我的个人微信,咱们一起聊技术!

老鸟系列源码已经上传至GitHub,需要的点击下方卡片关注并回复关键字 0923 获取

SpringBoot 如何进行限流?老鸟们都这么玩的!相关推荐

  1. SpringBoot 如何异步编程,老鸟们都这么玩的

    大家好,我是飘渺.今天继续给大家带来SpringBoot老鸟系列的第六篇,来聊聊在SpringBoot项目中如何实现异步编程. 老鸟系列文章导读: 1. SpringBoot 如何统一后端返回格式?老 ...

  2. vb6 由于超出容量限制 不能创建新事务_分布式限流?你也能轻松玩转(没啥新技术)...

    点击蓝色「日拱一兵」关注,持续侦破 Java 技术案件 一.什么是限流?为什么要限流? 不知道大家有没有做过帝都的地铁,就是进地铁站都要排队的那种,为什么要这样摆长龙转圈圈?答案就是为了限流!因为一趟 ...

  3. SpringBoot 如何进行参数校验,老鸟们都这么玩的!

    大家好,我是飘渺. 前几天写了一篇 SpringBoot如何统一后端返回格式?老鸟们都是这样玩的! 阅读效果还不错,而且被很多号主都转载过,今天我们继续第二篇,来聊聊在SprinBoot中如何集成参数 ...

  4. SpringBoot 如何生成接口文档,老鸟们都这么玩的!

    为什么要用Swagger ? " 作为一名程序员,我们最讨厌两件事:1. 别人不写注释.2. 自己写注释. 而作为一名接口开发者,我们同样讨厌两件事:1. 别人不写接口文档,文档不及时更新. ...

  5. SpringBoot 如何进行对象复制,老鸟们都这么玩的!

    ‍‍ ‍‍今天来聊聊在日常开发中如何优雅的实现对象复制. 首先我们看看为什么需要对象复制? 为什么需要对象复制 如上,是我们平时开发中最常见的三层MVC架构模型,编辑操作时Controller层接收到 ...

  6. SpringBoot 如何进行业务校验,老鸟们都这么玩的~

    大家好,我是飘渺. 今天继续给大家带来 SpringBoot老鸟系列 的第七篇,来聊聊在SpringBoot项目中如何实现业务异常校验Assert. 希望通过今天的文章,咱们能够了解到: 如何使用As ...

  7. ios手机python编译器免费_适用与IOS手机的python编辑器,让你不限空间,地点都能玩转pyhton代码 !...

    前言 现在越来越多人学习python,很多小伙伴都富有激情的,利用碎片化的时间都要学习,小编不得不佩服你们,大家都知道pyhton是简单易学的,但是光说不练,假把式,最好能编程并且运行,最好能有一款神 ...

  8. SpringCloud Gateway 通过redis实现限流

    前言 在高并发的系统中,往往需要在系统中做限流,一方面是为了防止大量的请求使服务器过载,导致服务不可用,另一方面是为了防止网络攻击. 常见的限流方式,比如Hystrix适用线程池隔离,超过线程池的负载 ...

  9. electron窗口自适应_Go 限流器系列(3)自适应限流

    漏斗桶/令牌桶确实能够保护系统不被拖垮, 但不管漏斗桶还是令牌桶, 其防护思路都是设定一个指标, 当超过该指标后就阻止或减少流量的继续进入,当系统负载降低到某一水平后则恢复流量的进入.但其通常都是被动 ...

最新文章

  1. 代码块练习题:看代码写程序的执行结果。
  2. C++STL容器vector
  3. windows----------windows10如何固定局域网ip
  4. SAP Commerce Accelerator和SAP Spartacus的技术对比
  5. kernel panic 和 kernel Oops
  6. java类似php魔术方法_PHP与类有关的几个魔术方法
  7. Lyft出售自动驾驶部门给丰田子公司:作价5.5亿美元
  8. 容器化RDS|计算存储分离 or 本地存储?
  9. html标记详解博客,HTML表格标记详解8:表格嵌套
  10. 想留长发没那么难,30个让头发快速生长的秘诀~
  11. AI搜索引擎优化工具-市场现状及未来发展趋势
  12. 中国大学mooc乐学python答案_中国大学mooc慕课_乐学Python_章节期末网课答案
  13. 影响债市行情的主要因素_决定债券收益的十大因素
  14. Android/安卓 半透明设置方法
  15. 破解背后的黑客,第1部分:如何绕过软件注册
  16. java---约数个数(每日一道算法2022.9.10)
  17. Multiple errors occurred while copying the files
  18. 男士最佳衣着选择搭配
  19. sinr的值在多少到多少之间
  20. html实现ezuikit.js萤石云直播监控,ezuikit.js实时监控实现,萤石云实时监控简单实现

热门文章

  1. 联想家悦微型计算机,联想家悦台式电脑拆机知识分享
  2. 七星购物巨亏3.8亿港元 拟转型空中沃尔玛
  3. 广州医保个账支付接口开发 微信医保支付
  4. Linux如何卸载slurm,在Ubuntu 16.04桌面上安装/模拟SLURM:slurmd无法启动
  5. 视觉SLAM十四讲:运动方程
  6. cmd命令行切换目录路径
  7. 三极管工作原理图解,快速了解三极管结构和工作原理
  8. 基于JSP的犯罪数据可视化系统
  9. unity动态修改标准材质自发光(Emission)
  10. Unity物体自发光