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

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();}
}
http://www.lryc.cn/news/104315.html

相关文章:

  • SLA探活工具EaseProbe
  • [Java] 观察者模式简述
  • linux驱动定时器实现按键按下打印字符
  • 反转链表(JS)
  • [PyTorch][chapter 45][RNN_2]
  • 基于canvas画布的实用类Fabric.js的使用
  • 基于SpringBoot+Vue驾校理论课模拟考试系统源码(自动化部署)
  • SpringBoot使用Redis对用户IP进行接口限流
  • MeterSphere学习篇
  • 大数据技术之Clickhouse---入门篇---数据类型、表引擎
  • 【微服务架构设计】微服务不是魔术:处理超时
  • 天下风云出我辈,AI准独角兽实在智能获评“十大数字经济风云企业
  • SpringBoot2学习笔记
  • 安达发|APS生产派单系统对数字化工厂有哪些影响和作用
  • 状态机的介绍和使用 | 京东物流技术团队
  • tinkerCAD案例:32. 使用对齐工具构建喷泉
  • 一起学数据结构(2)——线性表及线性表顺序实现
  • mqtt协议流程图
  • 7、单元测试--测试RestFul 接口
  • 国家留学基金委(CSC)|发布2024年创新型人才国际合作培养项目实施办法
  • 找好听的配乐、BGM就上这6个网站,免费商用。
  • 【前端知识】React 基础巩固(三十五)——ReduxToolKit (RTK)
  • android Android Studio Giraffe | 2022.3.1 版本Lombok不兼容 解决方案
  • 前端框架学习-基础前后端分离
  • 数据中心电子电气设备常见的五种地线种类和做法
  • 重学C++系列之STL库
  • JAVA SE -- 第十四天
  • 微信小程序监测版本更新
  • 《Java面向对象程序设计》学习笔记——第 2 章 基本数据类型、数组和枚举类型
  • TDesign中后台管理系统-访问后端服务