redis键在-1处被TTL卡住了

时间:2017-02-20 16:59:45

标签: redis

我使用redis来管理API的速率限制,并使用SETEX让速率限制每小时自动重置一次。

我发现redis无法清除某些密钥并在TTL报告-1。以下是使用占位符IP地址进行演示的redis-cli会话示例:

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

请注意,尽管其TTL为负数,但当我GET时,redis不会清除此密钥。

我试图重现这种状态而不能。

> 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 个答案:

答案 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

好吧,我正在使用这种方法,发现" get"之间存在共存问题。和" decr"电话会导致你所描述的确切问题。

当速率限制键的TTL在" get"之后到期时,会出现问题。打电话,然后在" decr"呼叫。会发生什么:

首先"得到"通话将返回当前限制。让我们说它返回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