• 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
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    ②、注解编写

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    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;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    ③、拦截注解 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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    ④、编写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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    Ⅱ、根据时间戳作为 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    ⑤、编写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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
  • 相关阅读:
    I2C 死锁原因及解决方法
    TDE和数据脱敏功能介绍
    经典模型——Transformer
    实时时钟 RTC
    S09-录入的数据快速分列
    Spark On YARN内存和CPU分配
    Java线上故障排查(CPU、磁盘、内存、网络、GC)+JVM性能调优监控工具+JVM常用参数和命令
    1030 Travel Plan
    网络编程套接字(一) 【简单的Udp网络程序】
    spring boot整合 Redis
  • 原文地址:https://blog.csdn.net/usa_washington/article/details/136284233