说明
redis常被用作限流处理,这里主要谈一个之前遇到的遇到过的问题及其解决(顺便会提供一个批量化的版本)
问题版本
假设检查的是一个key,要求是在规定时间x内的访问不超过y次(x>0, y>0):
- GET key 若为空表示规定时间内0次访问,此次通过,并且执行 SET key 1 EX x后就可以返回0了,客户端再进行比较x与0
- 若不为空,调用INCR key(此处的一个要点是INCR并不会去除掉key的expire信息),返回GET key或者INCR执行的返回减1即可
下面是不那么伪的伪代码
1
2
3
4
5
6
7def freq_controled(key: str, time_gap: int, limit: int):
visit_amount = redis_client.get(key) # 假设key存在时返回的是int类型吧
if visit_amount is None: # 没有此key
redis_client.set(key, 1, EX, limit)
return False
redis_client.incr(key, 1)
return visit_amount > limit问题解释
- 核心bug出现在redis_client.get(key)的时候key存在,但是当运行redis server执行redis_client.incr(key, 1)的时候,key过期从而变得不存在了,此时再执行redis_client.incr(key, 1)就会生成一个没有expire信息、永不过期的key
问题解决
- 通过检测redis_client.incr(key, 1)的返回值、如果是1则表示触发了bug情景(因为正常情况下此返回应大于等于2才对),此时需要再执行一次redis_client.set(key, 2, EX, limit) # 保险起见设为2吧(万一触发这个set为1的是另一个client呢)
1
2
3
4
5
6
7
8
9
10def freq_controled(key: str, time_gap: int, limit: int):
visit_amount = redis_client.get(key) # 假设返回的是int类型吧
if not resp_int:
redis_client.set(key, 1, EX, limit)
return False
incr_resp = redis_client.incr(key, 1) # 还是假设返回的是int类型吧
if incr_resp == 1:
redis_client.set(key, 2, EX, limit)
return False
return visit_amount > limit
优化
- 上面的问题在于最坏IO次数太多了,最坏情况下需要3次IO,同理优化还是找老朋友Lua脚本进行(这样无论好坏都1次IO搞定)
1
2
3
4
5
6
7
8
9
10
11
12
13local get_result = redis.call("GET", KEYS[1])
if not get_result then
redis.call("SET", KEYS[1], 1, "EX", ARGV[1])
return 0
else
local incr_result = redis.call("INCR", KEYS[1])
if incr_result == 1 then
redis.call("SET", KEYS[1], 2, "EX", ARGV[1])
return 1
else
return incr_result - 1
end
end
批处理
- 上面的问题在于每次只能提供一个key,那有场景情况下一下就拿到了n个key需要验证,IO次数就是n次了,也不划算,下面提供一个多key作为输入的版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21local function handle_result(key, expire)
local redis_resp = redis.call("GET", key)
if not redis_resp then
redis.call("SET", key, 1, "EX", expire)
return 0
else
local incr_result = redis.call("INCR", key)
if incr_result == 1 then
redis.call("SET", key, 2, "EX", tonumber(expire))
return 1
else
return incr_result - 1
end
end
end
local final_result = {}
for key_index, key in ipairs(KEYS) do
local func_ret = handle_result(key, ARGV[key_index])
table.insert(final_result, func_ret)
end
return final_result