2017-02-20 6 views
2

Я использую redis для управления лимитами скорости API, а также с использованием SETEX, чтобы ограничение скорости автоматически сбрасывалось каждый час.ключи redis, застрявшие с TTL в -1

Я обнаружил, что redis не может очистить некоторые ключи и сообщить их TTL по адресу -1. Вот пример Redis-кли сессии, демонстрирующей это, используя заполнитель 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 ключа ограничения скорости истекает сразу после вызова «получить», но до вызова «decr». Что произойдет:

Сначала звонок «get» вернет текущий предел. Предположим, что оно вернулось 500. Затем, всего за несколько миллисекунд, TTL этого ключа истекает, поэтому в Redis он больше не существует. Таким образом, код продолжает работать и достигается вызов «decr». Кроме того, ошибка достигается здесь:

В decr documentation государства (курсив мой):

Уменьшает число, сохраненное на ключ один. Если ключ не , он установлен в 0 перед выполнением операции. (...)

Поскольку ключ был удален (поскольку он истек), инструкция «decr» инициализирует ключ до нуля, а затем уменьшает его, поэтому значение ключа равно -1. И ключ будет создан без TTL, поэтому выпуск TTL key_name также выдаст -1.

Решение для этого может заключаться в том, чтобы обернуть весь этот код внутри transaction block с использованием команд MULTI и EXEC. Однако это может быть медленным, поскольку для повторного подключения к серверу 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 
Смежные вопросы