• 设为首页
  • 收藏本站
  • 积分充值
  • VIP赞助
  • 手机版
  • 微博
  • 微信
    微信公众号 添加方式:
    1:搜索微信号(888888
    2:扫描左侧二维码
  • 快捷导航
    福建二哥 门户 查看主题

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

    发布者: 姬7089 | 发布时间: 2025-6-19 12:47| 查看数: 73| 评论数: 0|帖子模式

    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)。
    考虑在临界值进行访问


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



    三:实现多规则限流

    ①、先确定最终需要的效果(能实现多种限流规则+能实现防重复提交)
    1. @RateLimiter(
    2.         rules = {
    3.                 // 60秒内只能访问10次
    4.                 @RateRule(count = 10, time = 60, timeUnit = TimeUnit.SECONDS),
    5.                 // 120秒内只能访问20次
    6.                 @RateRule(count = 20, time = 120, timeUnit = TimeUnit.SECONDS)

    7.         },
    8.         // 防重复提交 (5秒钟只能访问1次)
    9.         preventDuplicate = true
    10. )
    复制代码
    ②、注解编写
    RateLimiter 注解
    1. @Target(ElementType.METHOD)
    2. @Retention(RetentionPolicy.RUNTIME)
    3. @Inherited
    4. public @interface RateLimiter {

    5.     /**
    6.      * 限流key
    7.      */
    8.     String key() default RedisKeyConstants.RATE_LIMIT_CACHE_PREFIX;

    9.     /**
    10.      * 限流类型 ( 默认 Ip 模式 )
    11.      */
    12.     LimitTypeEnum limitType() default LimitTypeEnum.IP;

    13.     /**
    14.      * 错误提示
    15.      */
    16.     ResultCode message() default ResultCode.REQUEST_MORE_ERROR;

    17.     /**
    18.      * 限流规则 (规则不可变,可多规则)
    19.      */
    20.     RateRule[] rules() default {};

    21.     /**
    22.      * 防重复提交值
    23.      */
    24.     boolean preventDuplicate() default false;

    25.     /**
    26.      * 防重复提交默认值
    27.      */
    28.     RateRule preventDuplicateRule() default @RateRule(count = 1, time = 5);
    29. }
    复制代码
    RateRule 注解:
    1. @Target(ElementType.ANNOTATION_TYPE)
    2. @Retention(RetentionPolicy.RUNTIME)
    3. @Inherited
    4. public @interface RateRule {

    5.     /**
    6.      * 限流次数
    7.      */
    8.     long count() default 10;

    9.     /**
    10.      * 限流时间
    11.      */
    12.     long time() default 60;

    13.     /**
    14.      * 限流时间单位
    15.      */
    16.     TimeUnit timeUnit() default TimeUnit.SECONDS;

    17. }
    复制代码
    ③、拦截注解 RateLimiter

    • 确定 Redis 存储方式
      RedisKey = prefix : className : methodName
      RedisScore = 时间戳
      RedisValue = 任意分布式不重复的值即可
    • 编写生成 RedisKey 的方法
    1. public String getCombineKey(RateLimiter rateLimiter, JoinPoint joinPoint) {
    2.     StringBuffer key = new StringBuffer(rateLimiter.key());
    3.     // 不同限流类型使用不同的前缀
    4.     switch (rateLimiter.limitType()) {
    5.         // XXX 可以新增通过参数指定参数进行限流
    6.         case IP:
    7.             key.append(IpUtil.getIpAddr(((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest())).append(":");
    8.             break;
    9.         case USER_ID:
    10.             SysUserDetails user = SecurityUtil.getUser();
    11.             if (!ObjectUtils.isEmpty(user)) key.append(user.getUserId()).append(":");
    12.             break;
    13.         case GLOBAL:
    14.             break;
    15.     }
    16.     MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    17.     Method method = signature.getMethod();
    18.     Class<?> targetClass = method.getDeclaringClass();
    19.     key.append(targetClass.getSimpleName()).append("-").append(method.getName());
    20.     return key.toString();
    21. }
    复制代码
    ④、编写Lua脚本(两种将事件添加到Redis的方法)
    Ⅰ:UUID(可用其他有相同的特性的值)为 Zset 中的 value 值

    • 参数介绍:
      KEYS[1] = prefix : ? : className : methodName
      KEYS[2] = 唯一ID
      KEYS[3] = 当前时间
      ARGV = [次数,单位时间,次数,单位时间, 次数, 单位时间 …]
    • 由 Java传入分布式不重复的 value 值
    1. -- 1. 获取参数
    2. local key = KEYS[1]
    3. local uuid = KEYS[2]
    4. local currentTime = tonumber(KEYS[3])
    5. -- 2. 以数组最大值为 ttl 最大值
    6. local expireTime = -1;
    7. -- 3. 遍历数组查看是否超过限流规则
    8. for i = 1, #ARGV, 2 do
    9.     local rateRuleCount = tonumber(ARGV[i])
    10.     local rateRuleTime = tonumber(ARGV[i + 1])
    11.     -- 3.1 判断在单位时间内访问次数
    12.     local count = redis.call('ZCOUNT', key, currentTime - rateRuleTime, currentTime)
    13.     -- 3.2 判断是否超过规定次数
    14.     if tonumber(count) >= rateRuleCount then
    15.         return true
    16.     end
    17.     -- 3.3 判断元素最大值,设置为最终过期时间
    18.     if rateRuleTime > expireTime then
    19.         expireTime = rateRuleTime
    20.     end
    21. end
    22. -- 4. redis 中添加当前时间
    23. redis.call('ZADD', key, currentTime, uuid)
    24. -- 5. 更新缓存过期时间
    25. redis.call('PEXPIRE', key, expireTime)
    26. -- 6. 删除最大时间限度之前的数据,防止数据过多
    27. redis.call('ZREMRANGEBYSCORE', key, 0, currentTime - expireTime)
    28. return false
    复制代码
    Ⅱ、根据时间戳作为 Zset 中的 value 值

    • 参数介绍
      KEYS[1] = prefix : ? : className : methodName
      KEYS[2] = 当前时间
      ARGV = [次数,单位时间,次数,单位时间, 次数, 单位时间 …]
    • 根据时间进行生成 value 值,考虑同一毫秒添加相同时间值问题
      以下为第二种实现方式,在并发高的情况下效率低,value 是通过时间戳进行添加,但是访问量大的话会使得一直在调用 redis.call(‘ZADD’, key, currentTime, currentTime),但是在不冲突 value 的情况下,会比生成 UUID 好。
    1. -- 1. 获取参数
    2. local key = KEYS[1]
    3. local currentTime = KEYS[2]
    4. -- 2. 以数组最大值为 ttl 最大值
    5. local expireTime = -1;
    6. -- 3. 遍历数组查看是否越界
    7. for i = 1, #ARGV, 2 do
    8.     local rateRuleCount = tonumber(ARGV[i])
    9.     local rateRuleTime = tonumber(ARGV[i + 1])
    10.     -- 3.1 判断在单位时间内访问次数
    11.     local count = redis.call('ZCOUNT', key, currentTime - rateRuleTime, currentTime)
    12.     -- 3.2 判断是否超过规定次数
    13.     if tonumber(count) >= rateRuleCount then
    14.         return true
    15.     end
    16.     -- 3.3 判断元素最大值,设置为最终过期时间
    17.     if rateRuleTime > expireTime then
    18.         expireTime = rateRuleTime
    19.     end
    20. end
    21. -- 4. 更新缓存过期时间
    22. redis.call('PEXPIRE', key, expireTime)
    23. -- 5. 删除最大时间限度之前的数据,防止数据过多
    24. redis.call('ZREMRANGEBYSCORE', key, 0, currentTime - expireTime)
    25. -- 6. redis 中添加当前时间  ( 解决多个线程在同一毫秒添加相同 value 导致 Redis 漏记的问题 )
    26. -- 6.1 maxRetries 最大重试次数 retries 重试次数
    27. local maxRetries = 5
    28. local retries = 0
    29. while true do
    30.     local result = redis.call('ZADD', key, currentTime, currentTime)
    31.     if result == 1 then
    32.         -- 6.2 添加成功则跳出循环
    33.         break
    34.     else
    35.         -- 6.3 未添加成功则 value + 1 再次进行尝试
    36.         retries = retries + 1
    37.         if retries >= maxRetries then
    38.             -- 6.4 超过最大尝试次数 采用添加随机数策略
    39.             local random_value = math.random(1, 1000)
    40.             currentTime = currentTime + random_value
    41.         else
    42.             currentTime = currentTime + 1
    43.         end
    44.     end
    45. end

    46. return false
    复制代码
    ⑤、编写AOP拦截
    1. @Autowired
    2. private RedisTemplate<String, Object> redisTemplate;

    3. @Autowired
    4. private RedisScript<Boolean> limitScript;

    5. /**
    6. * 限流
    7. * XXX 对限流要求比较高,可以使用在 Redis中对规则进行存储校验 或者使用中间件
    8. *
    9. * @param joinPoint   joinPoint
    10. * @param rateLimiter 限流注解
    11. */
    12. @Before(value = "@annotation(rateLimiter)")
    13. public void boBefore(JoinPoint joinPoint, RateLimiter rateLimiter) {
    14.     // 1. 生成 key
    15.     String key = getCombineKey(rateLimiter, joinPoint);
    16.     try {
    17.         // 2. 执行脚本返回是否限流
    18.         Boolean flag = redisTemplate.execute(limitScript,
    19.                 ListUtil.of(key, String.valueOf(System.currentTimeMillis())),
    20.                 (Object[]) getRules(rateLimiter));
    21.         // 3. 判断是否限流
    22.         if (Boolean.TRUE.equals(flag)) {
    23.             log.error("ip: '{}' 拦截到一个请求 RedisKey: '{}'",
    24.                     IpUtil.getIpAddr(((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest()),
    25.                     key);
    26.             throw new ServiceException(rateLimiter.message());
    27.         }
    28.     } catch (ServiceException e) {
    29.         throw e;
    30.     } catch (Exception e) {
    31.         e.printStackTrace();
    32.     }
    33. }

    34. /**
    35. * 获取规则
    36. *
    37. * @param rateLimiter 获取其中规则信息
    38. * @return
    39. */
    40. private Long[] getRules(RateLimiter rateLimiter) {
    41.     int capacity = rateLimiter.rules().length << 1;
    42.     // 1. 构建 args
    43.     Long[] args = new Long[rateLimiter.preventDuplicate() ? capacity + 2 : capacity];
    44.     // 3. 记录数组元素
    45.     int index = 0;
    46.     // 2. 判断是否需要添加防重复提交到redis进行校验
    47.     if (rateLimiter.preventDuplicate()) {
    48.         RateRule preventRateRule = rateLimiter.preventDuplicateRule();
    49.         args[index++] = preventRateRule.count();
    50.         args[index++] = preventRateRule.timeUnit().toMillis(preventRateRule.time());
    51.     }
    52.     RateRule[] rules = rateLimiter.rules();
    53.     for (RateRule rule : rules) {
    54.         args[index++] = rule.count();
    55.         args[index++] = rule.timeUnit().toMillis(rule.time());
    56.     }
    57.     return args;
    58. }
    复制代码
    到此这篇关于Redis 多规则限流和防重复提交方案实现小结的文章就介绍到这了,更多相关Redis 多规则限流和防重复提交内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

    来源:https://www.jb51.net/database/335200npz.htm
    免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

    本帖子中包含更多资源

    您需要 登录 才可以下载或查看,没有账号?立即注册

    ×

    最新评论

    QQ Archiver 手机版 小黑屋 福建二哥 ( 闽ICP备2022004717号|闽公网安备35052402000345号 )

    Powered by Discuz! X3.5 © 2001-2023

    快速回复 返回顶部 返回列表