一瞥之见

0%

记基于redis做频次限制遇到的bug

说明

redis常被用作限流处理,这里主要谈一个之前遇到的遇到过的问题及其解决(顺便会提供一个批量化的版本)

问题版本

  • 假设检查的是一个key,要求是在规定时间x内的访问不超过y次(x>0, y>0):

    1. GET key 若为空表示规定时间内0次访问,此次通过,并且执行 SET key 1 EX x后就可以返回0了,客户端再进行比较x与0
    2. 若不为空,调用INCR key(此处的一个要点是INCR并不会去除掉key的expire信息),返回GET key或者INCR执行的返回减1即可
  • 下面是不那么伪的伪代码

    1
    2
    3
    4
    5
    6
    7
    def 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
    10
    def 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
    13
    local 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
    21
    local 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