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
-
Considering the distribution, we remove the flow limiting in memory and introduce Redis.
-
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