Spring Boot集成Redis与Lua脚本:构建高效的分布式多规则限流系统

文章目录

  • Redis多规则限流和防重复提交
    • 记录访问次数
    • 解决临界值访问问题
    • 实现多规则限流
      • 先确定最终需要的效果
      • 编写注解(RateLimiter,RateRule)
      • 拦截注解 RateLimiter
    • 编写lua脚本
      • UUID
      • 时间戳
      • 编写 AOP 拦截
    • 总结

Redis多规则限流和防重复提交

市面上很多介绍redis如何实现限流的,但是大部分都有一个缺点,就是只能实现单一的限流,比如1分钟访问1次或者60分钟访问10次这种,但是如果想一个接口两种规则都需要满足呢,我们的项目又是分布式项目,应该如何解决,下面就介绍一下redis实现分布式多规则限流的方式。

  • 如何一分钟只能发送一次验证码,一小时只能发送10次验证码等等多种规则的限流?
  • 如何防止接口被恶意打击(短时间内大量请求)?
  • 如何限制接口规定时间内访问次数?

记录访问次数

使用 String 结构,记录固定时间段内某用户IP访问某接口的次数
RedisKey = prefix : className : methodName
RedisVlue = 访问次数

拦截请求:

  1. 初次访问时设置 [RedisKey] [RedisValue=1] [规定的过期时间]
  2. 获取 RedisValue 是否超过规定次数,超过则拦截,未超过则对 RedisKey 进行加1

分析: 规则是每分钟访问 1000 次

  1. 考虑并发问题
    • 假设目前 RedisKey => RedisValue 为 999
    • 目前大量请求进行到第一步( 获取Redis请求次数 ),那么所有线程都获取到了值为999,进行判断都未超过限定次数则不拦截,导致实际次数超过 1000 次
    • 解决办法: 保证方法执行原子性(加锁、lua)
  2. 考虑在临界值进行访问
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IL4u0jaY-1721999915731)(https://i-blog.csdnimg.cn/direct/1eacf46030b6471e91ce43d1e5eae900.png)]
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import com.ruoyi.common.annotation.RateLimiter;
import com.ruoyi.common.enums.LimitType;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.ip.IpUtils;/*** 限流处理*/
@Aspect
@Component
public class RateLimiterAspect
{private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class);private RedisTemplate<Object, Object> redisTemplate;private RedisScript<Long> limitScript;@Autowiredpublic void setRedisTemplate1(RedisTemplate<Object, Object> redisTemplate){this.redisTemplate = redisTemplate;}@Autowiredpublic void setLimitScript(RedisScript<Long> limitScript){this.limitScript = 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, point);List<Object> keys = Collections.singletonList(combineKey);try{Long number = redisTemplate.execute(limitScript, keys, count, time);if (StringUtils.isNull(number) || number.intValue() > count){throw new ServiceException("访问过于频繁,请稍候再试");}log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), combineKey);}catch (ServiceException e){throw e;}catch (Exception e){throw new RuntimeException("服务器限流异常,请稍候再试");}}public String getCombineKey(RateLimiter rateLimiter, JoinPoint point){StringBuffer stringBuffer = new StringBuffer(rateLimiter.key());if (rateLimiter.limitType() == LimitType.IP){stringBuffer.append(IpUtils.getIpAddr()).append("-");}MethodSignature signature = (MethodSignature) point.getSignature();Method method = signature.getMethod();Class<?> targetClass = method.getDeclaringClass();stringBuffer.append(targetClass.getName()).append("-").append(method.getName());return stringBuffer.toString();}
}

解决临界值访问问题

使用 Zset 进行存储,解决临界值访问问题。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-frPzmAdy-1721999915734)(https://i-blog.csdnimg.cn/direct/1ce98a4076d343f69e7ed880eff292bf.png)]

实现多规则限流

先确定最终需要的效果

  • 能实现多种限流规则
  • 能实现防重复提交
    通过以上要求设计注解(先想象出最终实现效果)
@RateLimiter(rules = {// 60秒内只能访问10次@RateRule(count = 10, time = 60, timeUnit = TimeUnit.SECONDS),// 120秒内只能访问20次@RateRule(count = 20, time = 120, timeUnit = TimeUnit.SECONDS)},// 防重复提交 (5秒钟只能访问1次)preventDuplicate = true
)

编写注解(RateLimiter,RateRule)

编写 RateLimiter 注解

/*** @Description: 请求接口限制*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface RateLimiter {/*** 限流key*/String key() default RedisKeyConstants.RATE_LIMIT_CACHE_PREFIX;/*** 限流类型 ( 默认 Ip 模式 )*/LimitTypeEnum limitType() default LimitTypeEnum.IP;/*** 错误提示*/ResultCode message() default ResultCode.REQUEST_MORE_ERROR;/*** 限流规则 (规则不可变,可多规则)*/RateRule[] rules() default {};/*** 防重复提交值*/boolean preventDuplicate() default false;/*** 防重复提交默认值*/RateRule preventDuplicateRule() default @RateRule(count = 1, time = 5);
}

编写 RateRule 注解

@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface RateRule {/*** 限流次数*/long count() default 10;/*** 限流时间*/long time() default 60;/*** 限流时间单位*/TimeUnit timeUnit() default TimeUnit.SECONDS;
}

拦截注解 RateLimiter

  • 确定redis存储方式
    • RedisKey = prefix : className : methodName
    • RedisScore = 时间戳
    • RedisValue = 任意分布式不重复的值即可
  • 编写生成 RedisKey 的方法
/*** 通过 rateLimiter 和 joinPoint 拼接  prefix : ip / userId : classSimpleName - methodName** @param rateLimiter 提供 prefix* @param joinPoint   提供 classSimpleName : methodName* @return*/
public String getCombineKey(RateLimiter rateLimiter, JoinPoint joinPoint) {StringBuffer key = new StringBuffer(rateLimiter.key());// 不同限流类型使用不同的前缀switch (rateLimiter.limitType()) {// XXX 可以新增通过参数指定参数进行限流case IP:key.append(IpUtil.getIpAddr(((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest())).append(":");break;case USER_ID:SysUserDetails user = SecurityUtil.getUser();if (!ObjectUtils.isEmpty(user)) key.append(user.getUserId()).append(":");break;case GLOBAL:break;}MethodSignature signature = (MethodSignature) joinPoint.getSignature();Method method = signature.getMethod();Class<?> targetClass = method.getDeclaringClass();key.append(targetClass.getSimpleName()).append("-").append(method.getName());return key.toString();
}

编写lua脚本

两种将时间添加到Redis的方法。

UUID

UUID(可用其他有相同的特性的值)为Zset中的value值

  • 参数介绍
    • KEYS[1] = prefix : ? : className : methodName
    • KEYS[2] = 唯一ID
    • KEYS[3] = 当前时间
    • ARGV = [次数,单位时间,次数,单位时间, 次数, 单位时间 ...]
  • java传入分布式不重复的 value
-- 1. 获取参数
local key = KEYS[1]
local uuid = KEYS[2]
local currentTime = tonumber(KEYS[3])
-- 2. 以数组最大值为 ttl 最大值
local expireTime = -1;
-- 3. 遍历数组查看是否超过限流规则
for i = 1, #ARGV, 2 dolocal rateRuleCount = tonumber(ARGV[i])local rateRuleTime = tonumber(ARGV[i + 1])-- 3.1 判断在单位时间内访问次数local count = redis.call('ZCOUNT', key, currentTime - rateRuleTime, currentTime)-- 3.2 判断是否超过规定次数if tonumber(count) >= rateRuleCount thenreturn trueend-- 3.3 判断元素最大值,设置为最终过期时间if rateRuleTime > expireTime thenexpireTime = rateRuleTimeend
end
-- 4. redis 中添加当前时间
redis.call('ZADD', key, currentTime, uuid)
-- 5. 更新缓存过期时间
redis.call('PEXPIRE', key, expireTime)
-- 6. 删除最大时间限度之前的数据,防止数据过多
redis.call('ZREMRANGEBYSCORE', key, 0, currentTime - expireTime)
return false

时间戳

根据时间戳作为Zset中的value值

  • 参数介绍
    • KEYS[1] = prefix : ? : className : methodName
    • KEYS[2] = 当前时间
    • ARGV = [次数,单位时间,次数,单位时间, 次数, 单位时间 ...]
  • 根据时间进行生成value值,考虑同一毫秒添加相同时间值问题
    • 以下为第二种实现方式,在并发高的情况下效率低,value是通过时间戳进行添加,但是访问量大的话会使得一直在调用 redis.call('ZADD', key, currentTime, currentTime),但是在不冲突value的情况下,会比生成 UUID
-- 1. 获取参数
local key = KEYS[1]
local currentTime = KEYS[2]
-- 2. 以数组最大值为 ttl 最大值
local expireTime = -1;
-- 3. 遍历数组查看是否越界
for i = 1, #ARGV, 2 dolocal rateRuleCount = tonumber(ARGV[i])local rateRuleTime = tonumber(ARGV[i + 1])-- 3.1 判断在单位时间内访问次数local count = redis.call('ZCOUNT', key, currentTime - rateRuleTime, currentTime)-- 3.2 判断是否超过规定次数if tonumber(count) >= rateRuleCount thenreturn trueend-- 3.3 判断元素最大值,设置为最终过期时间if rateRuleTime > expireTime thenexpireTime = rateRuleTimeend
end
-- 4. 更新缓存过期时间
redis.call('PEXPIRE', key, expireTime)
-- 5. 删除最大时间限度之前的数据,防止数据过多
redis.call('ZREMRANGEBYSCORE', key, 0, currentTime - expireTime)
-- 6. redis 中添加当前时间  ( 解决多个线程在同一毫秒添加相同 value 导致 Redis 漏记的问题 )
-- 6.1 maxRetries 最大重试次数 retries 重试次数
local maxRetries = 5
local retries = 0
while true dolocal result = redis.call('ZADD', key, currentTime, currentTime)if result == 1 then-- 6.2 添加成功则跳出循环breakelse-- 6.3 未添加成功则 value + 1 再次进行尝试retries = retries + 1if retries >= maxRetries then-- 6.4 超过最大尝试次数 采用添加随机数策略local random_value = math.random(1, 1000)currentTime = currentTime + random_valueelsecurrentTime = currentTime + 1endend
endreturn false

编写 AOP 拦截

@Autowired
private RedisTemplate<String, Object> redisTemplate;@Autowired
private RedisScript<Boolean> limitScript;/*** 限流* XXX 对限流要求比较高,可以使用在 Redis中对规则进行存储校验 或者使用中间件** @param joinPoint   joinPoint* @param rateLimiter 限流注解*/
@Before(value = "@annotation(rateLimiter)")
public void boBefore(JoinPoint joinPoint, RateLimiter rateLimiter) {// 1. 生成 keyString key = getCombineKey(rateLimiter, joinPoint);try {// 2. 执行脚本返回是否限流Boolean flag = redisTemplate.execute(limitScript,ListUtil.of(key, String.valueOf(System.currentTimeMillis())),(Object[]) getRules(rateLimiter));// 3. 判断是否限流if (Boolean.TRUE.equals(flag)) {log.error("ip: '{}' 拦截到一个请求 RedisKey: '{}'",IpUtil.getIpAddr(((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest()),key);throw new ServiceException(rateLimiter.message());}} catch (ServiceException e) {throw e;} catch (Exception e) {e.printStackTrace();}
}/*** 获取规则** @param rateLimiter 获取其中规则信息* @return*/
private Long[] getRules(RateLimiter rateLimiter) {int capacity = rateLimiter.rules().length << 1;// 1. 构建 argsLong[] args = new Long[rateLimiter.preventDuplicate() ? capacity + 2 : capacity];// 3. 记录数组元素int index = 0;// 2. 判断是否需要添加防重复提交到redis进行校验if (rateLimiter.preventDuplicate()) {RateRule preventRateRule = rateLimiter.preventDuplicateRule();args[index++] = preventRateRule.count();args[index++] = preventRateRule.timeUnit().toMillis(preventRateRule.time());}RateRule[] rules = rateLimiter.rules();for (RateRule rule : rules) {args[index++] = rule.count();args[index++] = rule.timeUnit().toMillis(rule.time());}return args;
}

总结

为了实现多规则限流和防止重复提交,我们可以采用Redis作为后端存储,结合Lua脚本来确保原子性和准确性。下面是基于你的需求和提供的示例代码的详细总结:

设计目标

  • 多规则限流:实现多种不同时间范围内的访问频率控制。
  • 防止重复提交:在一定时间内限制同一请求的重复提交。

方案概述

  1. 使用String结构记录访问次数:适用于简单限流,但容易遇到并发问题。
  2. 使用ZSet结构:解决并发问题,并支持多规则限流。
  3. 编写Lua脚本:用于高效地处理多规则限流逻辑。
  4. AOP拦截器:在Spring框架中使用AspectJ进行请求拦截。

限流规则

  • 规则定义:通过自定义注解@RateLimiter@RateRule来指定限流规则和防重复提交的策略。
  • 规则应用:通过AspectJ切面编程在方法执行前进行检查。

Redis键值设计

  • prefix : ? : className : methodName
  • 分值:时间戳
  • 成员:唯一标识符(如UUID)或时间戳

Lua脚本实现

  • 使用UUID:解决并发问题的同时避免数据冗余。

  • 使用时间戳:简化脚本实现,但需要解决同一毫秒内多个请求的问题。

  • 多规则限流:通过使用ZSet结构和Lua脚本实现。

  • 防重复提交:通过在规则中添加额外的限流规则实现。

  • 并发安全:Lua脚本确保了限流逻辑的原子性。

  • 性能优化:通过预先计算过期时间,减少不必要的Redis命令调用。

这样,你可以有效地在分布式系统中实现多规则限流和防止重复提交。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/50431.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Unity在虚拟现实(VR)游戏开发中的优势

Unity引擎是一个功能强大的游戏开发平台&#xff0c;它在虚拟现实(VR)游戏开发中展现出了许多显著的优势。以下是Unity在VR游戏开发中的主要优势&#xff1a; 跨平台支持 Unity引擎支持将游戏部署到多个平台&#xff0c;包括PC、控制台、移动设备、VR/AR设备等。这种跨平台能…

在CentOS 7上安装和使用PostgreSQL的方法

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站。 简介 关系数据库管理系统是许多网站和应用程序的关键组成部分。它们提供了一种结构化的方式来存储、组织和访问信息。 PostgreSQL&…

每天一个数据分析题(四百五十)- 数据清洗

数据在真正被使用前需进行必要的清洗&#xff0c;使脏数据变为可用数据。下列不属于“脏数据”的是&#xff08;&#xff09; A. 重复数据 B. 错误数据 C. 交叉数据 D. 缺失数据 数据分析认证考试介绍&#xff1a;点击进入 题目来源于CDA模拟题库 点击此处获取答案 数据…

非线性校正算法在红外测温中的应用

非线性校正算法在红外测温中用于修正传感器输出与实际温度之间的非线性关系。红外传感器的输出信号&#xff08;通常是电压或电流&#xff09;与温度的关系理论上是线性的&#xff0c;但在实际应用中&#xff0c;由于传感器特性的限制&#xff0c;这种关系往往呈现出非线性。非…

好书推荐 -- 《精通推荐算法》

新书发布&#xff0c;京东限时15天内5折优惠&#xff0c;半天即可送到。 图书封底有读者微信群&#xff0c;作者也在群里&#xff0c;任何技术、offer选择和职业规划的问题&#xff0c;都可以咨询。 《精通推荐算法》&#xff0c;限时半价&#xff0c;半日达https://u.jd.com…

以flask为后端的博客项目——星云小窝

以flask为后端的博客项目——星云小窝 文章目录 以flask为后端的博客项目——星云小窝前言一、星云小窝项目——项目介绍&#xff08;一&#xff09;二、星云小窝项目——项目启动&#xff08;二&#xff09;三、星云小窝项目——项目结构&#xff08;三&#xff09;四、谈论一…

禁忌搜索算法(Tabu Search,TS)及其Python和MATLAB实现

禁忌搜索算法是一种现代启发式搜索方案&#xff0c;主要用于解决组合优化问题。该算法由George F. Lugeral于1986年首次提出&#xff0c;旨在增强局部搜索算法的性能&#xff0c;避免其陷入局部最优解。禁忌搜索利用一个称为“禁忌表”的数据结构&#xff0c;记住最近访问的解决…

Stable Diffusion 使用详解(3)---- ControlNet

背景 炼丹师在AI绘画的过程中&#xff0c;由于Stable Diffusion的原理是水滴式的扩散作图原理&#xff0c;其实在前面也有提到&#xff0c;他的发挥是‘不稳定’的&#xff0c;因为你没有办法做到精确控制&#xff0c;只能说是大致符合你的预期。你不能总依赖抽卡固定随机数种…

【HDFS】HADOOP-11552.Allow handoff on the server side for RPC requests

今天来分析一下 HADOOP-11552. Allow handoff on the server side for RPC requests. 这个之前没有使用场景,也没有细看,所以一直不明白它到底是做什么的? 最近在做Router RPC异步化,涉及到这个feature的使用,因此决定深入学习一下,特此记录。 根据ISSUE的描述,HDFS的…

Laravel Horizon:队列管理与监控的高级指南

引言 在现代Web应用开发中&#xff0c;任务队列是一个常见的需求&#xff0c;用于处理耗时的任务异步执行。Laravel提供了一个强大的队列系统&#xff0c;而Horizon是Laravel的一个扩展包&#xff0c;专门用于管理和监控队列。Horizon不仅提供了一个美观的Web界面来监控队列&a…

web学习笔记(八十三)git

目录 1.Git的基本概念 2.gitee常用的命令 3.解决两个人操作不同文件造成的冲突 4.解决两个人操作同一个文件造成的冲突 1.Git的基本概念 git是一种管理代码的方式&#xff0c;广泛用于软件开发和版本管理。我们通常使用gitee&#xff08;码云&#xff09;来云管理代码。 …

重生之我当程序猿外包

第一章 个人介绍与收入历程 我出生于1999年&#xff0c;在大四下学期进入了一家互联网公司实习。当时的实习工资是3500元&#xff0c;公司还提供住宿。作为一名实习生&#xff0c;这个工资足够支付生活开销&#xff0c;每个月还能给父母转1000元&#xff0c;自己留2500元用来吃…

前端开发知识(三)-javascript(对象)

一、JS对象 包括JS已经定义的对象&#xff0c;如&#xff0c;Array,Sting &#xff0c;DOM&#xff0c;BOM等&#xff0c;其中&#xff0c;JSON是用户自定义对象&#xff08;除对象外&#xff0c;还有文本&#xff09;&#xff0c;其他是JS定义 1.Array&#xff1a;数组 数…

Java从入门到精通 (十一) ~ 操作系统、进程和线程

无论做什么&#xff0c;请记住都是为你自己而做&#xff0c;这样就毫无怨言&#xff01;今天&#xff0c;我为自己而活&#xff01;今天&#xff0c;又是美丽的一天&#xff01;早安&#xff0c;朋友&#xff01; 目录 前言 一、操作系统 1. 概念 2. 操作系统的基本功能 3…

@RequiredArgsConstructor详解

RequiredArgsConstructor详解 一、什么是RequiredArgsConstructor? RequiredArgsConstructor是Lombok的一个注解&#xff0c;简化了我们对Autowired书写&#xff0c;我们在写Controller层或者Service层的时候&#xff0c;总是需要注入很多mapper接口或者service接口&#xf…

Java-----栈

目录 1.栈&#xff08;Stack&#xff09; 1.1概念 1.2栈的使用 1.3栈的模拟实现 1.4栈的应用场景 1.5栈、虚拟机栈、栈帧有什么区别呢 1.栈&#xff08;Stack&#xff09; 1.1概念 栈&#xff1a;一种特殊的线性表&#xff0c;其只允许在固定的一端进行插入和删除元素操…

【Python】基础学习技能提升代码样例4:常见配置文件和数据文件读写ini、yaml、csv、excel、xml、json

一、 配置文件 1.1 ini 官方-configparser config.ini文件如下&#xff1a; [url] ; section名称baidu https://www.zalou.cnport 80[email]sender ‘xxxqq.com’import configparser # 读取 file config.ini # 创建配置文件对象 con configparser.ConfigParser() # 读…

EEtrade:区块链是什么

区块链&#xff0c;这个近年来频繁出现在我们视野中的术语&#xff0c;已经从一个技术小众圈的词汇&#xff0c;逐渐演变为全球关注的焦点。从比特币的诞生&#xff0c;到如今在金融、供应链、物联网等领域的广泛应用&#xff0c;区块链技术正在深刻地改变着我们的生活。那么&a…

我在高职教STM32——串口通信(5)

大家好,我是老耿,高职青椒一枚,一直从事单片机、嵌入式、物联网等课程的教学。对于高职的学生层次,同行应该都懂的,老师在课堂上教学几乎是没什么成就感的。正因如此,才有了借助 CSDN 平台寻求认同感和成就感的想法。在这里,我准备陆续把自己花了很多心思的教学设计分享…

深入解析 Java 的 switch 语句

深入解析 Java 的 switch 语句 在 Java 编程中&#xff0c;switch 语句是一种常用的控制流语句&#xff0c;它能够根据变量的不同值执行不同的代码块。与 if-else 语句相比&#xff0c;switch 语句在处理多个条件判断时更加简洁和清晰&#xff0c;尤其适用于对一个变量的多个可…