Use Redis API limiter

In business, many places use traffic limiter, in order to reduce their own server pressure; In order to prevent interface abuse, limit the number of requests and so on, today we try to use redis API limiter

  1. Considering the distribution, we remove the flow limiting in memory and introduce Redis.

  2. Considering scrolling window requirements, we cancel 1. Counter mode and use 2. Token bucket method.

1. The counter

With the DECR of Redis, the counter decreases by one for each request and then decreases to 0, and the subsequent access is rejected until the next round of counters is turned on. There is a problem with this design, that is, it cannot avoid that the visits of two counters can be accepted from the end of the previous round to the beginning of the next round.


     

    const Redis = require('ioredis');

    const redis = new Redis(redisConfig);

    const rateLimitKey = 'rateLimitKey', pexpire = 60000, limit = 100, amount = 1;

    const ttl = await redis.pttl(rateLimitKey)

    if (ttl < 0) {

    await redis.psetex(rateLimitKey, pexpire, limit - amount)

    return {

    limit,

    remain: limit - amount,

    rejected: false,

    retryDelta: 0,

    };

    } else {

    const remain = await redis.decrby(rateLimitKey, amount)

    return {

    limit,

    remain: remain > 0 ? remain : 0,

    rejected: remain >= 0,

    retryDelta: remain > 0 ? 0 : ttl

    }

    }

Copy the code

2. The token bucket

One side continues to consume tokens and the other side continues to flow into the bucket. It is a mistake to use other processes to manipulate incoming tokens into buckets, which is undoubtedly the biggest load as keys increase. Therefore, we considered to calculate the number of tokens that should flow in by recording the last request time and remaining tokens, but this requires several redis operations. Considering the competition conditions, we chose to use lua script to do this.

  • Math.max (((nowTimeStamp – lastTimeStamp)/pexpire) * limit, 0)


     

    const Redis = require('ioredis');

    const client = new Redis(redisConfig)

    client.defineCommand('rateLimit', {

    numberOfKeys: 2,

    lua: fs.readFileSync(path.join(__dirname, './rateLimit.lua'), {encoding: 'utf8'}),

    })

    const args = [`${Key}:V`, `${Key}:T`,Limit, Pexpire, amount];

    const [limit, remain, rejected, retryDelta] = await client.rateLimit(... args)

    return {

    limit,

    remain,

    rejected: Boolean(rejected),

    retryDelta,

    }

Copy the code

The lua code is as follows: ratelimit.lua


     

    Local valueKey = KEYS[1] -- Store the KEY of the counter

    Local timeStampKey = KEYS[2] -- The KEY that stores the last access timestamp

    Local limit = tonumber(ARGV[1]) -- number of accesses per unit of time

    Local pexpire = tonumber(ARGV[2]) -- the expiry date of the KEY is ms

    Local amount = tonumber(ARGV[3]) -- decreases each time

    redis.replicate_commands()

    local time = redis.call('TIME')

    local nowTimeStamp = math.floor((time[1] * 1000) + (time[2] / 1000))

    local nowValue

    Local lastValue = redis. Call ('GET', valueKey

    Local lastTimeStamp - last update time

    if lastValue == false then

    lastValue = 0

    lastTimeStamp = nowTimeStamp - pexpire

    else

    lastTimeStamp = redis.call('GET', timeStampKey)

    if(lastTimeStamp == false) then

    lastTimeStamp = nowTimeStamp - ((lastValue / limit) * pexpire)

    end

    end

    Local addValue = math.max(((nowTimeStamp - lastTimeStamp)/pexpire) * limit, 0) -- Count times added

    nowValue = math.min(lastValue + addValue, limit)

    local remain = nowValue - amount

    local rejected = false

    local retryDelta = 0

    if remain < 0 then

    remain = 0

    rejected = true

    retryDelta = math.ceil(((amount - nowValue) / limit) * pexpire)

    else

    if (remain - amount) < 0 then

    retryDelta = math.ceil((math.abs(remain - amount) / limit) * pexpire)

    end

    end

    if rejected == false then

    redis.call('PSETEX', valueKey, pexpire, remain)

    if addValue > 0 then

    redis.call('PSETEX', timeStampKey, pexpire, nowTimeStamp)

    else

    redis.call('PEXPIRE', timeStampKey, pexpire)

    end

    end

    return { limit, remain, rejected, retryDelta }

Copy the code