Redis 速率限制器模式

Redis Rate Limiter Pattern

我正在尝试使用“模式:速率限制器 1”下 https://redis.io/commands/incr 中指定的 Redis 速率限制模式。但是如果我想跨多个服务器进行速率限制,我该如何扩展它。就像我在负载均衡器后面的 5 台服务器上部署了服务,我希望 5 台服务器上每个 api 键的总请求不应超过 x/sec。根据我提到的 redis 模式,问题是如果我在多个服务器本身有我的速率限制器 运行,那么对两个不同速率限制器服务器的两个不同请求,可以同时“获取密钥”并读取相同的值,在任何人更新它之前,这可能允许更多请求 go.How 我可以处理这个吗?我显然可以把 get 放在 MULTI 块中,但我认为这会使事情变得更慢。

您需要 运行 LUA 脚本来检查速率限制和 increase/decrease/reset 计数器。

您可以在此处找到 Larval 框架中的一个简单示例

https://github.com/laravel/framework/blob/8.x/src/Illuminate/Redis/Limiters/DurationLimiter.php

 /**
     * Get the Lua script for acquiring a lock.
     *
     * KEYS[1] - The limiter name
     * ARGV[1] - Current time in microseconds
     * ARGV[2] - Current time in seconds
     * ARGV[3] - Duration of the bucket
     * ARGV[4] - Allowed number of tasks
     *
     * @return string
     */
    protected function luaScript()
    {
        return <<<'LUA'
local function reset()
    redis.call('HMSET', KEYS[1], 'start', ARGV[2], 'end', ARGV[2] + ARGV[3], 'count', 1)
    return redis.call('EXPIRE', KEYS[1], ARGV[3] * 2)
end
if redis.call('EXISTS', KEYS[1]) == 0 then
    return {reset(), ARGV[2] + ARGV[3], ARGV[4] - 1}
end
if ARGV[1] >= redis.call('HGET', KEYS[1], 'start') and ARGV[1] <= redis.call('HGET', KEYS[1], 'end') then
    return {
        tonumber(redis.call('HINCRBY', KEYS[1], 'count', 1)) <= tonumber(ARGV[4]),
        redis.call('HGET', KEYS[1], 'end'),
        ARGV[4] - redis.call('HGET', KEYS[1], 'count')
    }
end
return {reset(), ARGV[2] + ARGV[3], ARGV[4] - 1}
LUA;
    }

INCR 回复更新后的值。所以它既可以用作写命令也可以用作读命令。

FUNCTION LIMIT_API_CALL(ip)
ts = CURRENT_UNIX_TIME()
keyname = ip+":"+ts

MULTI
    INCR(keyname)
    EXPIRE(keyname,10)
EXEC

current = RESPONSE_OF_INCR_WITHIN_MULTI
IF current > 10 THEN
    ERROR "too many requests per second"
ELSE
    PERFORM_API_CALL()
END

接受的答案不正确。它会导致不准确的计数器值。让我们看下面的例子:

5 个客户端对 Redis 执行 5 个并发请求。计数器的当前状态是10,而limit也是10。

5 个并发请求会将计数器递增到 15,同时拒绝每个请求。相反,该值应保持为 10,以反映“允许”客户端的正确次数。

解决方法: 我们实际上需要将两个独立的原子操作组合成一个原子操作。这就是 LUA 脚本的用武之地。它只是对 Redis 本身的一种修改,以引入另一个代码路径,以原子方式“执行获取,然后执行设置”。这样做是因为 Redis 是单线程的。