2012-11-01 27 views
16

我用INCREXPIRE实施限速限制(下面的例子中,只允许每个mintues 5个请求):如何实现率使用Redis的

if EXISTS counter 
    count = INCR counter 
else 
    EXPIRE counter 60 
    count = INCR counter 

if count > 5 
    print "Exceeded the limit"  

但有一个人可以发送一个问题在一分钟的最后一秒内发出5个请求,在下一分钟的第一秒发出5个其他请求,换句话说,在两秒钟内发出10个请求。

有没有更好的方法来避免这个问题?


更新:我想出了一个想法:使用列表来实现它。

times = LLEN counter 
if times < 5 
    LPUSH counter now() 
else 
    time = LINDEX counter -1 
    if now() - time < 60 
     print "Exceeded the limit" 
    else 
     LPUSH counter now() 
LTRIM counter 5 

这是一个好方法吗?

+0

是的,这是一个有效的和良好的解决方案。甚至比使用集合更好;) – alto

+0

在您的解决方案中,now()在Redis LUA脚本中不受支持吗?所以你想通过now()作为参数,那时不同的机器会有不同的毫秒粒度rit ..?所以现在() - 时间不准确? –

+0

对于第二个例子,我估计大约120秒后'计数器'有意义,特别是如果你有很多'计数器'键。 – keune

回答

9

您可以从“最后一分钟的5个请求”切换到“分钟x的5个请求”。通过这种有可能做到:

counter = current_time # for example 15:03 
count = INCR counter 
EXPIRE counter 60 # just to make sure redis doesn't store it forever 

if count > 5 
    print "Exceeded the limit" 

如果要使用“在最后一分钟5点要求”保持,那么你可以做

counter = Time.now.to_i # this is Ruby and it returns the number of milliseconds since 1/1/1970 
key = "counter:" + counter 
INCR key 
EXPIRE key 60 

number_of_requests = KEYS "counter"*" 
if number_of_requests > 5 
    print "Exceeded the limit" 

如果您有生产的制约因素(尤其是性能),那么使用KEYS关键字就是not advised。我们可以使用代替:

counter = Time.now.to_i # this is Ruby and it returns the number of milliseconds since 1/1/1970 
set = "my_set" 
SADD set counter 1 

members = SMEMBERS set 

# remove all set members which are older than 1 minute 
members {|member| SREM member if member[key] < (Time.now.to_i - 60000) } 

if (SMEMBERS set).size > 5 
    print "Exceeded the limit" 

这一切都是假的Ruby代码,但应该给你的想法。

+2

'keys'命令的使用[不建议](http://redis.io/commands /键)。 –

+0

你是对的。但是我们对任何与生产有关的限制还是一无所知。不过,我编辑了我的答案,使用* Redis设置*代替。 – alto

2

你的更新是一个非常好的算法,虽然我一个做一些改动:

times = LLEN counter 
if times < 5 
    LPUSH counter now() 
else 
    time = LINDEX counter -1 
    if now() - time <= 60 
     print "Exceeded the limit" 
    else 
     LPUSH counter now() 
     RPOP counter 
+5

你为什么做这个改变?你背后的推理是什么? – Matrix

0

这是一种替代方法。如果目标是在收到第一个请求时计时器开始每Y秒将请求数量限制为X个请求,那么您可以为每个想要跟踪的用户创建2个密钥:一个用于第一个请求的时间收到了另一个请求,并提出了请求的数量。

key = "123" 
key_count = "ct:#{key}" 
key_timestamp = "ts:#{key}" 

if (not redis[key_timestamp].nil?) && (not redis[key_count].nil?) && (redis[key_count].to_i > 3) 
    puts "limit reached" 
else 
    if redis[key_timestamp].nil? 
     redis.multi do 
      redis.set(key_count, 1) 
      redis.set(key_timestamp, 1) 
      redis.expire(key_timestamp,30) 
     end 
    else 
     redis.incr(key_count) 
    end 
    puts redis[key_count].to_s + " : " + redis[key_timestamp].to_s + " : " + redis.ttl(key_timestamp).to_s 
end 
+0

在这个算法中关键点数是否减少?似乎不是 –

2

这是一个已经回答了的老问题,但这里有一个实现,我从这里采取了一些灵感。我使用的是ioredis对Node.js的

这里是它的所有异步滚动窗口时间限制器但竞争条件免费(我希望)荣耀:

var Ioredis = require('ioredis'); 
var redis = new Ioredis(); 

// Rolling window rate limiter 
// 
// key is a unique identifier for the process or function call being limited 
// exp is the expiry in milliseconds 
// maxnum is the number of function calls allowed before expiry 
var redis_limiter_rolling = function(key, maxnum, exp, next) { 
    redis.multi([ 
    ['incr', 'limiter:num:' + key], 
    ['time'] 
    ]).exec(function(err, results) { 
    if (err) { 
     next(err); 
    } else { 
     // unique incremented list number for this key 
     var listnum = results[0][1]; 
     // current time 
     var tcur = (parseInt(results[1][1][0], 10) * 1000) + Math.floor(parseInt(results[1][1][1], 10)/1000); 
     // absolute time of expiry 
     var texpiry = tcur - exp; 
     // get number of transacation in the last expiry time 
     var listkey = 'limiter:list:' + key; 
     redis.multi([ 
     ['zadd', listkey, tcur.toString(), listnum], 
     ['zremrangebyscore', listkey, '-inf', texpiry.toString()], 
     ['zcard', listkey] 
     ]).exec(function(err, results) { 
     if (err) { 
      next(err); 
     } else { 
      // num is the number of calls in the last expiry time window 
      var num = parseInt(results[2][1], 10); 
      if (num <= maxnum) { 
      // does not reach limit 
      next(null, false, num, exp); 
      } else { 
      // limit surpassed 
      next(null, true, num, exp); 
      } 
     } 
     }); 
    } 
    }); 
}; 

,在这里是一种锁定式限速器:

// Lockout window rate limiter 
// 
// key is a unique identifier for the process or function call being limited 
// exp is the expiry in milliseconds 
// maxnum is the number of function calls allowed within expiry time 
var util_limiter_lockout = function(key, maxnum, exp, next) { 
    // lockout rate limiter 
    var idkey = 'limiter:lock:' + key; 
    redis.incr(idkey, function(err, result) { 
    if (err) { 
     next(err); 
    } else { 
     if (result <= maxnum) { 
     // still within number of allowable calls 
     // - reset expiry and allow next function call 
     redis.expire(idkey, exp, function(err) { 
      if (err) { 
      next(err); 
      } else { 
      next(null, false, result); 
      } 
     }); 
     } else { 
     // too many calls, user must wait for expiry of idkey 
     next(null, true, result); 
     } 
    } 
    }); 
}; 

Here's a gist of the functions。如果您发现任何问题,请告知我。

1

执行速率限制的标准方法是通过Leaky bucket algorithm。使用计数器的缺点是,用户可以在计数器复位后立即执行一堆请求,即在下一分钟的第一秒内针对您的情况执行5次动作。泄漏桶算法解决了这个问题。简而言之,您可以使用有序集来存储您的“漏桶”,并使用动作时间戳作为填充它的键。

看看这篇文章的具体实现: Better Rate Limiting With Redis Sorted Sets

0

我与LIST尝试,过期,热释光

如果TPS是每秒5幅,然后
吞吐量= 5
斜升= 1000(1000ms = 1秒)
interval = 200ms

local counter = KEYS[1] 
local throughput = tonumber(ARGV[1]) 
local rampUp = tonumber(ARGV[2]) 
local interval = rampUp/throughput 
local times = redis.call('LLEN', counter) 

if times == 0 then 
    redis.call('LPUSH', counter, rampUp) 
    redis.call('PEXPIRE', counter, rampUp) 
    return true 
elseif times < throughput then 
    local lastElemTTL = tonumber(redis.call('LINDEX', counter, 0)) 
    local currentTTL = redis.call('PTTL', counter) 
    if (lastElemTTL-currentTTL) < interval then 
     return false 
    else 
     redis.call('LPUSH', counter, currentTTL) 
     return true 
    end 
else 
    return false 
end 

更简单的版本:

local tpsKey = KEYS[1] 
local throughput = tonumber(ARGV[1]) 
local rampUp = tonumber(ARGV[2]) 
-- Minimum interval to accept the next request. 
local interval = rampUp/throughput 
local currentTime = redis.call('PTTL', tpsKey) 

-- -2 if the key does not exist, so set an year expiry 
if currentTime == -2 then 
    currentTime = 31536000000 - interval 
    redis.call('SET', tpsKey, 31536000000, "PX", currentTime) 
end 

local previousTime = redis.call('GET', tpsKey) 

if (previousTime - currentTime) >= interval then 
     redis.call('SET', tpsKey, currentTime, "PX", currentTime) 
     return true 
else 
     redis.call('ECHO',"0. ERR - MAX PERMIT REACHED IN THIS INTERVAL") 
     return false 
end