Redis zset 简单限流

场景

为防止用户在短时间内对服务器发起大量请求,服务器端须判断某一用户(userId)的某一个行为(actionKey)在指定时间段(period)内是否超出最大请求次数(maxCount),如果未超过,返回 true ,继续执行业务逻辑,否则返回 false

原理

以用户lucas的reply请求为例。
lucas向服务器发起一次reply请求,先检查系统中保存的以lucas+reply为key的记录的数量N,如果N大于maxCount,返回 false。如果N小于maxCount,将lucas+reply 作为 zset 的 key,当前时间作为zset的member 和 score 插入到zset中,返回 true。
因为系统只需检查从 (当前时间 - period) 到 当前时间 这段时间内用户行为的请求数量,因此可以将小于(当前时间 - period)的所有用户的所有行为的记录删除,因此可以为每条记录设置过期时间。

代码

以下代码参考 钱文品《Redis深度历险:核心原理和应用实践》应用 6:断尾求生——简单限流 ,但有逻辑上的不同。

package me.lucas.redisinaction.redis;

import redis.clients.jedis.Jedis;

/**
 * 用redis zset实现的简单的限流策略
 * 限定 userId 的 actionKey 行为在 period 秒内只允许发生 maxCount 次
 * Author: lin
 * Date: 2019/9/12 9:57
 */
public class MySimpleRateLimiter {
    private Jedis jedis;

    private MySimpleRateLimiter(Jedis jedis) {
        this.jedis = jedis;
    }

    /**
     * @param userId    用户ID
     * @param actionKey 行为标识
     * @param period    时间长度,单位秒
     * @param maxCount  最多次数
     * @return 允许用户行为:true  否则 false
     */
    private boolean isActionAllowed(String userId, String actionKey, int period, int maxCount) {
        String key = String.format("hist:%s:%s", userId, actionKey);

        long nowTs = System.currentTimeMillis();
        double min = 0;
        double max = nowTs - period * 1000;
        // 移除时间窗口之前的行为记录,剩下的都是时间窗口内的
        jedis.zremrangeByScore(key, min, max);
        // 获取窗口内的行为数量
        long count = jedis.zcard(key);
        boolean isAllow = count < maxCount;
        if (isAllow) {
            // key  userId + actionKey
            // score 访问时间
            // member  无特殊意义,只要保证唯一就可以了
            jedis.zadd(key, nowTs, String.valueOf(nowTs));
            // 设置 zset 过期时间,避免冷用户持续占用内存
            // 过期时间应该等于时间窗口的长度
            jedis.expire(key, period );
        }
        return isAllow;
    }

    public static void main(String[] args) throws InterruptedException {
        Jedis jedis = new Jedis();
        MySimpleRateLimiter limiter = new MySimpleRateLimiter(jedis);
        String userId = "lucas";
        String actionKey = "reply";
        int period = 3;
        int maxCount = 1;
        for (int i = 0; i < 20; i++) {
            System.out.println(limiter.isActionAllowed(userId, actionKey, period, maxCount));
            Thread.sleep(1000);
        }
    }
}
/*

 zset remove range by score: 根据score删除value
         一个zset代表 一次某个用户的某个行为,
         key:存储 用户+行为
         value: 只需保证唯一性
         score: 时间窗口,当前时间

zset  有序列表
zset{key,value,score}
set:保证内部value唯一,可以给value赋score,代表这个value的排序权重。内部实现是跳跃列表
实现:
    以 用户Id和操作标识作为 key
    以 当前时间作为 score
    以 当前时间作为 value
    用户请求过来时,
    先将 当前时间-时间窗口长度 之前的该用户和本次操作为key的zset删除
    找出内存中以该用户和本次操作为key的所有zset,判断zset数量,如果zset数量小于maxCount,返回true

 */

运行结果

因为 period = 3,maxCount = 1 ,所以三秒内只允许请求一次。
因为Thread.sleep(1000) , 所以每次请求间隔1秒。
所以最终的结果应当是每三次请求中只有一次返回 true。
Redis zset 简单限流_第1张图片

你可能感兴趣的:(学习笔记)