访问频率控制——防止恶意用户频繁访问

一、需求

       现在有需求如下:限制1秒中,每个用户最多访问10次后台接口

二、方案

       1、方案一:

        采用Redis String数据结构,以用户id为key,访问次数为value。过期时间为1s

        每次访问都使用INCR命令递增该键的键值,如果递增后的值为1(第一次访问),设置过期时间。这样每次访问先获取该键值,当键值超过100时,说明访问频率超过限制,返回“访问频率”。该键过期后,自动删除,所以下一个1s后,重新计数。 

 $isKeyExists = EXISTS rate.limiting:$userId    // 存在返回 1,不存在返回 0
    if $isKeyExists is 1
        $times = INCR rate.limiting:$userId
        if $times > 10        // 第10次访问会增加到11
        print 访问过于频繁,请稍后再试
        exit
    else
        MULTI       // 开启一个事务
        INCR rate.limiting:$userId
        EXPIRE $keyName, 1
        EXEC

        缺点:如果用户在第1s的最后0.1s内访问了9次。在第2s的前0.1s内访问了9次,也就是0.2s内访问了18次,与需求不符,尽管这种情况比较极端,但仍然存在,如果要实现更小粒度的控制方式,需要采用方案二。

       2、方案二

       采用Redis List数据结构,以用户id为key,访问时间存入list为value。不设置过期时间

       用list类型的键,记录最近10次的访问时间。先比较list中的元素是否大于10,如果小于10直接将时间加入list中。如果大于10,就判断时间最早的元素(lindex为-1)距离现在的时间是否小于1s,如果是,则表示用户1s内的访问次数超过10次。如果不是就将当前时间加入list,同时把最早的元素删除。

  伪代码如下:
        $limitLength = LLEN rate.limiting:$userId
        if $limitLength < 10
        LPUSH rate.limiting:$userId, now()
    else
        $time = LINDEX rate.limiting:$userId, -1   // 取最后一个元素
        if now() - $time < 1
        print 访问频率超过限制,请稍后再试
        else
        LPUSH rate.limiting:$userId, now()
        LTRIM rate.limiting:$userId, 0, 9     // 删除[0~9]以外的元素

    缺点:由于拦截了每个接口,采用Redis会造成网络开销。每个接口的耗时大概为100ms左右。会影响整体的性能。  

    方案3:java进程内缓存 

       在java中实现进程内缓存,主要思想是,用1个map做缓存,缓存有个生效时间。过期就删除缓存。这里有2种删除策略,一种是起一个线程,定期删除过期的key,第二个是剔除模式,比较懒,访问到某个key时,才检查key是否过期。

       这种思想类似于方案1。key为用户id,value为ValueWithExpireTime。ValueWithExpireTime包含访问次数和过期时间。

采用进程内缓存,可以避免网络开销,准确度要低一些,需求为限制1s内1个用户访问10次。我们部署两个结点,也就是2个JVM。则代码中的次数为5次。

      因为恶意用户频繁访问毕竟是极少数,所以就采用了此方案。

       ValueWithExpireTime类:

public class ValueWithExpireTime {
    private long expireTime;
    private int value;

    public ValueWithExpireTime(long expireTime, int value) {
        this.expireTime = expireTime;
        this.value = value;
    }

    public long getExpireTime() {
        return expireTime;
    }

    public void setExpireTime(long expireTime) {
        this.expireTime = expireTime;
    }

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}

      expireTime:过期时间

      value:访问次数 

      CacheMap类:

/**
 * @author:lishuo 这里使用懒惰模式,获取key的时候,才剔除过期的key
 * @Date:2018/12/3
 * @Time:19:43
 */
@Service
public class CacheMap {

    @Value("${service.access.maxAccessNumber}")
    private Integer maxAccessNumber;

    @Autowired
    private AsyncService asyncService;
    private Map cache = new ConcurrentHashMap<>();

    public void put(String key, Integer value, long expireTime) {
        ValueWithExpireTime valueWithTimeStamp = new ValueWithExpireTime(expireTime, value);
        cache.put(key, valueWithTimeStamp);
        if (cache.entrySet().size() > maxAccessNumber) {
            asyncService.removeExpireKey(cache);
        }
    }

    public ValueWithExpireTime get(String key) {
        ValueWithExpireTime valueWithExpireTime = cache.get(key);
        return valueWithExpireTime;
    }
}

 

你可能感兴趣的:(决战面试,数据结构与算法)