分布式限流之Redis的zset结构基于Lua和Pipeline的技术实现

啥也不说先上代码


   /**
     * 基于redis做的 滑动窗口限流
     *
     * @param key      redis的key
     * @param period   时间段(秒),比如: 限流60(period)秒内, 不能超过100(maxCount)次
     * @param maxCount 最大运行访问次数
     * @return bool true表示放行,false表示被限制未放行
     * @author wuqiong 2022/3/10 14:31
     */
    public boolean isAllowed(String key, int period, long maxCount) {
        // 方式一:  使用pipeline 实现
        try (Jedis jedis = jedisPool.getResource()) {
            long now = System.currentTimeMillis();
            // 1、管道一
            Pipeline pipeline = jedis.pipelined();
            pipeline.zremrangeByScore(key, 0, now - period * 1000);
            Response<Long> countResponse = pipeline.zcard(key);
            pipeline.close();
            if (countResponse.get() >= maxCount) return false; // 直接返回失败,被限制了
            // 2、管道二
            Pipeline pip = jedis.pipelined();
            pip.zadd(key, now, now + "Random"); // 假装一个随机数,各位看官老爷请自己实现. (主要是防止极端情况下时间戳也会重复的问题)
            pip.expire(key, period);// 设置过期时间
            pip.close();
            return true;
        } catch (Exception ex) {
            log.error("[Pipeline]滑动窗口限流失败", ex.getMessage());
        }


        // 方式二:  使用lua 脚本
        try (Jedis jedis = jedisPool.getResource()) {
            String script = "redis.call('zremrangeByScore', KEYS[1], 0, ARGV[1])\n" +
                    "local res = redis.call('zcard', KEYS[1])\n" +
                    "if res and (tonumber(res) < tonumber(ARGV[4])) then\n" +
                    "    redis.call('zadd', KEYS[1], ARGV[2], ARGV[3])\n" +
                    "    redis.call('expire',KEYS[1],ARGV[5]) \n" +
                    "    return 1\n" +
                    "else return 0 end\n";
            long now = System.currentTimeMillis();
            String args1 = "" + (now - period * 1000);
            String args2 = "" + now;
            String args3 = now + "Random"; // 假装一个随机数,各位看官老爷请自己实现. (主要是防止极端情况下时间戳也会重复的问题)
            String args4 = "" + maxCount;  // 最大次数
            String args5 = "" + period; // 过期时间
            Object eval = jedis.eval(script, Arrays.asList(key), Arrays.asList(args1, args2, args3, args4, args5));
            return eval.equals(1L);
        } catch (Exception ex) {
            log.error("[lua]滑动窗口限流失败", ex.getMessage());
        }
        return false;
    }


此处楼主展示了 基于 Lua 和 Pipeline 两种方式,使用zset结构进行的滑动窗口限流。
该限流方式相较于使用 setnx 要好一些,因为使用setnx 限流可能会出现一个bug , 比如: 每60秒限100次访问,在59秒访问了100次,那么下一分钟的0秒再访问100次,综合来看,就是两秒钟对程序访问了200次,因此这种限流方式是不科学的。 可以算作是限流失败,所以楼主比较推荐使用 基于滑动窗口的方式进行限流

分析 lua 和 Pipeline 两种方式优缺点

1、先说结论,楼主更推荐使用lua脚本
2、在使用 pipeline 时可以明显的看到需要开启两次 管道,并不能在一次内完成
3、恰好当第一个管道执行完毕后待执行第二个管道时,其他线程可能会进入造成影响(这是一个比较极端罕见的情况)
4、但 Lua 脚本则不存在此问题,可在一次原子性的操作内完成整个流程,不给其他线程入侵的机会
5、经楼主测试,在网络环境较弱的情况下,Lua 脚本相较于 Pipeline 管道有明显的优势。

补充说明 redis 的zset 结构

1、楼主此处对 zset 结构的member 设置了和 score 相同的时间戳值,此时member字段无意义,只是存了一个值而已, 如下图:
软件内查看zset结构界面

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
实现分布式限流可以使用 RedisLua 脚本来完成。以下是可能的实现方案: 1. 使用 Redis 的 SETNX 命令来实现基于令牌桶算法的限流 令牌桶算法是一种常见的限流算法,它可以通过令牌的放置和消耗来控制流量。在 Redis 中,我们可以使用 SETNX 命令来实现令牌桶算法。 具体实现步骤如下: - 在 Redis 中创建一个有序集合,用于存储令牌桶的令牌数量和时间戳。 - 每当一个请求到达时,我们首先获取当前令牌桶中的令牌数量和时间戳。 - 如果当前时间戳与最后一次请求的时间戳之差大于等于令牌桶中每个令牌的发放时间间隔,则将当前时间戳更新为最后一次请求的时间戳,并且将令牌桶中的令牌数量增加相应的数量,同时不超过最大容量。 - 如果当前令牌桶中的令牌数量大于等于请求需要的令牌数量,则返回 true 表示通过限流,将令牌桶中的令牌数量减去请求需要的令牌数量。 - 如果令牌桶中的令牌数量不足,则返回 false 表示未通过限流。 下面是使用 RedisLua 脚本实现令牌桶算法的示例代码: ```lua -- 限流的 key local key = KEYS[1] -- 令牌桶的容量 local capacity = tonumber(ARGV[1]) -- 令牌的发放速率 local rate = tonumber(ARGV[2]) -- 请求需要的令牌数量 local tokens = tonumber(ARGV[3]) -- 当前时间戳 local now = redis.call('TIME')[1] -- 获取当前令牌桶中的令牌数量和时间戳 local bucket = redis.call('ZREVRANGEBYSCORE', key, now, 0, 'WITHSCORES', 'LIMIT', 0, 1) -- 如果令牌桶为空,则初始化令牌桶 if not bucket[1] then redis.call('ZADD', key, now, capacity - tokens) return 1 end -- 计算当前令牌桶中的令牌数量和时间戳 local last = tonumber(bucket[2]) local tokensInBucket = tonumber(bucket[1]) -- 计算时间间隔和新的令牌数量 local timePassed = now - last local newTokens = math.floor(timePassed * rate) -- 更新令牌桶 if newTokens > 0 then tokensInBucket = math.min(tokensInBucket + newTokens, capacity) redis.call('ZADD', key, now, tokensInBucket) end -- 检查令牌数量是否足够 if tokensInBucket >= tokens then redis.call('ZREM', key, bucket[1]) return 1 else return 0 end ``` 2. 使用 RedisLua 脚本来实现基于漏桶算法的限流 漏桶算法是另一种常见的限流算法,它可以通过漏桶的容量和漏水速度来控制流量。在 Redis 中,我们可以使用 Lua 脚本来实现漏桶算法。 具体实现步骤如下: - 在 Redis 中创建一个键值对,用于存储漏桶的容量和最后一次请求的时间戳。 - 每当一个请求到达时,我们首先获取当前漏桶的容量和最后一次请求的时间戳。 - 计算漏水速度和漏水的数量,将漏桶中的容量减去漏水的数量。 - 如果漏桶中的容量大于等于请求需要的容量,则返回 true 表示通过限流,将漏桶中的容量减去请求需要的容量。 - 如果漏桶中的容量不足,则返回 false 表示未通过限流。 下面是使用 RedisLua 脚本实现漏桶算法的示例代码: ```lua -- 限流的 key local key = KEYS[1] -- 漏桶的容量 local capacity = tonumber(ARGV[1]) -- 漏水速度 local rate = tonumber(ARGV[2]) -- 请求需要的容量 local size = tonumber(ARGV[3]) -- 当前时间戳 local now = redis.call('TIME')[1] -- 获取漏桶中的容量和最后一次请求的时间戳 local bucket = redis.call('HMGET', key, 'capacity', 'last') -- 如果漏桶为空,则初始化漏桶 if not bucket[1] then redis.call('HMSET', key, 'capacity', capacity, 'last', now) return 1 end -- 计算漏水的数量和漏桶中的容量 local last = tonumber(bucket[2]) local capacityInBucket = tonumber(bucket[1]) local leak = math.floor((now - last) * rate) -- 更新漏桶 capacityInBucket = math.min(capacity, capacityInBucket + leak) redis.call('HSET', key, 'capacity', capacityInBucket) redis.call('HSET', key, 'last', now) -- 检查容量是否足够 if capacityInBucket >= size then return 1 else return 0 end ``` 以上是使用 RedisLua 脚本实现分布式限流的两种方案,可以根据实际需求选择适合的方案。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值