SpringBoot 项目使用 Redis 对用户 IP 进行接口限流
一、思路
使用接口限流的主要目的在于提高系统的稳定性,防止接口被恶意打击(短时间内大量请求)。
比如要求某接口在1分钟内请求次数不超过1000次,那么应该如何设计代码呢?
下面讲两种思路,如果想看代码可直接翻到后面的代码部分。
1.1 固定时间段(旧思路)
1.1.1 思路描述
该方案的思路是:使用Redis记录固定时间段内某用户IP访问某接口的次数,其中:
-
Redis的key:用户IP + 接口方法名
-
Redis的value:当前接口访问次数。
当用户在近期内第一次访问该接口时,向Redis中设置一个包含了用户IP和接口方法名的key,value的值初始化为1(表示第一次访问当前接口)。同时,设置该key的过期时间(比如为60秒)。
之后,只要这个key还未过期,用户每次访问该接口都会导致value自增1次。
用户每次访问接口前,先从Redis中拿到当前接口访问次数,如果发现访问次数大于规定的次数(如超过1000次),则向用户返回接口访问失败的标识。

1.1.2 思路缺陷
该方案的缺点在于,限流时间段是固定的。
比如要求某接口在1分钟内请求次数不超过1000次,观察以下流程:


可以发现,00:59和01:01之间仅仅间隔了2秒,但接口却被访问了1000+999=1999次,是限流次数(1000次)的2倍!
所以在该方案中,限流次数的设置可能不起作用,仍然可能在短时间内造成大量访问。
1.2 滑动窗口(新思路)
1.2.1 思路描述
为了避免出现方案1中由于键过期导致的短期访问量增大的情况,我们可以改变一下思路,也就是把固定的时间段改成动态的:
假设某个接口在10秒内只允许访问5次。用户每次访问接口时,记录当前用户访问的时间点(时间戳),并计算前10秒内用户访问该接口的总次数。如果总次数大于限流次数,则不允许用户访问该接口。这样就能保证在任意时刻用户的访问次数不会超过1000次。
如下图,假设用户在0:19时间点访问接口,经检查其前10秒内访问次数为5次,则允许本次访问。

假设用户0:20时间点访问接口,经检查其前10秒内访问次数为6次(超出限流次数5次),则不允许本次访问。

1.2.2 Redis部分的实现
1)选用何种 Redis 数据结构
首先是需要确定使用哪个Redis数据结构。用户每次访问时,需要用一个key记录用户访问的时间点,而且还需要利用这些时间点进行范围检查。
2)为何选择 zSet 数据结构
为了能够实现范围检查,可以考虑使用Redis中的zSet有序集合。
添加一个zSet元素的命令如下:
ZADD [key] [score] [member]
它有一个关键的属性score,通过它可以记录当前member的优先级。
于是我们可以把score设置成用户访问接口的时间戳,以便于通过score进行范围检查。key则记录用户IP和接口方法名,至于member设置成什么没有影响,一个member记录了用户访问接口的时间点。因此member也可以设置成时间戳。
3)zSet 如何进行范围检查(检查前几秒的访问次数)
思路是,把特定时间间隔之前的member都删掉,留下的member就是时间间隔之内的总访问次数。然后统计当前key中的member有多少个即可。
① 把特定时间间隔之前的member都删掉。
zSet有如下命令,用于删除score范围在[min~max]之间的member:
Zremrangebyscore [key] [min] [max]
假设限流时间设置为5秒,当前用户访问接口时,获取当前系统时间戳为currentTimeMill,那么删除的score范围可以设置为:
min = 0
max = currentTimeMill - 5 * 1000
相当于把5秒之前的所有member都删除了,只留下前5秒内的key。
② 统计特定key中已存在的member有多少个。
zSet有如下命令,用于统计某个key的member总数:
ZCARD [key]
统计的key的member总数,就是当前接口已经访问的次数。如果该数目大于限流次数,则说明当前的访问应被限流。
二、代码实现
主要是使用注解 + AOP的形式实现。
2.1 固定时间段思路
使用了lua脚本。
参考:https://blog.csdn.net/qq_43641418/article/details/127764462
2.1.1 限流注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RateLimiter {/*** 限流时间,单位秒*/int time() default 5;/*** 限流次数*/int count() default 10;
}
2.1.2 定义lua脚本
在resources/lua下新建limit.lua:
-- 获取redis键
local key = KEYS[1]
-- 获取第一个参数(次数)
local count = tonumber(ARGV[1])
-- 获取第二个参数(时间)
local time = tonumber(ARGV[2])
-- 获取当前流量
local current = redis.call('get', key);
-- 如果current值存在,且值大于规定的次数,则拒绝放行(直接返回当前流量)
if current and tonumber(current) > count thenreturn tonumber(current)
end
-- 如果值小于规定次数,或值不存在,则允许放行,当前流量数+1 (值不存在情况下,可以自增变为1)
current = redis.call('incr', key);
-- 如果是第一次进来,那么开始设置键的过期时间。
if tonumber(current) == 1 then redis.call('expire', key, time);
end
-- 返回当前流量
return tonumber(current)
2.1.3 注入Lua执行脚本
关键代码是limitScript()方法
@Configuration
public class RedisConfig {@Beanpublic RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();redisTemplate.setConnectionFactory(connectionFactory);// 使用Jackson2JsonRedisSerialize 替换默认序列化(默认采用的是JDK序列化)Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);ObjectMapper om = new ObjectMapper();om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(om);redisTemplate.setKeySerializer(jackson2JsonRedisSerializer);redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);return redisTemplate;}/*** 解析lua脚本的bean*/@Bean("limitScript")public DefaultRedisScript<Long> limitScript() {DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/limit.lua")));redisScript.setResultType(Long.class);return redisScript;}
}
2.1.3 定义Aop切面类
@Slf4j
@Aspect
@Component
public class RateLimiterAspect {@Autowiredprivate RedisTemplate redisTemplate;@Autowiredprivate RedisScript<Long> limitScript;@Before("@annotation(rateLimiter)")public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable {int time = rateLimiter.time();int count = rateLimiter.count();String combineKey = getCombineKey(rateLimiter.type(), point);List<String> keys = Collections.singletonList(combineKey);try {Long number = (Long) redisTemplate.execute(limitScript, keys, count, time);// 当前流量number已超过限制,则抛出异常if (number == null || number.intValue() > count) {throw new RuntimeException("访问过于频繁,请稍后再试");}log.info("[limit] 限制请求数'{}',当前请求数'{}',缓存key'{}'", count, number.intValue(), combineKey);} catch (Exception ex) {ex.printStackTrace();throw new RuntimeException("服务器限流异常,请稍候再试");}}/*** 把用户IP和接口方法名拼接成 redis 的 key* @param point 切入点* @return 组合key*/private String getCombineKey(JoinPoint point) {StringBuilder sb = new StringBuilder("rate_limit:");ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest();sb.append( Utils.getIpAddress(request) );MethodSignature signature = (MethodSignature) point.getSignature();Method method = signature.getMethod();Class<?> targetClass = method.getDeclaringClass();// keyPrefix + "-" + class + "-" + methodreturn sb.append("-").append( targetClass.getName() ).append("-").append(method.getName()).toString();}
}
2.2 滑动窗口思路
2.2.1 限流注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RateLimiter {/*** 限流时间,单位秒*/int time() default 5;/*** 限流次数*/int count() default 10;
}
2.2.2 定义Aop切面类
@Slf4j
@Aspect
@Component
public class RateLimiterAspect {@Autowiredprivate RedisTemplate redisTemplate;/*** 实现限流(新思路)* @param point* @param rateLimiter* @throws Throwable*/@SuppressWarnings("unchecked")@Before("@annotation(rateLimiter)")public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable {// 在 {time} 秒内仅允许访问 {count} 次。int time = rateLimiter.time();int count = rateLimiter.count();// 根据用户IP(可选)和接口方法,构造keyString combineKey = getCombineKey(rateLimiter.type(), point);// 限流逻辑实现ZSetOperations zSetOperations = redisTemplate.opsForZSet();// 记录本次访问的时间结点long currentMs = System.currentTimeMillis();zSetOperations.add(combineKey, currentMs, currentMs);// 这一步是为了防止member一直存在于内存中redisTemplate.expire(combineKey, time, TimeUnit.SECONDS);// 移除{time}秒之前的访问记录(滑动窗口思想)zSetOperations.removeRangeByScore(combineKey, 0, currentMs - time * 1000);// 获得当前窗口内的访问记录数Long currCount = zSetOperations.zCard(combineKey);// 限流判断if (currCount > count) {log.error("[limit] 限制请求数'{}',当前请求数'{}',缓存key'{}'", count, currCount, combineKey);throw new RuntimeException("访问过于频繁,请稍后再试!");}}/*** 把用户IP和接口方法名拼接成 redis 的 key* @param point 切入点* @return 组合key*/private String getCombineKey(JoinPoint point) {StringBuilder sb = new StringBuilder("rate_limit:");ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest();sb.append( Utils.getIpAddress(request) );MethodSignature signature = (MethodSignature) point.getSignature();Method method = signature.getMethod();Class<?> targetClass = method.getDeclaringClass();// keyPrefix + "-" + class + "-" + methodreturn sb.append("-").append( targetClass.getName() ).append("-").append(method.getName()).toString();}
}相关文章:
SpringBoot 项目使用 Redis 对用户 IP 进行接口限流
一、思路 使用接口限流的主要目的在于提高系统的稳定性,防止接口被恶意打击(短时间内大量请求)。 比如要求某接口在1分钟内请求次数不超过1000次,那么应该如何设计代码呢? 下面讲两种思路,如果想看代码可…...
SLA探活工具EaseProbe
工具介绍 EaseProbe可以做三种工作:探测、通知和报告。 项目地址:https://github.com/megaease/easeprobe 1、安装 [rootlocalhost ]# yum -y install unzip go [rootlocalhost ]# unzip easeprobe-main.zip [rootlocalhost ]# cd easeprobe-main [r…...
[Java] 观察者模式简述
模式定义:定义了对象之间的一对多依赖,让多个观察者对象同时监听某一个主题对象,当主题对象发生变化时,他的所有依赖者都会收到通知并且更新 依照这个图,简单的写一个代码 package Section1.listener;import java.ut…...
linux驱动定时器实现按键按下打印字符
#include <linux/init.h> #include <linux/module.h> #include <linux/of.h> #include <linux/of_irq.h> #include <linux/interrupt.h>struct device_node *dev; unsigned int irqno; //中断处理函数 irqreturn_t myirq_handler(int irq,void *…...
反转链表(JS)
反转链表 题目 给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。 示例 1: 输入:head [1,2,3,4,5] 输出:[5,4,3,2,1]示例 2: 输入:head [1,2] 输出:[2,1]示例 3&…...
[PyTorch][chapter 45][RNN_2]
目录: RNN 问题 RNN 时序链问题 RNN 词组预测的例子 RNN简洁实现 一 RNN 问题 RNN 主要有两个问题,梯度弥散和梯度爆炸 1.1 损失函数 梯度 其中: 则 1.1 梯度爆炸(Gradient Exploding) 上面矩阵进行连乘后…...
基于canvas画布的实用类Fabric.js的使用
目录 前言 一、Fabric.js简介 二、开始 1、引入Fabric.js 2、在main.js中使用 3、初始化画布 三、方法 四、事件 1、常用事件 2、事件绑定 3、事件解绑 五、canvas常用属性 六、对象属性 1、基本属性 2、扩展属性 七、图层层级操作 八、复制和粘贴 1、复制 2…...
基于SpringBoot+Vue驾校理论课模拟考试系统源码(自动化部署)
DrivingTestSimulation Unity3D Project, subject two, simulated driving test 【更新信息】 更新时间-2021-1-17 解决了方向盘不同机型转动轴心偏离 更新时间-2021-2-18 加入了手刹系统 待更新-2021-6-19(工作太忙少有时间更新,先指出问题…...
SpringBoot使用Redis对用户IP进行接口限流
使用接口限流的主要目的在于提高系统的稳定性,防止接口被恶意打击(短时间内大量请求)。 一、创建限流注解 引入redis依赖 <!--redis--><dependency><groupId>org.springframework.boot</groupId><artifactId&g…...
MeterSphere学习篇
从开发环境部署开始 metersphere-1.20.4 源码下载地址: https://gitee.com/fit2cloud-feizhiyun/MeterSphere/tree/v1.20/ MeterSphere GitHub 相关插件程序下载 相关准备 安装mysql 配置IDEA...
大数据技术之Clickhouse---入门篇---数据类型、表引擎
星光下的赶路人star的个人主页 今天没有开始的事,明天绝对不会完成 文章目录 1、数据类型1.1 整型1.2 浮点型1.3 布尔型1.4 Decimal型1.5 字符串1.6 枚举类型1.7 时间类型1.8 数组 2、表引擎2.1 表引擎的使用2.2 TinyLog2.3 Memory2.4 MergeTree2.4.1 Partition by分…...
【微服务架构设计】微服务不是魔术:处理超时
微服务很重要。它们可以为我们的架构和团队带来一些相当大的胜利,但微服务也有很多成本。随着微服务、无服务器和其他分布式系统架构在行业中变得更加普遍,我们将它们的问题和解决它们的策略内化是至关重要的。在本文中,我们将研究网络边界可…...
天下风云出我辈,AI准独角兽实在智能获评“十大数字经济风云企业
时值盛夏,各地全力拼经济的氛围同样热火朝天。在浙江省经济强区余杭区这片创业热土上,人工智能助力数字经济建设正焕发出蓬勃生机。 7月28日,经专家评审、公开投票,由中共杭州市余杭区委组织部(区委两新工委ÿ…...
SpringBoot2学习笔记
信息来源:https://www.bilibili.com/video/BV19K4y1L7MT?p5&vd_source3969f30b089463e19db0cc5e8fe4583a 作者提供的文档:https://www.yuque.com/atguigu/springboot 作者提供的代码:https://gitee.com/leifengyang/springboot2 ----…...
安达发|APS生产派单系统对数字化工厂有哪些影响和作用
数字化工厂是当今制造业的热门话题,而APS软件则是这一领域的颠覆者。它以其独特的影响和作用,给制造业带来了巨大的改变。让我们一起来看看APS软件对数字化工厂有哪些影响和作用吧! 提高生产效率的神器 1.APS软件作为数字化工厂的核心系统&a…...
状态机的介绍和使用 | 京东物流技术团队
1 状态机简介 1.1 定义 我们先来给出状态机的基本定义。一句话: 状态机是有限状态自动机的简称,是现实事物运行规则抽象而成的一个数学模型。 先来解释什么是“状态”( State )。现实事物是有不同状态的,例如一个自…...
tinkerCAD案例:32. 使用对齐工具构建喷泉
tinkerCAD案例:32. 使用对齐工具构建喷泉 In this lesson, you will practice the basics in Tinkercad, such as move, rotate, and scale. You will also learn how to use the Align Tool. 在本课中,您将练习 Tinkercad 中的基础知识,例如…...
一起学数据结构(2)——线性表及线性表顺序实现
目录 1. 什么是数据结构: 1.1 数据结构的研究内容: 1.2 数据结构的基本概念: 1.2.1 逻辑结构: 1.2.2 存储结构: 2. 线性表: 2.1 线性表的基本定义: 2.2 线性表的运用: 3 .线性…...
mqtt协议流程图
转载于...
7、单元测试--测试RestFul 接口
单元测试–测试RestFul 接口 – 测试用例类使用SpringBootTest(webEnvironment WebEnvironment.RANDOM_PORT)修饰。 – 测试用例类会接收容器依赖注入TestRestTemplate这个实例变量。 – 测试方法可通过TestRestTemplate来调用RESTful接口的方法。 测试用例应该定义在和被测…...
变量 varablie 声明- Rust 变量 let mut 声明与 C/C++ 变量声明对比分析
一、变量声明设计:let 与 mut 的哲学解析 Rust 采用 let 声明变量并通过 mut 显式标记可变性,这种设计体现了语言的核心哲学。以下是深度解析: 1.1 设计理念剖析 安全优先原则:默认不可变强制开发者明确声明意图 let x 5; …...
vscode里如何用git
打开vs终端执行如下: 1 初始化 Git 仓库(如果尚未初始化) git init 2 添加文件到 Git 仓库 git add . 3 使用 git commit 命令来提交你的更改。确保在提交时加上一个有用的消息。 git commit -m "备注信息" 4 …...
基于FPGA的PID算法学习———实现PID比例控制算法
基于FPGA的PID算法学习 前言一、PID算法分析二、PID仿真分析1. PID代码2.PI代码3.P代码4.顶层5.测试文件6.仿真波形 总结 前言 学习内容:参考网站: PID算法控制 PID即:Proportional(比例)、Integral(积分&…...
k8s从入门到放弃之Ingress七层负载
k8s从入门到放弃之Ingress七层负载 在Kubernetes(简称K8s)中,Ingress是一个API对象,它允许你定义如何从集群外部访问集群内部的服务。Ingress可以提供负载均衡、SSL终结和基于名称的虚拟主机等功能。通过Ingress,你可…...
QMC5883L的驱动
简介 本篇文章的代码已经上传到了github上面,开源代码 作为一个电子罗盘模块,我们可以通过I2C从中获取偏航角yaw,相对于六轴陀螺仪的yaw,qmc5883l几乎不会零飘并且成本较低。 参考资料 QMC5883L磁场传感器驱动 QMC5883L磁力计…...
渗透实战PortSwigger靶场-XSS Lab 14:大多数标签和属性被阻止
<script>标签被拦截 我们需要把全部可用的 tag 和 event 进行暴力破解 XSS cheat sheet: https://portswigger.net/web-security/cross-site-scripting/cheat-sheet 通过爆破发现body可以用 再把全部 events 放进去爆破 这些 event 全部可用 <body onres…...
LeetCode - 394. 字符串解码
题目 394. 字符串解码 - 力扣(LeetCode) 思路 使用两个栈:一个存储重复次数,一个存储字符串 遍历输入字符串: 数字处理:遇到数字时,累积计算重复次数左括号处理:保存当前状态&a…...
Java-41 深入浅出 Spring - 声明式事务的支持 事务配置 XML模式 XML+注解模式
点一下关注吧!!!非常感谢!!持续更新!!! 🚀 AI篇持续更新中!(长期更新) 目前2025年06月05日更新到: AI炼丹日志-28 - Aud…...
python爬虫:Newspaper3k 的详细使用(好用的新闻网站文章抓取和解析的Python库)
更多内容请见: 爬虫和逆向教程-专栏介绍和目录 文章目录 一、Newspaper3k 概述1.1 Newspaper3k 介绍1.2 主要功能1.3 典型应用场景1.4 安装二、基本用法2.2 提取单篇文章的内容2.2 处理多篇文档三、高级选项3.1 自定义配置3.2 分析文章情感四、实战案例4.1 构建新闻摘要聚合器…...
VTK如何让部分单位不可见
最近遇到一个需求,需要让一个vtkDataSet中的部分单元不可见,查阅了一些资料大概有以下几种方式 1.通过颜色映射表来进行,是最正规的做法 vtkNew<vtkLookupTable> lut; //值为0不显示,主要是最后一个参数,透明度…...
