2017-02-20 48 views
2

我正在使用redis來管理API的速率限制,並且使用SETEX來限制每小時自動重置速率限制。redis鍵在TTL處卡住-1

我發現redis無法清除一些密鑰並報告其TTL-1。這裏有一個Redis的-CLI會話展示這個的一個例子,使用佔位符IP地址:

> GET allowance:127.0.0.1 
> 0 
> TTL allowance:127.0.0.1 
-1 
> GET allowance:127.0.0.1 
0 

注意的是,儘管它的TTL爲負,Redis的不當我GET它清除該鍵。

我試圖重現這種狀態,不能。

> SETEX doomedkey -1 hello 
(error) ERR invalid expire time in SETEX 
> SETEX doomedkey 0 hello 
(error) ERR invalid expire time in SETEX 
> SETEX doomedkey 5 hello 
OK 
> TTL doomedkey 
4 
> GET doomedkey 
hello 

(... wait 5 seconds) 

> TTL doomedkey 
-2 
> GET doomedkey 
(nil) 

這是一些不幸的競爭條件導致redis無法過期這些鍵嗎?在成功過期的成千上萬箇中,只有約10個停留在-1狀態。我正在使用redis_version:2.8.9

+1

'-1'意味着沒有與相關的關鍵到期。我想有人在密鑰上調用了「設置密鑰值」,並且過期已被重置。 –

+0

@for_stack這將是我的回答太 –

+0

有趣。謝謝,我會研究爲什麼會發生這種情況。 –

回答

0

我遇到了同樣的問題,只使用Redis 2.8.24,但也使用它的API速率限制。

我懷疑你正在做限制這樣的(使用Ruby代碼只是爲例子)速度:

def consume_rate_limit 
    # Fetch the current limit for a given account or user 
    rate_limit = Redis.get('available_limit:account_id') 

    # It can be nil if not already initialized or if TTL has expired 
    if rate_limit == nil 
    # So let's just initialize it to the initial limit 
    # Let's use a window of 10,000 requests, resetting every hour 
    rate_limit = 10000 
    Redis.setex('available_limit:account_id', 3600, rate_limit - 1) 
    else 
    # If the key already exists, just decrement the limit 
    Redis.decr('available_limit:account_id') 
    end 

    # Return true if we are OK or false the limit has been reached 
    return (rate_limit > 0) 
end 

嗯,我就是用這個方法,發現有間「搞定」一個cocurrency問題和「decr」調用導致你描述的確切問題。

當速率限制密鑰的TTL在「get」調用之後但在「decr」調用之前到期時,會發生此問題。將會發生什麼:

首先,「get」調用將返回當前的限制。假設它返回500. 然後,只需幾分之一毫秒的時間,該密鑰的TTL就會過期,因此它在Redis中不再存在。 所以代碼繼續運行,並達到「decr」調用。也達到了錯誤的位置:

decr documentation狀態(我的重點):

遞減一保存在按鍵的號碼。 如果密鑰不存在 ,則在執行操作之前將其設置爲0。 (...)

由於密鑰已被刪除(因爲它已過期),「decr」指令將初始化密鑰爲零,然後遞減密鑰值,這就是密鑰值爲-1的原因。密鑰將在沒有TTL的情況下創建,因此發行TTL key_name也會發布-1。

解決方案可能是使用MULTI和EXEC命令將所有代碼包含在transaction block中。但是,這可能會很慢,因爲它需要多次往返Redis服務器。

我用過的解決方案是編寫一個Lua腳本並使用EVAL命令運行它。它具有原子化的優勢(意味着沒有併發問題),並且只有一個RTT連接到Redis服務器。

local expire_time = ARGV[1] 
local initial_rate_limit = ARGV[2] 
local rate_limit = redis.call('get', KEYS[1]) 
-- rate_limit will be false when the key does not exist. 
-- That's because redis converts Nil to false in Lua scripts. 
if rate_limit == false then 
    rate_limit = initial_rate_limit 
    redis.call('setex', KEYS[1], initial_rate_limit, rate_limit - 1) 
else 
    redis.call('decr', KEYS[1]) 
end 
return rate_limit 

要使用它,我們可以把consume_rate_limit功能如下:

def consume_rate_limit 
    script = <<-LUA 
     ... that script above, omitting it here not to bloat things ... 
    LUA 
    rate_limit = Redis.eval(script, keys: ['available_limit:account_id'], argv: [3600, 10000]).to_i 
    return (rate_limit > 0) 
end