2012-11-01 22 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