SpringBoot接口 - 如何实现接口限流之单实例
在以SpringBoot开发Restful接口时,当流量超过服务极限能力时,系统可能会出现卡死、崩溃的情况,所以就有了降级和限流。在接口层如何做限流呢? 本文主要回顾限流的知识点,并实践单实例限流的一种思路。
- SpringBoot接口 - 如何实现接口限流之单实例
- 准备知识点
- 为什么要限流
- 限流有哪些常见思路?
- 实现思路
- 定义RateLimit注解
- 定义AOP
- 自定义相关异常
- 统一结果返回封装
- controller接口
- 接口测试
- 上述实现方案的槽点
- 示例源码
- 准备知识点
# 准备知识点
主要的知识点,请参考架构之高并发:限流, 这里小结下。
# 为什么要限流
每个系统都有服务的上线,所以当流量超过服务极限能力时,系统可能会出现卡死、崩溃的情况,所以就有了降级和限流。限流其实就是:当高并发或者瞬时高并发时,为了保证系统的稳定性、可用性,系统以牺牲部分请求为代价或者延迟处理请求为代价,保证系统整体服务可用。
# 限流有哪些常见思路?
- 从算法上看
令牌桶(Token Bucket)、漏桶(leaky bucket)和计数器算法是最常用的三种限流的算法。
- 单实例
应用级限流方式只是单应用内的请求限流,不能进行全局限流。
- 限流总资源数
- 限流总并发/连接/请求数
- 限流某个接口的总并发/请求数
- 限流某个接口的时间窗请求数
- 平滑限流某个接口的请求数
- Guava RateLimiter
- 分布式
我们需要分布式限流和接入层限流来进行全局限流。
- redis+lua实现中的lua脚本
- 使用Nginx+Lua实现的Lua脚本
- 使用 OpenResty 开源的限流方案
- 限流框架,比如Sentinel实现降级限流熔断
# 实现思路
主要思路:AOP拦截自定义的RateLimit注解,在AOP中通过Guava RateLimiter; Guava RateLimiter提供了令牌桶算法实现:平滑突发限流(SmoothBursty)和平滑预热限流(SmoothWarmingUp)实现。
# 定义RateLimit注解
package tech.pdai.ratelimit.guava.config.ratelimit;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** @author pdai*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {int limit() default 10;}
# 定义AOP
package tech.pdai.ratelimit.guava.config.ratelimit;import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;import com.google.common.util.concurrent.RateLimiter;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;/*** @author pdai*/
@Slf4j
@Aspect
@Component
public class RateLimitAspect {private final ConcurrentHashMap<String, RateLimiter> EXISTED_RATE_LIMITERS = new ConcurrentHashMap<>();@Pointcut("@annotation(tech.pdai.ratelimit.guava.config.ratelimit.RateLimit)")public void rateLimit() {}@Around("rateLimit()")public Object around(ProceedingJoinPoint point) throws Throwable {MethodSignature signature = (MethodSignature) point.getSignature();Method method = signature.getMethod();RateLimit annotation = AnnotationUtils.findAnnotation(method, RateLimit.class);// get rate limiterRateLimiter rateLimiter = EXISTED_RATE_LIMITERS.computeIfAbsent(method.getName(), k -> RateLimiter.create(annotation.limit()));// processif (rateLimiter!=null && rateLimiter.tryAcquire()) {return point.proceed();} else {throw new RuntimeException("too many requests, please try again later...");}}
}
# 自定义相关异常
package tech.pdai.ratelimit.guava.config.exception;import lombok.extern.slf4j.Slf4j;/*** business exception, besides normal exception.** @author pdai*/
@Slf4j
public class BusinessException extends RuntimeException {/*** Constructs a new exception with {@code null} as its detail message. The cause is not initialized, and may* subsequently be initialized by a call to {@link #initCause}.*/public BusinessException() {super();}/*** Constructs a new exception with the specified detail message. The cause is not initialized, and may subsequently* be initialized by a call to {@link #initCause}.** @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()}* method.*/public BusinessException(final String message) {super(message);}/*** Constructs a new exception with the specified detail message and cause.* <p>* Note that the detail message associated with {@code cause} is <i>not</i> automatically incorporated in this* exception's detail message.** @param message the detail message (which is saved for later retrieval by the {@link #getMessage()} method).* @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). (A <tt>null</tt>* value is permitted, and indicates that the cause is nonexistent or unknown.)* @since 1.4*/public BusinessException(final String message, final Throwable cause) {super(message, cause);}/*** Constructs a new exception with the specified cause and a detail message of* <tt>(cause==null ? null : cause.toString())</tt> (which typically contains the class and detail message of* <tt>cause</tt>). This constructor is useful for exceptions that are little more than wrappers for other* throwables (for example, {@link java.security.PrivilegedActionException}).** @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). (A <tt>null</tt>* value is permitted, and indicates that the cause is nonexistent or unknown.)* @since 1.4*/public BusinessException(final Throwable cause) {super(cause);}/*** Constructs a new exception with the specified detail message, cause, suppression enabled or disabled, and* writable stack trace enabled or disabled.** @param message the detail message.* @param cause the cause. (A {@code null} value is permitted, and indicates that the cause is nonexistent or* unknown.)* @param enableSuppression whether or not suppression is enabled or disabled* @param writableStackTrace whether or not the stack trace should be writable* @since 1.7*/protected BusinessException(final String message, final Throwable cause, boolean enableSuppression,boolean writableStackTrace) {super(message, cause, enableSuppression, writableStackTrace);}}
异常的处理
package tech.pdai.ratelimit.guava.config.exception;import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import tech.pdai.ratelimit.guava.config.response.ResponseResult;
import tech.pdai.ratelimit.guava.config.response.ResponseStatus;/*** @author pdai*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {/*** handle business exception.** @param businessException business exception* @return ResponseResult*/@ResponseBody@ExceptionHandler(BusinessException.class)public ResponseResult<BusinessException> processBusinessException(BusinessException businessException) {log.error(businessException.getLocalizedMessage());return ResponseResult.fail(null, businessException.getLocalizedMessage()==null? ResponseStatus.HTTP_STATUS_500.getDescription():businessException.getLocalizedMessage());}/*** handle other exception.** @param exception exception* @return ResponseResult*/@ResponseBody@ExceptionHandler(Exception.class)public ResponseResult<Exception> processException(Exception exception) {log.error(exception.getLocalizedMessage(), exception);return ResponseResult.fail(null, ResponseStatus.HTTP_STATUS_500.getDescription());}
}
# 统一结果返回封装
package tech.pdai.ratelimit.guava.config.response;import java.io.Serializable;import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
public class ResponseResult<T> {/*** response timestamp.*/private long timestamp;/*** response code, 200 -> OK.*/private String status;/*** response message.*/private String message;/*** response data.*/private T data;/*** response success result wrapper.** @param <T> type of data class* @return response result*/public static <T> ResponseResult<T> success() {return success(null);}/*** response success result wrapper.** @param data response data* @param <T> type of data class* @return response result*/public static <T> ResponseResult<T> success(T data) {return ResponseResult.<T>builder().data(data).message(ResponseStatus.SUCCESS.getDescription()).status(ResponseStatus.SUCCESS.getResponseCode()).timestamp(System.currentTimeMillis()).build();}/*** response error result wrapper.** @param message error message* @param <T> type of data class* @return response result*/public static <T extends Serializable> ResponseResult<T> fail(String message) {return fail(null, message);}/*** response error result wrapper.** @param data response data* @param message error message* @param <T> type of data class* @return response result*/public static <T> ResponseResult<T> fail(T data, String message) {return ResponseResult.<T>builder().data(data).message(message).status(ResponseStatus.FAIL.getResponseCode()).timestamp(System.currentTimeMillis()).build();}}
# controller接口
package tech.pdai.ratelimit.guava.controller;import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.pdai.ratelimit.guava.config.ratelimit.RateLimit;
import tech.pdai.ratelimit.guava.config.response.ResponseResult;/*** @author pdai*/
@Slf4j
@RestController
public class RateLimitTestController {@RateLimit@GetMapping("/limit")public ResponseResult<String> limit() {log.info("limit");return ResponseResult.success();}@RateLimit(limit = 5)@GetMapping("/limit1")public ResponseResult<String> limit1() {log.info("limit1");return ResponseResult.success();}@GetMapping("/nolimit")public ResponseResult<String> noRateLimiter() {log.info("no limit");return ResponseResult.success();}}
# 接口测试
@SneakyThrows
public static void test(int clientSize) {CountDownLatch downLatch = new CountDownLatch(clientSize);ExecutorService fixedThreadPool = Executors.newFixedThreadPool(clientSize);IntStream.range(0, clientSize).forEach(i ->fixedThreadPool.submit(() -> {RestTemplate restTemplate = new RestTemplate();restTemplate.getForObject("http://localhost:8080/limit1", ResponseResult.class);downLatch.countDown();}));downLatch.await();fixedThreadPool.shutdown();
}
测试结果
2021-10-01 15:22:47.171 INFO 30092 --- [nio-8080-exec-4] t.p.r.g.c.RateLimitTestController : limit1
2021-10-01 15:22:47.171 INFO 30092 --- [nio-8080-exec-8] t.p.r.g.c.RateLimitTestController : limit1
2021-10-01 15:22:47.171 INFO 30092 --- [nio-8080-exec-5] t.p.r.g.c.RateLimitTestController : limit1
2021-10-01 15:22:47.187 INFO 30092 --- [nio-8080-exec-9] t.p.r.g.c.RateLimitTestController : limit1
2021-10-01 15:22:47.187 INFO 30092 --- [nio-8080-exec-2] t.p.r.g.c.RateLimitTestController : limit1
2021-10-01 15:22:47.187 INFO 30092 --- [io-8080-exec-10] t.p.r.g.c.RateLimitTestController : limit1
2021-10-01 15:22:47.202 ERROR 30092 --- [nio-8080-exec-7] t.p.r.g.c.e.GlobalExceptionHandler : too many requests, please try again later...
2021-10-01 15:22:47.202 ERROR 30092 --- [nio-8080-exec-6] t.p.r.g.c.e.GlobalExceptionHandler : too many requests, please try again later...
2021-10-01 15:22:47.221 ERROR 30092 --- [nio-8080-exec-1] t.p.r.g.c.e.GlobalExceptionHandler : too many requests, please try again later...
2021-10-01 15:22:47.222 ERROR 30092 --- [nio-8080-exec-5] t.p.r.g.c.e.GlobalExceptionHandler : too many requests, please try again later...
2021-10-01 15:22:47.225 ERROR 30092 --- [nio-8080-exec-6] t.p.r.g.c.e.GlobalExceptionHandler : too many requests, please try again later...
2021-10-01 15:22:47.225 ERROR 30092 --- [nio-8080-exec-8] t.p.r.g.c.e.GlobalExceptionHandler : too many requests, please try again later...
2021-10-01 15:22:47.225 ERROR 30092 --- [nio-8080-exec-3] t.p.r.g.c.e.GlobalExceptionHandler : too many requests, please try again later...
2021-10-01 15:22:47.225 ERROR 30092 --- [io-8080-exec-12] t.p.r.g.c.e.GlobalExceptionHandler : too many requests, please try again later...
2021-10-01 15:22:47.225 ERROR 30092 --- [io-8080-exec-14] t.p.r.g.c.e.GlobalExceptionHandler : too many requests, please try again later...
2021-10-01 15:22:47.225 ERROR 30092 --- [io-8080-exec-13] t.p.r.g.c.e.GlobalExceptionHandler : too many requests, please try again later...
2021-10-01 15:22:47.225 ERROR 30092 --- [io-8080-exec-15] t.p.r.g.c.e.GlobalExceptionHandler : too many requests, please try again later...
2021-10-01 15:22:47.240 ERROR 30092 --- [io-8080-exec-11] t.p.r.g.c.e.GlobalExceptionHandler : too many requests, please try again later...
2021-10-01 15:22:47.240 ERROR 30092 --- [nio-8080-exec-4] t.p.r.g.c.e.GlobalExceptionHandler : too many requests, please try again later...
2021-10-01 15:22:47.256 ERROR 30092 --- [nio-8080-exec-2] t.p.r.g.c.e.GlobalExceptionHandler : too many requests, please try again later...
# 上述实现方案的槽点
注意
必须要说明一下,上述实现方式只是单实例下一种思路而已,如果细细的看,上面的代码存在一些槽点。
- 首先,
EXISTED_RATE_LIMITERS.computeIfAbsent(method.getName(), k -> RateLimiter.create(annotation.limit()))
这行代码中method.getName()
表明是对方法名进行限流的,其实并不合适,应该需要至少加上类名; - 其次, 如果首次运行时访问的请求是一次性涌入的,即EXISTED_RATE_LIMITERS还是空的时候并发请求@RateLimit接口,那么RateLimiter.create(annotation.limit())是会重复创建并加入到EXISTED_RATE_LIMITERS的,这是明显的bug;
- 再者, 上述实现方式按照方法名去限定请求量,对于很多情况下至少需要支持按照IP和方法名,或者其它自定义的方式进行限流。
- 其它一些场景支持的参数抽象和封装等
相关文章:
SpringBoot接口 - 如何实现接口限流之单实例
在以SpringBoot开发Restful接口时,当流量超过服务极限能力时,系统可能会出现卡死、崩溃的情况,所以就有了降级和限流。在接口层如何做限流呢? 本文主要回顾限流的知识点,并实践单实例限流的一种思路。 SpringBoot接口 …...

【花雕学AI】深度挖掘ChatGPT角色扮演的一个案例—CHARACTER play : 莎士比亚
CHARACTER play : 莎士比亚 : 52岁,男性,剧作家,诗人,喜欢文学,戏剧,爱情 : 1、问他为什么写《罗密欧与朱丽叶》 AI: 你好,我是莎士比亚,一位英国的剧作家和诗人。我很高兴你对我的…...

腾讯云物联网开发平台 LoRaWAN 透传接入 更新版
前言 之前有一篇文章介绍LoRaWAN透传数据,不过还是用物模型云端数据解析脚本,不是真正的透传。腾讯云物联网开发平台也支持对LoRaWAN原始数据的透传、转发。今天来介绍下。腾讯云 IoT Explorer 是腾讯云主推的一站式物联网开发平台,IoT 小能手…...

4.6--计算机网络之TCP篇之TCP的基本认识--(复习+深入)---好好沉淀,加油呀
1.TCP 头格式有哪些? 序列号: 在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。 用来解决网络包乱序问题。 确认应答号: …...

一文吃透Elasticsearch
本文已经收录到Github仓库,该仓库包含计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享等核心知识点,欢迎star~ Github地址 如果访问不了Github,…...

CPU占用率高怎么办?正确解决方法在这里!
案例:CPU占用率高怎么解决 【各位朋友,我的电脑现在运行太慢了,同事说可能是CPU占用率太高了,但对本电脑小白来说,完全不知道怎么处理,大家有什么好的方法可以解决这个问题吗?】 在计算机中&a…...
ChatGPT实现用C语言写一个学生成绩管理系统
随着ChatGPT爆火,大家都在使用ChatGPT来帮助自己提高效率,对于程序员来说使用它来写代码怎么样呢?今天尝试让ChatGPT,写了一个学生成绩管理系统。 问题是:使用C语言写一个学生成绩管理系统,要求使用链表&a…...

Swagger文档注释
本文以DRF框架为例使用 为什么要接口文档注释 一. 方便后端调试与后续接口更新; 二. 对于大型前后端分离项目,前后端人员是分开开发的,甚至前端的人你都不知道远在何处,这时候接口文档的重要性就太重要了。 三. 接口注释文档常用…...

pdf怎么转换ppt格式,两个方法转换
PDF作为一种常用的文件格式,被大众所熟悉。虽然PDF具备的稳定性,安全性,以及很强的兼容性可以让我们更方便顺畅的阅读PDF文件,但若是有需要展示PDF文件内容的时候,其优点就没有那么凸显了,这时还是将pdf转换…...
深度学习编译器相关的优秀论文合集-附下载地址
公司排名不分先后 目前在AI芯片编译器领域,有很多大公司在进行研究和开发。以下是一些主要的公司和它们在该领域的研究时间: 英伟达(NVIDIA):英伟达是一家全球知名的图形处理器制造商,其在AI芯片编译器领域…...

vue全局使用svg
1、安装依赖 npm install svg-sprite-loader2、配置选项 在vue.config.js的chainWebpack里配置下面代码 解释:config.module.rule是一个方法,用来获取某个对象的规则。.exclude.add(文件a)是往禁用组添加文件a,就是对文…...
每天一点C++——杂记
结构体的深拷贝和浅拷贝 浅拷贝就是只拷贝指针,并不拷贝指针所指向的内容,深拷贝则会对指针的内容进行拷贝。浅拷贝会在一些场景下出现问题,看下面的例子: struct s {char * name;int age; };如果我定义 一个对象s1,…...

Document Imaging SDK 11.6 for .NET Crack
Document Imaging SDK for .NET View, Convert, Annotate, Process,Edit, Scan, OCR, Print 基本上被认为是一种导出 PDF 解决方案,能够为用户和开发人员提供完整且创新的 PDF 文档处理属性。它具有提供简单集成的能力,可用于增强用户 .NET 的文档成像程…...

数据挖掘(3.1)--频繁项集挖掘方法
目录 1.Apriori算法 Apriori性质 伪代码 apriori算法 apriori-gen(Lk-1)【候选集产生】 has_infrequent_subset(c,Lx-1)【判断候选集元素】 例题 求频繁项集: 对于频繁项集L{B,C,E},可以得到哪些关联规则: 2.FP-growth算法 FP-tre…...

2023年信息安全推荐证书
随着网络安全行业的不断升温,相关的认证数量也不断增加,对于在网络安全行业发展的人才来说,提升职业竞争力最有效的办法之一,就是取得权威认证。 那么如何从繁多的适合网络安全从业者的证书中选择含金量高、发展潜力大的证书&…...

基于ArcGIS、ENVI、InVEST、FRAGSTATS等多技术融合提升环境、生态、水文、土地、土壤、农业、大气等领域应用
【自选】 时间地点:2023年7月22日-28日【乌鲁木齐】时间地点:2023年8月12日-18日【福建泉州】 【六天实践教学、提供全部资料】 专题一、空间数据获取与制图 1.1 软件安装与应用讲解 1.2 空间数据介绍 1.3海量空间数据下载 1.4 ArcGIS软件快速入门…...

基于ZC序列的帧同步
Zadoff-Chu序列是一种特殊的序列,具有良好的自相关性和很低的互相关性,这种性能可以被用来产生同步信号,作为对时间和频率的相关运算在TD-LTE系统中,Zadoff-Chu(ZC)序列主要应用于上行RS序列生成、PRACH前导序列生成以及主同步信号…...
配置NFS服务器-debian
NFS(Network Files System)是网络文件系统的英文缩写,由Sun公司于1980年开发,用于在UNIX操作系统间实现磁盘文件共享。在Linux操作系统出现后,NFS被Linux继承,并成为文件服务的一种标准。 通过网络,NFS可以在不同文件…...
正点原子STEMWIN死机
在用正点原子STM32F4开发板,搭配对应的button历程时,发现运行一会,button都无法使用了,以为是emwin死机了,但是看到Led还在闪烁,排除系统死机问题。那就是emwin的任务没有运行起来,但是打断点后…...
PMP考试中的固定答题套路
1、掌握PMBOK 编制的逻辑(整范进,成质资,沟风采,相) 2、事实求是,项目经理该怎么做就怎么做,不能违背职业道德。 3、PM 做事流程(5 步法): ①收集信息&…...
浅谈 React Hooks
React Hooks 是 React 16.8 引入的一组 API,用于在函数组件中使用 state 和其他 React 特性(例如生命周期方法、context 等)。Hooks 通过简洁的函数接口,解决了状态与 UI 的高度解耦,通过函数式编程范式实现更灵活 Rea…...

RocketMQ延迟消息机制
两种延迟消息 RocketMQ中提供了两种延迟消息机制 指定固定的延迟级别 通过在Message中设定一个MessageDelayLevel参数,对应18个预设的延迟级别指定时间点的延迟级别 通过在Message中设定一个DeliverTimeMS指定一个Long类型表示的具体时间点。到了时间点后…...

CTF show Web 红包题第六弹
提示 1.不是SQL注入 2.需要找关键源码 思路 进入页面发现是一个登录框,很难让人不联想到SQL注入,但提示都说了不是SQL注入,所以就不往这方面想了 先查看一下网页源码,发现一段JavaScript代码,有一个关键类ctfs…...
Java如何权衡是使用无序的数组还是有序的数组
在 Java 中,选择有序数组还是无序数组取决于具体场景的性能需求与操作特点。以下是关键权衡因素及决策指南: ⚖️ 核心权衡维度 维度有序数组无序数组查询性能二分查找 O(log n) ✅线性扫描 O(n) ❌插入/删除需移位维护顺序 O(n) ❌直接操作尾部 O(1) ✅内存开销与无序数组相…...

理解 MCP 工作流:使用 Ollama 和 LangChain 构建本地 MCP 客户端
🌟 什么是 MCP? 模型控制协议 (MCP) 是一种创新的协议,旨在无缝连接 AI 模型与应用程序。 MCP 是一个开源协议,它标准化了我们的 LLM 应用程序连接所需工具和数据源并与之协作的方式。 可以把它想象成你的 AI 模型 和想要使用它…...
macOS多出来了:Google云端硬盘、YouTube、表格、幻灯片、Gmail、Google文档等应用
文章目录 问题现象问题原因解决办法 问题现象 macOS启动台(Launchpad)多出来了:Google云端硬盘、YouTube、表格、幻灯片、Gmail、Google文档等应用。 问题原因 很明显,都是Google家的办公全家桶。这些应用并不是通过独立安装的…...

GitFlow 工作模式(详解)
今天再学项目的过程中遇到使用gitflow模式管理代码,因此进行学习并且发布关于gitflow的一些思考 Git与GitFlow模式 我们在写代码的时候通常会进行网上保存,无论是github还是gittee,都是一种基于git去保存代码的形式,这样保存代码…...
【前端异常】JavaScript错误处理:分析 Uncaught (in promise) error
在前端开发中,JavaScript 异常是不可避免的。随着现代前端应用越来越多地使用异步操作(如 Promise、async/await 等),开发者常常会遇到 Uncaught (in promise) error 错误。这个错误是由于未正确处理 Promise 的拒绝(r…...
LLaMA-Factory 微调 Qwen2-VL 进行人脸情感识别(二)
在上一篇文章中,我们详细介绍了如何使用LLaMA-Factory框架对Qwen2-VL大模型进行微调,以实现人脸情感识别的功能。本篇文章将聚焦于微调完成后,如何调用这个模型进行人脸情感识别的具体代码实现,包括详细的步骤和注释。 模型调用步骤 环境准备:确保安装了必要的Python库。…...

Axure 下拉框联动
实现选省、选完省之后选对应省份下的市区...