Redis 多规则限流和防重复提交方案实现

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

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

一:使用 String 结构记录固定时间段内某用户 IP 访问某接口的次数

  • RedisKey = prefix : className : methodName
  • RedisVlue = 访问次数

拦截请求:

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

规则是每分钟访问 1000 次

  • 假设目前 RedisKey => RedisValue 为 999;
  • 目前大量请求进行到第一步( 获取 Redis 请求次数 ),那么所有线程都获取到了值为999,进行判断都未超过限定次数则不拦截,导致实际次数超过 1000 次
  • 解决办法: 保证方法执行原子性(加锁、Lua)。

考虑在临界值进行访问
在这里插入图片描述

二:使用 Zset 进行存储,解决临界值访问问题
在这里插入图片描述

三:实现多规则限流

①、先确定最终需要的效果(能实现多种限流规则+能实现防重复提交)

@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 注解

@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 的方法
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(可用其他有相同的特性的值)为 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 do
    local 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 then
        return true
    end
    -- 3.3 判断元素最大值,设置为最终过期时间
    if rateRuleTime > expireTime then
        expireTime = rateRuleTime
    end
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 do
    local 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 then
        return true
    end
    -- 3.3 判断元素最大值,设置为最终过期时间
    if rateRuleTime > expireTime then
        expireTime = rateRuleTime
    end
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 do
    local result = redis.call('ZADD', key, currentTime, currentTime)
    if result == 1 then
        -- 6.2 添加成功则跳出循环
        break
    else
        -- 6.3 未添加成功则 value + 1 再次进行尝试
        retries = retries + 1
        if retries >= maxRetries then
            -- 6.4 超过最大尝试次数 采用添加随机数策略
            local random_value = math.random(1, 1000)
            currentTime = currentTime + random_value
        else
            currentTime = currentTime + 1
        end
    end
end

return 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. 生成 key
    String 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. 构建 args
    Long[] 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;
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

飞天使-linux操作的一些技巧与知识点7-devops

文章目录 简述devopsCICD 简述devops 让技术团队&#xff0c;运维&#xff0c;测试等团队实现一体式流程自动化 进阶版图 CICD 持续集成&#xff0c; 从编译&#xff0c;测试&#xff0c;发布的完成自动化流程 持续交付&#xff0c;包含持续集成&#xff0c;并且将项目部署…

2023年总结与2024展望

今天是春节后上班第一天&#xff0c;你懂的&#xff0c;今天基本上是摸鱼状态&#xff0c;早上把我们负责的项目的ppt介绍完善了一下&#xff0c;然后写了一篇技术文章&#xff0c;《分布式系统一致性与共识算法》。接着就看了我近几年写的的年度总结&#xff0c;我一般不会在元…

K线实战分析系列之八:十字星——容易识别的特殊形态

K线实战分析系列之八&#xff1a;十字星——容易识别的特殊形态 一、十字启明星和十字黄昏星二、弃婴底部形态和弃婴顶部形态三、总结十字启明星和十字黄昏星形态的要点 一、十字启明星和十字黄昏星 当开盘价与收盘价极为接近的时候&#xff0c;当期的K线就呈现为一根十字线&am…

01|Mysql底层存储引擎

1. 聚集索引&#xff08;聚簇&#xff09;与非聚集索引 1.1 聚集索引 索引和数据存储在一起。叶子节点存储了完整的数据记录&#xff1b; 1.2 非聚集索引 MyISAM存储引擎就是非聚集索引&#xff0c;索引和数据文件是分开存储的。索引在MYI文件中&#xff0c;数据在MYD文件中…

蓝桥杯-数字三角形

原题链接&#xff1a;用户登录 上图给出了一个数字三角形。从三角形的顶部到底部有很多条不同的路径。对于每条路径&#xff0c;把路径上面的数加起来可以得到一个和&#xff0c;你的任务就是找到最大的和 (路径上的每一步只可沿左斜线向下或右斜线向下走)。 输入描述 输入的第…

普中51单片机学习(8*8LED点阵)

8*8LED点阵 实验代码 #include "reg52.h" #include "intrins.h"typedef unsigned int u16; typedef unsigned char u8; u8 lednum0x80;sbit SHCPP3^6; sbit SERP3^4; sbit STCPP3^5;void HC595SENDBYTE(u8 dat) {u8 a;SHCP1;STCP1;for(a0;a<8;a){SERd…

Jmeter分布式测试必踩坑,全部帮你排雷

在jmeter分布式环境部署上&#xff0c;有很同学都遇到了不少问题&#xff0c;就算是看过安装教程&#xff0c;也会在实际操作的时候一脸懵&#xff0c;经常的状态是就是&#xff1a;眼睛会了手不会。 所以我们把大家容易出问题的地方总结出来&#xff0c;一起来看看吧&#xff…

5个免费文章神器,用来改写文章太方便了

在当今信息爆炸的时代&#xff0c;内容创作和编辑是网络世界中至关重要的环节。然而&#xff0c;有时候我们可能会遇到一些内容需要进行改写或者重组的情况。为了提高效率&#xff0c;让这一过程更加顺畅&#xff0c;我们可以借助一些免费的文章神器来帮助我们完成这一任务。下…

板块一 Servlet编程:第七节 ServletContext对象全解与Servlet三大域对象总结 来自【汤米尼克的JAVAEE全套教程专栏】

板块一 Servlet编程&#xff1a;第七节 ServletContext对象全解与Servlet三大域对象总结 一、什么是ServletContext对象二、获取ServletContext对象及常用方法&#xff08;1&#xff09;获取 ServletContext 对象&#xff08;2&#xff09;ServletContext对象提供的方法 三、se…

pytorch自定义数据集分类resnet18

# 文件结构为&#xff1a; # |--- data # |--- dog # |--- dog1_1.jpg # |--- dog1_2.jpg # |--- cat # |--- cat2_1.jpg # |--- cat2_2.jpg import torch import torchvision import torchvision.transforms as transforms import torch.nn as nn import to…

【软件测试面试】要你介绍项目-如何说?完美面试攻略...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 1、测试面试时&am…

UI风格汇:扁平化风格来龙去脉,特征与未来趋势

Hello&#xff0c;我是大千UI工场&#xff0c;设计风格是我们新开辟的栏目&#xff0c;主要讲解各类UI风格特征、辨识方法、应用场景、运用方法等&#xff0c;本次带来的扁平化风格的解读&#xff0c;有设计需求&#xff0c;我们也可以接单。 一、什么是扁平化风格 扁平化风格…

C# EF Core迁移数据库

现象&#xff1a; 在CodeFirst时&#xff0c;先写字段与表&#xff0c;创建数据库后&#xff0c;再添加内容 但字段与表会变更&#xff0c;比如改名删除增加等 需求&#xff1a; 当表字段变更时&#xff0c;同时变更数据库&#xff0c;执行数据库迁移 核心命令 Add-Migrat…

陪诊小程序:温暖您的就医之路,让关怀触手可及

随着社会的进步和科技的发展&#xff0c;人们对于医疗健康的需求日益增长。然而&#xff0c;在繁忙的生活节奏中&#xff0c;许多人在面对就医时却面临着无人陪伴的困境。为了解决这一问题&#xff0c;陪诊小程序应运而生。 陪诊小程序是一种便捷、高效、人性化的医疗服务应用…

9-pytorch-现有模型使用及修改

b站小土堆pytorch教程学习笔记 1 使用ImageNet测试模型vgg16 train_datatorchvision.datasets.ImageNet(dataset/ImageNet,trainTrue ,downloadTrue ,transformtorchvision.transforms.ToTensor())代码运行报错&#xff1a;ImageNet数据集过大&#xff0c;导致现在无法公开访问…

聊聊 Go 边界检查消除

前言 在这篇文章中碰巧看到了Go边界检查消除相关的讨论. 我也借此简单聊聊. 有这样一段代码, 非常简单, 就是一段求向量点积的程序: func sum(a, b []int) int {if len(a) ! len(b) {panic("must be same len")}ret : 0for i : 0; i < len(a); i {ret a[i] * …

SAM轻量化的终点竟然是RepViT + SAM

本文首发&#xff1a;AIWalker&#xff0c;欢迎关注~~ 殊途同归&#xff01;SAM轻量化的终点竟然是RepViT SAM&#xff0c;移动端速度可达38.7fps。 对于 2023 年的计算机视觉领域来说&#xff0c;「分割一切」&#xff08;Segment Anything Model&#xff09;是备受关注的一项…

0-1背包问题-动态规划

解法归纳&#xff1a; 一、如果装不下当前物品&#xff0c;那么前n个物品的最佳组合和前n-1个物品的最佳组合是一样的。 二、如果装得下当前物品。 假设1 :装当前物品&#xff0c;在给当前物品预留了相应空间的情况下&#xff0c;前n-1 个物品的最佳组 合加上当前物品的价值就…

作业 找单身狗2

方法一&#xff1a; 思路&#xff1a; 我们可以先创建一个新的数组&#xff0c;初始化为0&#xff0c;然后让原来的数组里面的元素作为新数组的下标 如果该下标对应的值为0&#xff0c;说明没有出现过该数&#xff0c;赋值为1作为标记&#xff0c;表示出现过1次 如果该下标…

#FPGA(基础知识)

1.IDE:Quartus II 2.设备&#xff1a;Cyclone II EP2C8Q208C8N 3.实验&#xff1a;正点原子-verilog基础知识 4.时序图&#xff1a; 5.步骤 6.代码&#xff1a;
最新文章