redis 做分布式限流

参看来源:

https://blog.51cto.com/u_15708799/5703440

 

测试有效代码:我们要做的是:当并发请求超出了限定阈值时,要延迟请求,而不是直接丢弃  。当然也可以把结果给业务端,看业务端是提醒用户下次,还是延迟处理,还是丢弃。

 

 @Test
    public void testLimitWait() throws InterruptedException {
        ExecutorService pool = Executors.newCachedThreadPool();
        log.info("--------{}", redisTemplate.opsForValue().get("abc"));
        for (int j = 1; j <= 5; j++) {
            int i=j;
            pool.execute(() -> {
                Thread.currentThread().setName( Thread.currentThread().getName().replace("-","_"));
                limitWait("abc", 3, 1);
                log.info(i + ":" + true + " ttl:" + redisTemplate.getExpire("abc", TimeUnit.MILLISECONDS));
                try {
                    // 线程等待,模拟执行业务逻辑
                    Thread.sleep(new Random().nextInt(100));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        pool.shutdown();
        pool.awaitTermination(2,TimeUnit.SECONDS);
    }


    /**
     * 达到限流时,则等待,直到新的间隔。
     *
     * @param key 可以是ip + 当前秒  或者 是 uid +  当前秒  或者 固定的一个入口 key
     * @param limitCount   一定时间内最多访问次数
     * @param limitSecond  给定的时间范围 单位(秒)
     */
    public void limitWait(String key, int limitCount, int limitSecond) {
        boolean ok;//放行标志
        do {
            ok = limit(key, limitCount, limitSecond);
            log.info("放行标志={}", ok);
            if (!ok) {
                Long ttl = redisTemplate.getExpire(key, TimeUnit.MILLISECONDS);
                if (null != ttl && ttl > 0) {
                    try {
                        Thread.sleep(ttl);
                        log.info("sleeped:{}", ttl);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        } while (!ok);
    }


    /**
     * 限流方法    true-放行;false-限流
     *
     * @param key
     * @param limitCount
     * @param limitSecond
     * @return
     */
    public boolean limit(String key, int limitCount, int limitSecond) {
        List<String> keys = Collections.singletonList(key);
        String luaScript = buildLuaScript();
        RedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
        Long count = redisTemplate.execute(redisScript, keys, limitCount, limitSecond);
        log.info("Access try count is {} for key = {}", count, key);
        if (count != null && count.intValue() <= limitCount) {
            return true;//放行
        } else {
            return false;//限流
//            throw new RuntimeException("You have been dragged into the blacklist");
        }
    }


    /**
     * 编写 redis Lua 限流脚本
     */
    public String buildLuaScript() {
        StringBuilder lua = new StringBuilder();
        lua.append("local c");
        lua.append("\nc = redis.call('get',KEYS[1])");
        // 实际调用次数超过阈值,则直接返回
        lua.append("\nif c and tonumber(c) > tonumber(ARGV[1]) then");
        lua.append("\nreturn c;");
        lua.append("\nend");
        // 执行计算器自加
        lua.append("\nc = redis.call('incr',KEYS[1])");
        lua.append("\nif tonumber(c) == 1 then");
        // 从第一次调用开始限流,设置对应键值的过期
        lua.append("\nredis.call('expire',KEYS[1],ARGV[2])");
        lua.append("\nend");
        lua.append("\nreturn c;");
        return lua.toString();
    }

 

 

关于TTL(Time to Live)
不管是redis还是jedis,其实都是利用了消息的ttl(Time to Live),即,当消息的ttl=0时,消息会自动过期。ttl还见诸于RabbitMQ的死信队列,队列里的消息会延迟消费,当等待ttl指定的时间后,才会自动转移到实时队列。

redis是使用RedisTemplate.expire来设置ttl;使用RedisTemplate.getExpire(key)或RedisTemplate.getExpire(key,TimeUnit)方法来获取ttl。当然,对于并发限流,我们需要使用后者指定时间单位为TimeUnit.MILLISECONDS来得到精确的剩余毫秒数。

jedis是使用Jedis.expire来设置ttl;使用Jedis.ttl(key)方法来获取ttl,返回的时间是毫秒。

getExpire/ttl返回值:

-2:key不存在
-1:未设置ttl
n:实际的剩余ttl
 

redis.incr指令说明
关于redis的increment :

当key不存在时,创建key,默认值是delta值(不指定delta的话,则为1)。
当key存在时,按delta来递增。

posted @ 2023-04-23 19:17  变换  阅读(43)  评论(0编辑  收藏  举报