Redis 密钥的 TTL 被困在 -1

Art*_*pek 5 redis

我使用 Redis 来管理 API 上的速率限制,并用于SETEX每小时自动重置速率限制。

我发现 Redis 无法清除某些密钥并报告其TTLat -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
Run Code Online (Sandbox Code Playgroud)

请注意,尽管其 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)
Run Code Online (Sandbox Code Playgroud)

这是一些不幸的竞争条件导致 Redis 无法使这些密钥过期吗?在数万名已成功过期的人中,只有约10人仍滞留在该-1州。

我在用redis_version:2.8.9

And*_*ske 2

我遇到了同样的问题,仅使用 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
Run Code Online (Sandbox Code Playgroud)

好吧,我正在使用这种方法,发现“get”和“decr”调用之间存在并发问题,这导致了您所描述的确切问题。

当速率限制密钥的 TTL 在“get”调用之后但在“decr”调用之前到期时,就会出现此问题。会发生什么:

首先,“get”调用将返回当前限制。假设它返回 500。然后在几毫秒内,该密钥的 TTL 到期,因此它在 Redis 中不再存在。因此代码继续运行并到达“decr”调用。该错误也在这里出现:

decr文档指出(我的重点):

将键中存储的数字减一。如果该键不存在,则在执行操作之前将其设置为0。(...)

由于密钥已被删除(因为已过期),因此“decr”指令会将密钥初始化为零,然后将其递减,这就是密钥值为-1的原因。并且密钥将在没有 TTL 的情况下创建,因此发出 aTTL key_name也会发出 -1。

解决方案可能是使用 MULTI 和 EXEC 命令将所有代码包装在事务块内。但是,这可能会很慢,因为它需要多次往返 Redis 服务器。

我使用的解决方案是编写一个 Lua 脚本并使用 EVAL 命令运行它。它的优点是原子性(这意味着没有并发问题),并且到 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
Run Code Online (Sandbox Code Playgroud)

要使用它,我们可以将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
Run Code Online (Sandbox Code Playgroud)