当前位置: 首页 > news >正文

基于redis实现API接口访问次数限制

一,概述

日常开发中会有一个常见的需求,需要限制接口在单位时间内的访问次数,比如说某个免费的接口限制单个IP一分钟内只能访问5次。该怎么实现呢,通常大家都会想到用redis,确实通过redis可以实现这个功能,下面实现一下。

二,常见错误

固定时间窗口

有人设计了一个在每分钟内只允许访问1000次的限流方案,如下图01:00s-02:00s之间只允许访问1000次。这种设计的问题在于,请求可能在01:59s-02:00s之间被请求1000次,02:00s-02:01s之间被请求了1000次,这种情况下01:59s-02:01s间隔0.02s之间被请求2000次,很显然这种设计是错误的。

三, 实现

1,基于滑动时间窗口

在指定的时间窗口内次数是累积的,超过阈值,都会限制。

2,流程如下

3,代码实现

前提:pom文件引入redis,Spring AOP等

(1)添加注解RequestLimit
package com.xxx.demo.aspect;import java.lang.annotation.*;/*** 接口访问频率注解,默认一分钟只能访问10次*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestLimit {// 限制时间 单位:秒(默认值:一分钟)long period() default 60;// 允许请求的次数(默认值:10次)long count() default 10;
}
(2)添加切面实现注解的限制访问逻辑
package com.xxx.demo.aspect;import com.xgd.demo.commons.ErrorCode;
import com.xgd.demo.handler.BusinessException;
import com.xgd.demo.util.IpUtil;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.log4j.Log4j2;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.concurrent.TimeUnit;/*** @date 2024/11/8 上午8:43*/
@Aspect
@Component
@Log4j2
public class RequestLimitAspect {@AutowiredRedisTemplate redisTemplate;@Pointcut("@annotation(requestLimit)")public void controllerAspect(RequestLimit requestLimit) {}@Around("controllerAspect(requestLimit)")public Object doAround(ProceedingJoinPoint joinPoint, RequestLimit requestLimit) throws Throwable {// 从注解中获取限制次数和窗口时间long period = requestLimit.period();long limitCount = requestLimit.count();// 请求ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();assert attributes != null;HttpServletRequest request = attributes.getRequest();String ip = IpUtil.getIpFromRequest(request);String uri = request.getRequestURI();//设置客户端访问的keyString key = "req_limit_".concat(uri).concat(ip);ZSetOperations zSetOperations = redisTemplate.opsForZSet();// 添加当前时间戳,分数为当前时间戳long currentMs = System.currentTimeMillis();zSetOperations.add(key, currentMs, currentMs);// 设置窗口时间作为过期时间redisTemplate.expire(key, period, TimeUnit.SECONDS);// 移除掉不在窗口里的数据zSetOperations.removeRangeByScore(key, 0, currentMs - period * 1000);// 查询窗口内已经访问过的次数Long count = zSetOperations.zCard(key);if (count > limitCount) {log.error("接口拦截:{} 请求超过限制频率【{}次/{}s】,IP为{}", uri, limitCount, period, ip);throw new BusinessException(ErrorCode.REQUEST_LIMITED.getCode(), ErrorCode.REQUEST_LIMITED.getMessage());}// 继续执行请求return  joinPoint.proceed();}
}

上面里面请求被拦截,是抛出了一个自定义的业务异常,大家可以根据自己的情况自己定义。

(3)同时附上上面中引用到自定义工具类
package com.xxx.demo.util;import jakarta.servlet.http.HttpServletRequest;
import java.util.Objects;/*** @date 2024/11/8 上午9:06*/
public class IpUtil {private static final String X_FORWARDED_FOR_HEADER = "X-Forwarded-For";private static final String X_REAL_IP_HEADER = "X-Real-IP";/*** 从请求中获取IP** @return IP;当获取不到时,返回null*/public static String getIpFromRequest(HttpServletRequest request ) {return getRealIp(request);}/*** 获取请求的真实IP,优先级从高到低为:<br/>* 1.从请求头X-Forwarded-For中获取ip,并且只获取第一个ip(从左到右) <br/>* 2.从请求头X-Real-IP中获取ip <br/>* 3.使用{@link HttpServletRequest#getRemoteAddr()}方法获取ip** @param request 请求对象,必须不能为null* @return ip*/private static String getRealIp(HttpServletRequest request) {Objects.requireNonNull(request, "request must be not null");String ip = request.getHeader(X_FORWARDED_FOR_HEADER);if (ip != null && !ip.isBlank()) {int delimiterIndex = ip.indexOf(',');if (delimiterIndex != -1) {// 如果存在多个ip,则取第一个ipip = ip.substring(0, delimiterIndex);}return ip;}ip = request.getHeader(X_REAL_IP_HEADER);if (ip != null && !ip.isBlank()) {return ip;} else {return request.getRemoteAddr();}}
}
(4)使用注解

这里限制为10秒内只允许访问3次,超过就抛出异常

(5)访问测试

前3次访问,接口正常访问

后面的访问,返回自定义异常的结果

如果对你有帮助,记得点赞关注哟!

相关文章:

基于redis实现API接口访问次数限制

一&#xff0c;概述 日常开发中会有一个常见的需求&#xff0c;需要限制接口在单位时间内的访问次数&#xff0c;比如说某个免费的接口限制单个IP一分钟内只能访问5次。该怎么实现呢&#xff0c;通常大家都会想到用redis&#xff0c;确实通过redis可以实现这个功能&#xff0c…...

[ Linux 命令基础 3 ] Linux 命令详解-文件和目录管理命令

&#x1f36c; 博主介绍 &#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 _PowerShell &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【数据通信】 【通讯安全】 【web安全】【面试分析】 &#x1f389;点赞➕评论➕收藏 养成习…...

npm i 的时候报错: npm ERR! Error: EPERM: operation not permitted, rename

文章目录 噩梦解决办法总结 噩梦 最近改漏洞&#xff0c;这个项目删掉了 node_modules文件夹 重新安装依赖&#xff0c;结果安装一半的时候就一直报这个错。 然后查了很多方法&#xff0c;基本都是下面这些&#xff1a; 权限不够&#xff0c;以管理员运行cmd重新安装。清除 n…...

如何迁移剪映源文件

1、打开剪映&#xff0c;打开全局设置 2、查看草稿位置。把要迁移的文件拷贝到这个路径下面。 3、关闭文件&#xff0c;返回上一层界面&#xff0c;可以看到拷贝到目录下的文件。...

Go语言中的`io.Copy`函数:高效的数据复制解决方案

在Go语言中&#xff0c;io.Copy函数是一个强大而高效的工具&#xff0c;用于将数据从一个io.Reader复制到一个io.Writer。这篇文章将深入探讨io.Copy函数的工作原理、使用方法及其在实际应用中的优势。无论您是后端开发人员还是对Go语言感兴趣的程序员&#xff0c;这篇文章都将…...

datastage在升级版本到11.7之后,部分在11.3上正常执行的SP报错SQLSTATE = 22007: 本机错误代码 = -180

在升级版本到11.7之后&#xff0c;部分在11.3上正常执行的SP开始报错&#xff0c;报的SQL错误是时间参数问题&#xff0c;但是一样的SP可以直接call sp执行&#xff0c;也可以手动调用作业执行&#xff0c;只有设置定时调度时作业会报错&#xff0c; CALLXXX.XXX(1,CURRENT TIM…...

docker——项目部署

什么是Docker&#xff1f; Docker是一个开源的应用容器引擎&#xff0c;让开发者可以打包他们的应用以及依赖包到一个可抑制的容器中&#xff0c;然后发布到任何流行的Linux机器上&#xff0c;也可以实现虚拟化。容器完全使用沙盒机制&#xff0c;相互之间不会存在任何接口。几…...

设计模式(Unity)——更新中

设计模式 文章目录 设计模式工厂模式创建方法&#xff08;Create Methods&#xff09;简单工厂&#xff08;Simple Factory&#xff09;工厂方法&#xff08;Method Factory&#xff09;抽象工厂&#xff08;Abstract Factroy&#xff09; 策略模式 工厂模式 创建方法&#xf…...

小程序中引入下载到本地的iconfont字体图标加载不出来问题解决

我这个是uniapp项目,字体图标都是一样的,在vue项目中web端、uniapp运行到h5都没问题,但是运行到小程序加载不出来,报错如下: 不让用本地路径,所以我们要转为base64编码,这里给大家提供一个工具,它可以把本地字体文件转为base64:transfonter 进入官网后,第一步: …...

百度富文本禁止编辑

<script type"text/javascript">$(function () {editorcontent new baidu.editor.ui.Editor();editorcontent.render(authentication);//禁用代码editorcontent.ready(function () {editorcontent.setDisabled();});try {editorcontent.sync();} catch (err) …...

C++开发基础之使用librabbitmq库实现RabbitMQ消息队列通信

1. 前言 RabbitMQ是一个流行的开源消息队列系统&#xff0c;支持多种消息协议&#xff0c;广泛用于构建分布式系统和微服务架构。可以在不同应用程序之间实现异步消息传递。在本文中&#xff0c;我们将熟悉如何使用C与RabbitMQ进行消息通信。 2. 准备工作 在 Windows 平台上…...

头歌网络安全(11.12)

头歌禁止复制解决 必须先下篡改猴&#xff01;&#xff01;&#xff01;&#xff01; 头歌复制助手 Educoder Copy Helperhttps://scriptcat.org/zh-CN/script-show-page/1860 Java生成验证码 第1关&#xff1a;使用Servlet生成验证码 任务描述 本关任务&#xff1a;使用se…...

洛谷 P1725 琪露诺(线段树优化dp)

题目链接 https://www.luogu.com.cn/problem/P1725 思路 我们令 d p [ i ] dp[i] dp[i]表示琪露诺移动到第 i i i个格子时能够获得的最大冰冻指数。 显然&#xff0c;状态转移方程为&#xff1a; d p [ i ] m a x ( d p [ i ] , d p [ k ] a [ i ] ) dp[i] max(dp[i],dp…...

【LeetCode】【算法】19. 删除链表的倒数第N个结点

LeetCode 19. 删除链表的倒数第N个结点 题目描述 给你一个链表&#xff0c;删除链表的倒数第 n 个结点&#xff0c;并且返回链表的头结点。 思路 思路&#xff1a;快慢指针&#xff0c;快指针先移动n步&#xff0c;快慢指针再同时移动直到快指针到达链表末尾&#xff0c;此…...

Python爬虫 | 爬取豆瓣电影Top250的数据

简单记录一下&#xff0c;实现爬取豆瓣电影Top 250的数据。 这里我使用requests库来发送HTTP请求&#xff0c;以及BeautifulSoup库来解析HTML页面。 1.安装requests和BeautifulSoup库。 如果没有安装&#xff0c;可以通过以下命令安装&#xff1a; pip install requests bea…...

mac 中python 安装mysqlclient 出现 ld: library ‘ssl‘ not found错误

1. 出现报错 2. 获取openssl位置 brew info openssl 3. 配置环境变量&#xff08;我的是在~/.bash.profile&#xff09; export LDFLAGS"-L/opt/homebrew/Cellar/openssl3/3.4.0/lib" export CPPFLAGS"-I/opt/homebrew/Cellar/openssl3/…...

完全清除:苹果手机照片怎么彻底删除

在使用iPhone的过程中&#xff0c;由于拍摄积累的照片往往会占用大量存储空间。有时候&#xff0c;我们需要彻底删除这些照片以释放空间或保护隐私。苹果手机照片怎么彻底删除&#xff1f;在此&#xff0c;本文将与你分享一些实用的技巧。 彻底删除的重要性 彻底删除照片不仅涉…...

高德地图多个图片组成标点(自定义点标记内容)

图标的实现自定义点标记内容...

02-1_MVCC版本链清理

MVCC-版本链清理 文章目录 MVCC-版本链清理简介依赖机制Purge 操作的触发时机版本链清理的详细过程示例操作流程延迟清理配置和监控总结 简介 MySQL 中的 MVCC 机制通过版本链来管理数据的多版本存储&#xff0c;以支持高并发的读写操作。然而&#xff0c;随着事务的进行&…...

探索Python视频处理的瑞士军刀:ffmpeg-python库

文章目录 **探索Python视频处理的瑞士军刀&#xff1a;ffmpeg-python库**第一部分&#xff1a;背景介绍第二部分&#xff1a;ffmpeg-python库是什么&#xff1f;第三部分&#xff1a;如何安装ffmpeg-python库&#xff1f;第四部分&#xff1a;简单库函数使用方法1. 视频转码2. …...

内存分配函数malloc kmalloc vmalloc

内存分配函数malloc kmalloc vmalloc malloc实现步骤: 1)请求大小调整:首先,malloc 需要调整用户请求的大小,以适应内部数据结构(例如,可能需要存储额外的元数据)。通常,这包括对齐调整,确保分配的内存地址满足特定硬件要求(如对齐到8字节或16字节边界)。 2)空闲…...

深入剖析AI大模型:大模型时代的 Prompt 工程全解析

今天聊的内容&#xff0c;我认为是AI开发里面非常重要的内容。它在AI开发里无处不在&#xff0c;当你对 AI 助手说 "用李白的风格写一首关于人工智能的诗"&#xff0c;或者让翻译模型 "将这段合同翻译成商务日语" 时&#xff0c;输入的这句话就是 Prompt。…...

Golang 面试经典题:map 的 key 可以是什么类型?哪些不可以?

Golang 面试经典题&#xff1a;map 的 key 可以是什么类型&#xff1f;哪些不可以&#xff1f; 在 Golang 的面试中&#xff0c;map 类型的使用是一个常见的考点&#xff0c;其中对 key 类型的合法性 是一道常被提及的基础却很容易被忽视的问题。本文将带你深入理解 Golang 中…...

Vue3 + Element Plus + TypeScript中el-transfer穿梭框组件使用详解及示例

使用详解 Element Plus 的 el-transfer 组件是一个强大的穿梭框组件&#xff0c;常用于在两个集合之间进行数据转移&#xff0c;如权限分配、数据选择等场景。下面我将详细介绍其用法并提供一个完整示例。 核心特性与用法 基本属性 v-model&#xff1a;绑定右侧列表的值&…...

cf2117E

原题链接&#xff1a;https://codeforces.com/contest/2117/problem/E 题目背景&#xff1a; 给定两个数组a,b&#xff0c;可以执行多次以下操作&#xff1a;选择 i (1 < i < n - 1)&#xff0c;并设置 或&#xff0c;也可以在执行上述操作前执行一次删除任意 和 。求…...

TRS收益互换:跨境资本流动的金融创新工具与系统化解决方案

一、TRS收益互换的本质与业务逻辑 &#xff08;一&#xff09;概念解析 TRS&#xff08;Total Return Swap&#xff09;收益互换是一种金融衍生工具&#xff0c;指交易双方约定在未来一定期限内&#xff0c;基于特定资产或指数的表现进行现金流交换的协议。其核心特征包括&am…...

【Oracle】分区表

个人主页&#xff1a;Guiat 归属专栏&#xff1a;Oracle 文章目录 1. 分区表基础概述1.1 分区表的概念与优势1.2 分区类型概览1.3 分区表的工作原理 2. 范围分区 (RANGE Partitioning)2.1 基础范围分区2.1.1 按日期范围分区2.1.2 按数值范围分区 2.2 间隔分区 (INTERVAL Partit…...

Java求职者面试指南:Spring、Spring Boot、MyBatis框架与计算机基础问题解析

Java求职者面试指南&#xff1a;Spring、Spring Boot、MyBatis框架与计算机基础问题解析 一、第一轮提问&#xff08;基础概念问题&#xff09; 1. 请解释Spring框架的核心容器是什么&#xff1f;它在Spring中起到什么作用&#xff1f; Spring框架的核心容器是IoC容器&#…...

【JVM】Java虚拟机(二)——垃圾回收

目录 一、如何判断对象可以回收 &#xff08;一&#xff09;引用计数法 &#xff08;二&#xff09;可达性分析算法 二、垃圾回收算法 &#xff08;一&#xff09;标记清除 &#xff08;二&#xff09;标记整理 &#xff08;三&#xff09;复制 &#xff08;四&#xff…...

关于easyexcel动态下拉选问题处理

前些日子突然碰到一个问题&#xff0c;说是客户的导入文件模版想支持部分导入内容的下拉选&#xff0c;于是我就找了easyexcel官网寻找解决方案&#xff0c;并没有找到合适的方案&#xff0c;没办法只能自己动手并分享出来&#xff0c;针对Java生成Excel下拉菜单时因选项过多导…...