NOTE

2.2 Redis RateLimiter

A Redis-based rate limiter using a counter algorithm and Lua scripts.

Redis / CacheCreated Updated 1 min readhistorical

This is a historical learning note and may contain outdated or incomplete understanding.

1. What It Is

  • Rate limiting implemented based on Redis.

2. Counter Algorithm

2.1. Interface

2.1.1. Increment Access Count by One

  • Parameter: key
  • Returns:
    • current access count;
    • remaining time before the access count resets.

2.1.2. Get the Current Access Count

  • Parameter: key
  • Returns:
    • current access count;
    • remaining time before the access count resets.

2.2. Logic

  • Increment access count by one

    local freqKey = KEYS[1]
    local freqTTL = tonumber(ARGV[1])
    
    local function isFreqKeyExists()
      return redis.call("EXISTS", freqKey)
    end
    
    local function incrFreqCount()
        return redis.call("INCR", freqKey)
    end
    
    local function getFreqTTL()
        return redis.call("TTL", freqKey)
    end
    
    local function initFreqKey()
        return redis.call("SET", freqKey, 1, "EX", freqTTL)
    end
    
    local function main()
        if isFreqKeyExists() == 1 then
          local freqCount = incrFreqCount()
          local freqTTL = getFreqTTL()
          return {freqCount, freqTTL}
        else
          initFreqKey()
          return {1, freqTTL}
        end
    end
    
    return main()
  • Get the current access count

    local freqKey = KEYS[1]
    
    local function getFreqCount()
        return redis.call("GET", freqKey)
    end
    
    local function getFreqTTL()
        return redis.call("TTL", freqKey)
    end
    
    local function main()
        local freqCount = getFreqCount()
        if not freqCount then
          return {0, 0}
        else 
          local freqTTL = getFreqTTL()
          return {freqCount, freqTTL}
        end
    end
    
    return main()

2.2.1. Why Wrap It in a Lua Script?

  • Package multiple commands together to avoid multiple round trips to the server and improve efficiency.
    • Of course, pipeline can also implement this.
  • Prevent concurrency problems.
    • For example, the following scenario would confuse the business side: the access count is clearly 0, so why is the next reset time 60 seconds?
    1. Get current access count: get returns an access count of 0.
    2. Increment access count: incr makes the count 1.
    3. Increment access count: expire sets the timeout to 60 seconds.
    4. Get current access count: pttl returns 60 seconds.
-- appid_business-key_current-time-under-the-current-time-type
local freqKey = KEYS[1]
-- Logical expiration timestamp: 1651420800
local freqExpireTs = tonumber(ARGV[1])
-- Actual expiration time: ttl + 7 days: ttl + 604800
local freqTTL = tonumber(ARGV[2])
-- Current timestamp
local currentTimeTs = tonumber(ARGV[3])

local function isFreqKeyExists(freqKey)
  local isExists = redis.call("EXISTS", freqKey)
  return isExists == 1
end

local function getFreqExpireTs()
  return tonumber(redis.call("HGET", freqKey, "expire"))
end

local function incrFreqCount()
    return redis.call("HINCRBY", freqKey, "count", 1)
end

local function initFreqKey(freqKey, freqExpireTs, freqTTL)
    redis.call("HMSET", freqKey, "count", 1, "expire", freqExpireTs)
    redis.call("EXPIRE", freqKey, freqTTL)
end

local function isFreqExpired(freqExpireTs, currentTimeTs)
    return currentTimeTs >= freqExpireTs
end

local function main()
    if isFreqKeyExists(freqKey) then
        local freqExpireTs = getFreqExpireTs(freqKey)
        if isFreqExpired(freqExpireTs, currentTimeTs)==false then
            local freqCount = incrFreqCount()
            return {freqCount, freqExpireTs}
        end
    end
    initFreqKey(freqKey, freqExpireTs, freqTTL)
    return {1, freqExpireTs}
end

return main()
local freqKey = KEYS[1]
local currentTimeTs = tonumber(ARGV[1])

local function isFreqKeyExists(freqKey)
  local isExists = redis.call("EXISTS", freqKey)
  return isExists == 1
end

local function getFreq(freqKey)
    local freq = redis.call("HMGET", freqKey, "count", "expire")
    return tonumber(freq[1]), tonumber(freq[2])
end

local function isFreqAlived(freqExpireTs, currentTimeTs)
    return currentTimeTs < freqExpireTs
end

local function main()
    if isFreqKeyExists(freqKey) then
        local freqCount, freqExpireTs = getFreq(freqKey)
        if isFreqAlived(freqExpireTs, currentTimeTs) then
            return {freqCount, freqExpireTs}
        end
    end
    return {0, currentTimeTs}
end

return main()

3. References

Discussion

Sign in with GitHub to comment. Discussions are stored as GitHub Issues.View on GitHub