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

2025-02-07 04:50

本文主要是介绍Redis 多规则限流和防重复提交方案实现小结,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

《Redis多规则限流和防重复提交方案实现小结》本文主要介绍了Redis多规则限流和防重复提交方案实现小结,包括使用String结构和Zset结构来记录用户IP的访问次数,具有一定的参考价值,感兴趣...

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

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

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

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

拦截请求:

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

规则是每分钟访问 1000 次

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

考虑在临界值进行访问

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

二:使用 Zset 进行存储,解决临界值访问问题

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

三:实现多规则限流

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

@RateLimiter(
        rules = {
      China编程          // 60秒内只能访问10次
                @RateRule(count = 10, tandroidime = 60, timeUnit = TimeUnit.SECOpythonNDS),
                // 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 >python 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;
}

到此这篇关于Redis 多规则限流和防重复提交方案实现小结的文章就介绍到这了,更多相关Redis 多规则限流和防重复提交内容请搜索China编程(www.chinasem.cn)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程China编程(www.chinasem.cn)! 

这篇关于Redis 多规则限流和防重复提交方案实现小结的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1153328

相关文章

SpringBoot集成redisson实现延时队列教程

《SpringBoot集成redisson实现延时队列教程》文章介绍了使用Redisson实现延迟队列的完整步骤,包括依赖导入、Redis配置、工具类封装、业务枚举定义、执行器实现、Bean创建、消费... 目录1、先给项目导入Redisson依赖2、配置redis3、创建 RedissonConfig 配

Python的Darts库实现时间序列预测

《Python的Darts库实现时间序列预测》Darts一个集统计、机器学习与深度学习模型于一体的Python时间序列预测库,本文主要介绍了Python的Darts库实现时间序列预测,感兴趣的可以了解... 目录目录一、什么是 Darts?二、安装与基本配置安装 Darts导入基础模块三、时间序列数据结构与

Python使用FastAPI实现大文件分片上传与断点续传功能

《Python使用FastAPI实现大文件分片上传与断点续传功能》大文件直传常遇到超时、网络抖动失败、失败后只能重传的问题,分片上传+断点续传可以把大文件拆成若干小块逐个上传,并在中断后从已完成分片继... 目录一、接口设计二、服务端实现(FastAPI)2.1 运行环境2.2 目录结构建议2.3 serv

C#实现千万数据秒级导入的代码

《C#实现千万数据秒级导入的代码》在实际开发中excel导入很常见,现代社会中很容易遇到大数据处理业务,所以本文我就给大家分享一下千万数据秒级导入怎么实现,文中有详细的代码示例供大家参考,需要的朋友可... 目录前言一、数据存储二、处理逻辑优化前代码处理逻辑优化后的代码总结前言在实际开发中excel导入很

SpringBoot+RustFS 实现文件切片极速上传的实例代码

《SpringBoot+RustFS实现文件切片极速上传的实例代码》本文介绍利用SpringBoot和RustFS构建高性能文件切片上传系统,实现大文件秒传、断点续传和分片上传等功能,具有一定的参考... 目录一、为什么选择 RustFS + SpringBoot?二、环境准备与部署2.1 安装 RustF

Nginx部署HTTP/3的实现步骤

《Nginx部署HTTP/3的实现步骤》本文介绍了在Nginx中部署HTTP/3的详细步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学... 目录前提条件第一步:安装必要的依赖库第二步:获取并构建 BoringSSL第三步:获取 Nginx

springboot中使用okhttp3的小结

《springboot中使用okhttp3的小结》OkHttp3是一个JavaHTTP客户端,可以处理各种请求类型,比如GET、POST、PUT等,并且支持高效的HTTP连接池、请求和响应缓存、以及异... 在 Spring Boot 项目中使用 OkHttp3 进行 HTTP 请求是一个高效且流行的方式。

MyBatis Plus实现时间字段自动填充的完整方案

《MyBatisPlus实现时间字段自动填充的完整方案》在日常开发中,我们经常需要记录数据的创建时间和更新时间,传统的做法是在每次插入或更新操作时手动设置这些时间字段,这种方式不仅繁琐,还容易遗漏,... 目录前言解决目标技术栈实现步骤1. 实体类注解配置2. 创建元数据处理器3. 服务层代码优化填充机制详

Python实现Excel批量样式修改器(附完整代码)

《Python实现Excel批量样式修改器(附完整代码)》这篇文章主要为大家详细介绍了如何使用Python实现一个Excel批量样式修改器,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一... 目录前言功能特性核心功能界面特性系统要求安装说明使用指南基本操作流程高级功能技术实现核心技术栈关键函

Java实现字节字符转bcd编码

《Java实现字节字符转bcd编码》BCD是一种将十进制数字编码为二进制的表示方式,常用于数字显示和存储,本文将介绍如何在Java中实现字节字符转BCD码的过程,需要的小伙伴可以了解下... 目录前言BCD码是什么Java实现字节转bcd编码方法补充总结前言BCD码(Binary-Coded Decima