简体   繁体   English

redis键在-1处卡住TTL

[英]redis keys getting stuck with TTL at -1

I am using redis to manage rate limits on an API, and using SETEX to have the rate limit automatically reset every hour. 我正在使用redis管理API上的速率限制,并使用SETEX来使速率限制每小时自动重置。

I've found that redis is failing to purge some keys and reporting their TTL at -1 . 我发现redis无法清除某些键并在-1处报告其TTL Here's an example of a redis-cli session demonstrating this, using a placeholder IP address: 这是一个使用占位符IP地址的redis-cli会话演示此示例:

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

Notice that despite its TTL being negative, redis does not purge this key when I GET it. 请注意,尽管TTL为负,但在我GET它时,redis不会清除此键。

I've tried to reproduce this state and cannot. 我试图重现这种状态,但是不能。

> 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)

Is this some unfortunate race condition causing redis to fail to expire these keys? 这是某种不幸的竞争状况导致redis无法使这些密钥失效吗? Out of tens of thousands that have been successfully expired, only about 10 remain stuck in the -1 state. 在成千上万成功过期的数据中,只有约10个处于-1状态。

I am using redis_version:2.8.9 . 我正在使用redis_version:2.8.9

I ran into the same issue, only using Redis 2.8.24, but also using it for API rate limiting. 我遇到了相同的问题,仅使用Redis 2.8.24,但也将其用于API速率限制。

I suspect you are doing the rate limiting like this (using Ruby code just for the example): 我怀疑您正在像这样进行速率限制(仅以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

Well, I was using this approach and found out there's a cocurrency problem between the "get" and the "decr" call which leads to the exact issue you described. 好吧,我正在使用这种方法,发现“ get”和“ decr”调用之间存在并发问题,导致您所描述的确切问题。

The issue happens when the TTL of the rate-limit key expires just after the "get" call but before the "decr" call. 当速率限制键的TTL在“ get”调用之后但在“ decr”调用之前到期时,就会发生此问题。 What will happen: 会发生什么:

First the "get" call will return the current limit. 首先,“ get”调用将返回当前限制。 Let's say it returned 500. Then in just a matter of some fraction of miliseconds, the TTL of that key expires, so it doesn't exist anymore in Redis. 假设它返回了500。然后在短短的几分之一毫秒内,该键的TTL失效,因此Redis中不再存在该键。 So the code continues to run and the "decr" call is reached. 因此,代码继续运行,并且达到了“ decr”调用。 Also the bug is reached here: 还可以在这里找到错误:

The decr documentation states (my emphasis): decr文档指出(我的重点):

Decrements the number stored at key by one. 将键处存储的数字减一。 If the key does not exist, it is set to 0 before performing the operation . 如果键不存在,则在执行操作之前将其设置为0 (...) (......)

As the key has been deleted (because it has expired), the "decr" instruction will initialize the key to zero and then decrement it, which is why the key value is -1. 由于密钥已删除(因为它已过期),因此“ decr”指令会将密钥初始化为零,然后再递减,这就是密钥值为-1的原因。 And the key will be created without a TTL, so issuing a TTL key_name will also issue -1. 并且将在没有TTL的情况下创建密钥,因此发出TTL key_name也会发出-1。

The solution for that might be to wrap all that code inside a transaction block using MULTI and EXEC commands. 解决方案可能是使用MULTI和EXEC命令将所有代码包装在事务块中 However, that might be slow because it requires multiple round-trips to the Redis server. 但是,这可能很慢,因为它需要多次往返Redis服务器。

The solution I've used was to write a Lua script and run it using the EVAL command. 我使用的解决方案是编写一个Lua脚本并使用EVAL命令运行它。 It has the advantage of being atomical (which means no concurrency issues) and has only one RTT to the Redis server. 它具有原子性的优点(这意味着没有并发问题),并且到Redis服务器只有一个RTT。

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

To use it, we could rewrite the consume_rate_limit function to this: 要使用它,我们可以将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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM