官方方案
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 由业务标识 + 秒级时间戳组成。