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

Java语言环境下使用redis进行限流

  • 啥也不说先上代码
      • 分析 lua 和 Pipeline 两种方式优缺点
      • 补充说明 redis 的zset 结构

啥也不说先上代码


   /**
     * 基于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字段无意义,只是存了一个值而已, 如下图:
分布式限流之Redis的zset结构基于Lua和Pipeline的技术实现_第1张图片

你可能感兴趣的:(Java操作,数据库操作,网络相关,java,redis,限流,Lua脚本,Zset结构)