Redis实践 利用Redis实现简单限流

利用Redis来限流,可以限定用户的某个行为在指定的时间里只能允许发生N次。

场景: 某个用户在一秒内只能回复5次,那么利用Redis如何实现呢。

思路:这个限流需求中存在一个滑动时间窗口,我们可以联想到zset数据结构的score值,我们可以通过score来圈出这个时间窗口来。而且我们只需要维护这个时间窗口,窗口之外的数据都可以砍掉。那这个zset 的value填什么比较合适呢?它只需要保证唯一性即可,用 uuid 会比较浪费空间,改用毫秒时间戳比较好。
图如下:

now_ts是当前的毫秒时间戳,我们只需要维护[now_s-period,now_s]这段时间里用户的操作数即可。
具体代码:

public class SimpleRateLimiter {

    private final Jedis jedis;

    public SimpleRateLimiter(Jedis jedis) {
        this.jedis = jedis;
    }

    public boolean isActionAllow(String userId,String actionKey,int period,int maxCount) throws IOException {
        String key=String.format("hist6:%s:%s",userId,actionKey);
        long nowTs=System.currentTimeMillis();
        //毫秒时间戳
        Pipeline pipeline=jedis.pipelined();
        pipeline.multi();//用了multi,也就是事务,能保证一系列指令的原子顺序执行
        //value和score都使用毫秒时间戳
        pipeline.zadd(key,nowTs,nowTs+"");
        //移除时间窗口之前的行为记录,剩下的都是时间窗口内的
        pipeline.zremrangeByScore(key,0,nowTs-period*1000);
        //获得[nowTs-period*1000,nowTs]的key数量
        Response<Long> count=pipeline.zcard(key);
        //每次设置都能保持更新key的过期时间
        pipeline.expire(key,period);
        pipeline.exec();
        pipeline.close();
        return count.get()<=maxCount;
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        Jedis jedis=new Jedis("localhost",6379);
        jedis.auth("iostream");
        SimpleRateLimiter limiter=new SimpleRateLimiter(jedis);
        for (int i = 0; i < 20; i++) {
            //每个用户在1秒内最多能做五次动作
            System.out.println(limiter.isActionAllow("viscu","reply",1,5));
        }
    }
}

由于毫秒时间戳的精度问题,1ms内可能有执行好几次操作,有zset的去重操作,所以会看到true出现了超过5次,说明还不够精确。
我下面用了Thread.sleep(1)来模拟了不同操作的间的时间间隔 可是这种方法并不提倡

public class SimpleRateLimiter {

    private final Jedis jedis;

    public SimpleRateLimiter(Jedis jedis) {
        this.jedis = jedis;
    }

    public boolean isActionAllow(String userId,String actionKey,int period,int maxCount) throws IOException {
        String key=String.format("hist6:%s:%s",userId,actionKey);
        long nowTs=System.currentTimeMillis();
        //毫秒时间戳
        Pipeline pipeline=jedis.pipelined();
        pipeline.multi();
        //value和score都使用毫秒时间戳
        pipeline.zadd(key,nowTs,nowTs+"");
        //移除时间窗口之前的行为记录,剩下的都是时间窗口内的
        pipeline.zremrangeByScore(key,0,nowTs-period*1000);
        //获得[nowTs-period*1000,nowTs]的key数量
        Response<Long> count=pipeline.zcard(key);
        //每次设置都能更新key的过期时间
        pipeline.expire(key,period);
        pipeline.exec();
        pipeline.close();
        return count.get()<=maxCount;
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        Jedis jedis=new Jedis("localhost",6379);
        jedis.auth("iostream");
        SimpleRateLimiter limiter=new SimpleRateLimiter(jedis);
        while (true){
            Thread.sleep(1000); //这里模拟每次经过1s限流之后  "viscu"这个用户就可以重新进行"reply"行为的操作。 
            for (int i = 0; i < 20; i++) {
                //模拟每个动作之间的间隔时间为1ms 具体看情况8 这里只是简单模拟一下。
                Thread.sleep(1);
                //这样就确保了该用户在1秒内最多能做五次动作
                System.out.println(limiter.isActionAllow("viscu","reply",1,5));
            }
        }
    }

}

或者我们可以使用纳秒这种更加精确的数或者加上随机数:

public class SimpleRateLimiter {

    private final Jedis jedis;

    public SimpleRateLimiter(Jedis jedis) {
        this.jedis = jedis;
    }

    public boolean isActionAllow(String userId,String actionKey,int period,int maxCount) throws IOException {
        String key=String.format("hist6:%s:%s",userId,actionKey);
        long nowTs=System.nanoTime();
        //纳秒时间戳
        Pipeline pipeline=jedis.pipelined();
        pipeline.multi();
        //value和score都使用纳秒时间戳
        pipeline.zadd(key,nowTs,nowTs+"");    
        pipeline.zremrangeByScore(key,0,nowTs-period*1000*1000*1000);     
        Response<Long> count=pipeline.zcard(key);      
        pipeline.expire(key,period);
        pipeline.exec();
        pipeline.close();
        return count.get()<=maxCount;
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        Jedis jedis=new Jedis("localhost",6379);
        jedis.auth("iostream");
        SimpleRateLimiter limiter=new SimpleRateLimiter(jedis); 
        for (int i = 0; i < 20; i++) {       
            System.out.println(limiter.isActionAllow("viscu","reply",1,5));
        }
    }

}

posted on 2018-10-20 20:32  小新动感光波  阅读(1961)  评论(0编辑  收藏  举报

导航