Redis实现访问频率控制

    现在系统中由于各种需要,经常遇到一种场景:需要限定每个IP地址每分钟最大访问次数类似的需求。下面是使用Redis实现范文频率限制的一种方式。

    场景:要限制每分钟每个用户最多只能访问100个页面。

    思路:1. 对每个用户使用一个名为“rate.limiting:用户IP”的字符串类型键;

               2. 每次用户访问,使用INCR命令递增该键的键值

               3. 如果递增后的值是1(第一次访问页面),则同时还要设置该键的生存时间为1分钟

               4. 这样每次访问页面时都读取该键的键值,如果超过了100就表明该用户的访问频率超过了限制

               PS:该键每分钟会自动被删除,所以下一分钟用户的访问次数又会重新计算,这样就达到了限制访问频率的目的。

    上述流程的伪代码如下:

$isKeyExists = EXISTS rate.limiting:$IP
if $isKeyExists is 1
    $times = INCR rate.limiting:$IP
    if $times > 100
        print 访问频率超过了限制,请稍后再试
        exit
else
    INCR rate.limiting:$IP
    EXPIRE $keyName, 60

    上面的这段代码存在一个不太明显的问题:加入程序执行完倒数第二行后,因为某种原因突然退出了,没能够为该键值设置生存时间,那么该键会永久存在,导致使用对应IP的用户最多只能访问系统100次,除非管理员手动删除该键。这是一个很严重的问题,但是可以结合Redis的事务功能解决该问题,修改后的伪代码如下:

$isKeyExists = EXISTS rate.limiting:$IP
if $isKeyExists is 1
    $times = INCR rate.limiting:$IP
    if $times > 100 
elsee
    EXPIRE $keyName, 60
    EXEC

    访问频率限制到此基本上已经实现,但是仍然有细节地方可以改进。

    场景:一个用户在1分钟的第1秒访问了1次系统,在同一分钟的最后1秒访问了99次;又在下一分钟的第一秒访问了100次系统,这种情况用户实际上在2秒内访问了199次系统,这与每个用户每分钟只能访问100次的限制的差距较大。

        尽管这种情况比较极端,但是在一些场合中还是需要粒度更小的控制方案。

    问题解决思路:如果要精确的保证每分钟最多访问100次,需要记录下每次访问的时间。因此对每个用户,我们使用一个列表类型的键来记录他最近100次访问时间,一旦键中的元素超过100个,就判断时间最早的元素距离现在的时间是否小于1分钟。如果是则表示最近1分钟内的访问次数超过了100次;如果不是就讲现在的时间加入到列表中,同时把最早的元素删除。

    上述流程的伪代码如下:

$listLength = LLEN rate.limiting:$IP
if $listLength < 100
    LPUSH rate.limiting:$IP, now()
else
    $time = LINDEX rate.limiting:$IP, -1
    if now() - $time < 60
        print 访问频率超过了限制,请稍后再试
    else
        LPUSH rate.limiting:$IP, now()
        LTRIM rate.limiting:$IP, 0, 99

    上述代码中用now()函数获得当前的Unix时间。由于需要记录每次访问的时间,所以当要限制“单位时间最多访问N次” 时,如果N的数值越大,此方法占用的存储空间就越多,实际使用时还需要开发者自己去权衡。除此之外,该方法也会出现竞态条件,使用时请注意。

猜你喜欢

转载自fdd.iteye.com/blog/2366032