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接口的方法。 测试用例应该定义在和被测…...

国家留学基金委(CSC)|发布2024年创新型人才国际合作培养项目实施办法
2023年7月28日,国家留学基金委(CSC)发布了《2024年创新型人才国际合作培养项目实施办法》,在此知识人网小编做全文转载。详细信息请参见https://www.csc.edu.cn/chuguo/s/2648。 2024年创新型人才国际合作培养项目实施办法 第一章…...

找好听的配乐、BGM就上这6个网站,免费商用。
推荐几个音乐素材网站给你,各种类似、风格的都有,而且免费下载,还可以商用,建议收藏起来~ 菜鸟图库 https://www.sucai999.com/audio.html?vNTYxMjky 站内有上千首音效素材,网络流行的音效素材这里都能找到…...

【前端知识】React 基础巩固(三十五)——ReduxToolKit (RTK)
React 基础巩固(三十五)——ReduxToolKit (RTK) 一、RTK介绍 Redux Tool Kit (RTK)是官方推荐的编写Redux逻辑的方法,旨在成为编写Redux逻辑的标准方式,从而解决上面提到的问题。 RTK的核心API主要有如下几个: confi…...

android Android Studio Giraffe | 2022.3.1 版本Lombok不兼容 解决方案
android Android Studio Giraffe | 2022.3.1 版本Lombok不兼容 解决方案 1.查看当前的android studio 版本 Android Studio Giraffe | 2022.3.1 Build #AI-223.8836.35.2231.10406996, built on June 29, 2023 2.打开 idea 官网下载页面 idea下载历史版本 找到对应的版本编号…...

前端框架学习-基础前后端分离
前端知识栈 前端三要素:HTML、CSS、JS HTML 是前端的一个结构层,HTML相当于一个房子的框架,可类比于毛坯房只有一个结构。CSS 是前端的一个样式层,有了CSS的装饰,相当于房子有了装修。JS 是前端的一个行为层ÿ…...

数据中心电子电气设备常见的五种地线种类和做法
数据中心机房计算机系统的集成化程度很高,其正常工作对环境的要求很严格。接地,是指电力系统和电气装置的中性点、电气设备的外露导电部分和装置外导电部分经由导体与大地相连。其作用主要是防止人身遭受电击、设备和线路遭受损坏、预防火灾和防止雷击、…...

重学C++系列之STL库
一、什么是STL库 STL是“Standard Template Library”的缩写,中文翻译为“标准模板库”。CSTL是一套功能强大的C模板类,提供了通用的模板类和函数,这些模板类和函数可以实现多种流行和常用的算法和数据结构,如字符串操作、链表、队…...

JAVA SE -- 第十四天
(全部来自“韩顺平教育”) 泛型 一、泛型 1、基本介绍 ①泛型又称参数类型,是Jdk5.0出现的新特性,解决数据类型的安全性问题 ②在类声明或实例化时只要指定好需要的具体的类型即可 ③Java泛型可以保证如果程序在编译时没有发…...

微信小程序监测版本更新
在index.js里面 不放到app.js里面是因为有登录页面,在登录页面显示更新不太友好 onShow() {const updateManager wx.getUpdateManager()// 请求完新版本信息的回调updateManager.onCheckForUpdate(res > {if (res.hasUpdate) {// 新版本下载成功updateManage…...

《Java面向对象程序设计》学习笔记——第 2 章 基本数据类型、数组和枚举类型
专栏:《Java面向对象程序设计》学习笔记...