我使用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
。
答案 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