API Service 在操作某些行為時需要耗費資源,如果 Client 不如預期的大量呼叫,會造成服務受到嚴重的影響,所以需要針對用戶做 API 呼叫次數的限制;Redis 作為中心化的高效能記憶體資料庫,很適合拿來當作 Rate Limit 的儲存方案,以下分享三種常見的做法 static time window / sliding time window 與 token bucket
最近公司 API 服務被 Client 不預期的高頻存取,造成後端 DB 很大的負擔,開始評估各種 API Rate Limit 的方案,其中一個最常見的作法就是靠 Redis,但具體的方案其實有蠻多種,參考以下影片整理三種作法
-- valueKey timestampKey | limit intervalMS nowMS [amount]localvalueKey=KEYS[1]-- "limit:1:V"localtimestampKey=KEYS[2]-- "limit:1:T"locallimit=tonumber(ARGV[1])localintervalMS=tonumber(ARGV[2])localamount=math.max(tonumber(ARGV[3]),0)localforce=ARGV[4]=="true"locallastUpdateMSlocalprevTokens-- Use effects replication, not script replication;; this allows us to call 'TIME' which is non-deterministicredis.replicate_commands()localtime=redis.call('TIME')localnowMS=math.floor((time[1]*1000)+(time[2]/1000))localinitialTokens=redis.call('GET',valueKey)localinitialUpdateMS=falseifinitialTokens==falsethen-- If we found no record, we temporarily rewind the clock to refill-- via addTokens belowprevTokens=0lastUpdateMS=nowMS-intervalMSelseprevTokens=initialTokensinitialUpdateMS=redis.call('GET',timestampKey)if(initialUpdateMS==false)then-- this is a corruption-- 如果資料有問題,需要回推 lastUpdateMS 時間,也就是用現在時間回推殘存 Token 數量的回補時間lastUpdateMS=nowMS-((prevTokens/limit)*intervalMS)elselastUpdateMS=initialUpdateMSendend
localaddTokens=math.max(((nowMS-lastUpdateMS)/intervalMS)*limit,0)-- calculated token balance coming into this transactionlocalgrossTokens=math.min(prevTokens+addTokens,limit)-- token balance after trying this transactionlocalnetTokens=grossTokens-amount-- time to fill enough to retry this amountlocalretryDelta=0localrejected=falselocalforced=falseifnetTokens<0then-- we used more than we haveifforcethenforced=truenetTokens=0-- drain the swampelserejected=truenetTokens=grossTokens-- rejection doesn't eat tokensend-- == percentage of `intervalMS` required before you have `amount` tokensretryDelta=math.ceil(((amount-netTokens)/limit)*intervalMS)else-- polite transaction-- nextNet == pretend we did this again...localnextNet=netTokens-amountifnextNet<0then-- ...we would need to wait to repeat-- == percentage of `invervalMS` required before you would have `amount` tokens againretryDelta=math.ceil((math.abs(nextNet)/limit)*intervalMS)endend
如果成功操作 ( rejected == false ),則延長 key 的過期時間
1
2
3
4
5
6
7
8
9
10
ifrejected==falsethenredis.call('PSETEX',valueKey,intervalMS,netTokens)ifaddTokens>0orinitialUpdateMS==falsethen-- we filled some tokens, so update our timestampredis.call('PSETEX',timestampKey,intervalMS,nowMS)else-- we didn't fill any tokens, so just renew the timestamp so it survives with the valueredis.call('PEXPIRE',timestampKey,intervalMS)endend