Redis:使用 Redis 实现分布式限流


#Redis#


官方方案

https://redis.io/commands/incr 给了3种限流的伪代码实现,使用场景是限制每个ip每秒的请求数量。

方案1

FUNCTION LIMIT_API_CALL(ip)
ts = CURRENT_UNIX_TIME()
keyname = ip+":"+ts  // ip + 秒级时间戳
current = GET(keyname)
IF current != NULL AND current > 10 THEN
    ERROR "too many requests per second"
ELSE
    MULTI
        INCR(keyname,1)
        EXPIRE(keyname,10)  // 10 秒后过期
    EXEC
    PERFORM_API_CALL()
END

要点:

  • 每秒一个 key,10秒后过期。请求时,在当前时间对应的 key 上 incr。
  • 在并发情况下,不能严格限制1秒的请求量。

为什么 incr 和 expire 放在 redis 事务中?

为了保证原子性。否则,incr后还没执行 expire,且之后也没有对应 keyname 的请求过来,那么 key 就永远不失效了。

方案2

FUNCTION LIMIT_API_CALL(ip):
current = GET(ip)
IF current != NULL AND current > 10 THEN
    ERROR "too many requests per second"
ELSE
    value = INCR(ip)
    IF value == 1 THEN
        EXPIRE(ip,1)
    END
    PERFORM_API_CALL()
END

要点:始终用一个 key,让 key 在1秒后过期。问题:

  • incr 和 expire 不在一个 redis 事务中,若 expire 未执行到,则 key 会一直累加,然后出翔了。
  • 假设 expire 正常执行了,在并发的情况下,依然可能超出限流值。

对于 incr 和 expire 不在一个事务中,可以用 eval 指令执行 lua 脚本改造为原子操作:

local current
current = redis.call("incr",KEYS[1])
if tonumber(current) == 1 then
    redis.call("expire",KEYS[1],1)
end

方案3

FUNCTION LIMIT_API_CALL(ip)
current = LLEN(ip)
IF current > 10 THEN
    ERROR "too many requests per second"
ELSE
    IF EXISTS(ip) == FALSE
        MULTI
            RPUSH(ip,ip)
            EXPIRE(ip,1)
        EXEC
    ELSE
        RPUSHX(ip,ip)
    END
    PERFORM_API_CALL()
END

要点:value 使用 list。问题:在并发的情况下,可能超出限流值。

精准限流 - 方案1

执行 lua 脚本,返回累加后的值。 示例:

127.0.0.1:6379> EVAL "local r = redis.call('INCR', KEYS[1]) redis.call('EXPIRE', KEYS[1], ARGV[1]) return r" 1 key-name 100
(integer) 1
127.0.0.1:6379> ttl key-name
(integer) 91
127.0.0.1:6379> get key-name
"1"
127.0.0.1:6379>

实际业务中,key-name 由业务标识 + 秒级时间戳组成。

精准限流 - 方案2

每一次请求过来时,依次执行 set ex nx 、 incr 。

set key-name 0 EX 120 NX  // 值不存在时设置为0,并设置超时
result = INCR key-name    // 加1,并返回加1后的值

实际业务中,key-name 由业务标识 + 秒级时间戳组成。


( 本文完 )