æ¬æ讲述åºäº Redisçéæµç³»ç»ç设计 ï¼ä¸»è¦ä¼è°åéæµç³»ç»ä¸ éæµçç¥ è¿ä¸ªåè½ç设计ï¼å¨å®ç°æ¹é¢ï¼ç®æ³ä½¿ç¨çæ¯ ä»¤ç桶 ç®æ³æ¥ï¼è®¿é®Redis使ç¨luaèæ¬ã
In computer networks, rate limiting is used to control the rate of traffic sent or received by a network interface controller and is used to prevent DoS attacks
ç¨æçç解翻è¯ä¸ä¸ï¼éæµæ¯å¯¹ç³»ç»ç åºå ¥æµé è¿è¡ æ§å¶ ï¼é²æ¢å¤§æµéåºå ¥ï¼å¯¼è´ èµæº ä¸è¶³ï¼ç³»ç»ä¸ç¨³å®ã
éæµç³»ç»æ¯å¯¹èµæºè®¿é®çæ§å¶ç»ä»¶ï¼æ§å¶ä¸»è¦ç两个åè½ï¼ éæµçç¥ å çæçç¥ ï¼å¯¹äºçæçç¥ï¼ä¸åçç³»ç»æä¸åççæçç¥è¯æ±ï¼æçç³»ç»å¸æç´æ¥æç»ãæçç³»ç»å¸ææéçå¾ ãæçç³»ç»å¸ææå¡é级ãæçç³»ç»ä¼å®å¶èªå·±ççæçç¥ï¼å¾é¾ä¸ä¸å举ï¼æ以æ¬æåªé对 éæµçç¥ è¿ä¸ªåè½å详ç»ç设计ã
é对 æ¾åºè¶ åºéçéå¼çè¯·æ± è¿ä¸ªåè½ï¼éæµç³»ç»ä¸æ两个åºç¡æ¦å¿µï¼èµæºåçç¥ã
èµæº ï¼æè å«ç¨ç¼ºèµæºï¼è¢«æµéæ§å¶ç对象ï¼æ¯å¦åæ¥å£ãå¤é¨åæ·æ¥å£ã大æµéä¸ç读æ¥å£ çç¥ ï¼éæµçç¥ç±éæµç®æ³åå¯è°èçåæ°ä¸¤é¨åç»æçæçç¥ï¼è¶ åºéçéå¼ç请æ±çå¤ççç¥ï¼æ¯æèªå·±ç解çä¸ä¸ªå«æ³ï¼ä¸æ¯ä¸ç主æµç说æ³ã
2ãéæµç®æ³
2.1ãéå¶ç¬æ¶å¹¶åæ°
å®ä¹ ï¼ç¬æ¶ 并åæ° ï¼ç³»ç»åæ¶å¤çç请æ±/äºå¡æ°é
ä¼ç¹ ï¼è¿ä¸ªç®æ³è½å¤å®ç°æ§å¶å¹¶åæ°çææ
ç¼ºç¹ ï¼ä½¿ç¨åºæ¯æ¯è¾åä¸ï¼ä¸è¬ç¨æ¥å¯¹å ¥æµéè¿è¡æ§å¶
java伪代ç å®ç° ï¼
AtomicInteger atomic = new AtomicInteger(1)
try {
if(atomic.incrementAndGet() > éæµæ°) {
//çæé»è¾
} else {
//å¤çé»è¾
} finally {
atomic.decrementAndGet();
2.2ãéå¶æ¶é´çªæ大请æ±æ°
å®ä¹ï¼æ¶é´çªæ大请æ±æ°ï¼æå®çæ¶é´èå´å
å
许çæ大请æ±æ°
ä¼ç¹ï¼è¿ä¸ªç®æ³è½å¤æ»¡è¶³ç»å¤§å¤æ°çæµæ§éæ±ï¼éè¿æ¶é´çªæ大请æ±æ°å¯ä»¥ç´æ¥æ¢ç®åºæ大çQPSï¼QPS = 请æ±æ°/æ¶é´çªï¼
缺ç¹ï¼è¿ç§æ¹å¼å¯è½ä¼åºç°æµéä¸å¹³æ»çæ
åµï¼æ¶é´çªå
ä¸å°æ®µæµéå æ¯ç¹å«å¤§
lua代ç å®ç°ï¼
--- èµæºå¯ä¸æ è¯
local key = KEYS[1]
--- æ¶é´çªæ大并åæ°
local max_window_concurrency = tonumber(ARGV[1])
--- æ¶é´çª
local window = tonumber(ARGV[2])
--- æ¶é´çªå
å½å并åæ°
local curr_window_concurrency = tonumber(redis.call('get', key) or 0)
if current + 1 > limit then
return false
redis.call("INCRBY", key,1)
if window > -1 then
redis.call("expire", key,window)
return true
2.3ã令ç桶
åå¦ç¨æ·é
ç½®çå¹³ååééç为rï¼åæ¯é1/rç§ä¸ä¸ªä»¤ç被å å
¥å°æ¡¶ä¸
å设桶ä¸æå¤å¯ä»¥åæ¾b个令çãå¦æ令çå°è¾¾æ¶ä»¤ç桶已ç»æ»¡äºï¼é£ä¹è¿ä¸ªä»¤çä¼è¢«ä¸¢å¼
å½æµé以éçvè¿å
¥ï¼ä»æ¡¶ä¸ä»¥éçvå令çï¼æ¿å°ä»¤ççæµééè¿ï¼æ¿ä¸å°ä»¤çæµéä¸éè¿ï¼æ§è¡çæé»è¾
é¿ææ¥çï¼ç¬¦åæµéçéçæ¯åå°ä»¤çæ·»å éççå½±åï¼è¢«ç¨³å®ä¸ºï¼r
å 为令ç桶æä¸å®çåå¨éï¼å¯ä»¥æµæ¡ä¸å®çæµéçªåæ
åµ
Mæ¯ä»¥åè/ç§ä¸ºåä½çæ大å¯è½ä¼ è¾éçã M>r
T max = b/(M-r) æ¿åæå¤§ä¼ è¾éççæ¶é´
B max = T max * M æ¿åæå¤§ä¼ è¾éççæ¶é´å
ä¼ è¾çæµé
ä¼ç¹ï¼æµéæ¯è¾å¹³æ»ï¼å¹¶ä¸å¯ä»¥æµæ¡ä¸å®çæµéçªåæ
åµ
å 为æ们éæµç³»ç»çå®ç°å°±æ¯åºäºä»¤ç桶è¿ä¸ªç®æ³ï¼å
·ä½ç代ç å®ç°åèä¸æã
3ãå·¥ç¨å®ç°
3.1ãææ¯éå
mysql:åå¨éæµçç¥çåæ°çå
æ°æ®
redis+lua:令ç桶ç®æ³å®ç°
说æï¼å 为æ们æredis å®ä½ä¸ºï¼ç¼åã计ç®åªä»ï¼æ以å
æ°æ®é½æ¯åå¨dbä¸
3.2ãæ¶æå¾
éæµç³»ç»çå®ç°æ¯åºäºredisçï¼æ¬å¯ä»¥ååºç¨æ å
³ï¼ä½æ¯ä¸ºäºåéæµå
æ°æ®é
ç½®çç»ä¸ç®¡çï¼byåºç¨ç»´åº¦ç®¡çå使ç¨ï¼å¨æ°æ®ç»æä¸å å
¥äºappsè¿ä¸ªå段ï¼åºç°é®é¢ï¼ææ¥èµ·æ¥ä¹æ¯è¾æ¹ä¾¿ã
3.4ã代ç å®ç°
3.4.1ã代ç å®ç°éå°çé®é¢
åè令ç桶çç®æ³æè¿°ï¼ä¸è¬æè·¯æ¯å¨RateLimiter-clientæ¾ä¸ä¸ªéå¤æ§è¡ç线ç¨ï¼çº¿ç¨æ ¹æ®é
ç½®å¾ä»¤ç桶éæ·»å 令çï¼è¿æ ·çå®ç°ç±å¦ä¸ç¼ºç¹ï¼
éè¦ä¸ºæ¯ä¸ªä»¤ç桶é
置添å ä¸ä¸ªéå¤æ§è¡ç线ç¨
éå¤çé´é精度ä¸å¤ç²¾ç¡®ï¼çº¿ç¨éè¦æ¯1/rç§å桶éæ·»å ä¸ä¸ªä»¤çï¼å½r >1000 æ¶é´çº¿ç¨æ§è¡çæ¶é´é´éæ ¹æ¬æ²¡åæ³è®¾ç½®ï¼ä»åé¢æ§è½æµè¯çåç°æ¥çRateLimiter-client æ¯å¯ä»¥æ¿æ
QPS > 5000 ç请æ±éçï¼
3.4.2ã解å³æ¹æ¡
åºäºä¸é¢ç缺ç¹ï¼åèäºgoogleçguavaä¸RateLimiterä¸çå®ç°ï¼æ们使ç¨äºè§¦åå¼æ·»å 令ççæ¹å¼ã
last_mill_second = ä¸ä¸æ¬¡æ·»å 令çç毫ç§æ°
r = æ·»å 令ççéç
reserve_permits = (curr_mill_second-last_mill_second)/1000 * r
æ·»å å®ä»¤çä¹ååæ§è¡å令çé»è¾
3.4.3ã lua代ç å®ç°
--- è·å令ç
--- è¿åç
--- 0 没æ令ç桶é
ç½®
--- -1 表示å令ç失败ï¼ä¹å°±æ¯æ¡¶é没æ令ç
--- 1 表示å令çæå
--- @param key 令çï¼èµæºï¼çå¯ä¸æ è¯
--- @param permits 请æ±ä»¤çæ°é
--- @param curr_mill_second å½å毫ç§æ°
--- @param context 使ç¨ä»¤ççåºç¨æ è¯
local function acquire(key, permits, curr_mill_second, context)
local rate_limit_info = redis.pcall("HMGET", key, "last_mill_second", "curr_permits", "max_permits", "rate", "apps")
local last_mill_second = rate_limit_info[1]
local curr_permits = tonumber(rate_limit_info[2])
local max_permits = tonumber(rate_limit_info[3])
local rate = rate_limit_info[4]
local apps = rate_limit_info[5]
--- æ è¯æ²¡æé
置令ç桶
if type(apps) == 'boolean' or apps == nil or not contains(apps, context) then
return 0
local local_curr_permits = max_permits;
--- 令ç桶ååå建ï¼ä¸ä¸æ¬¡è·å令çç毫ç§æ°ä¸ºç©º
--- æ ¹æ®åä¸ä¸æ¬¡å桶éæ·»å 令ççæ¶é´åå½åæ¶é´å·®ï¼è§¦åå¼å¾æ¡¶éæ·»å 令ç
--- 并ä¸æ´æ°ä¸ä¸æ¬¡å桶éæ·»å 令ççæ¶é´
--- å¦æå桶éæ·»å ç令çæ°ä¸è¶³ä¸ä¸ªï¼åä¸æ´æ°ä¸ä¸æ¬¡å桶éæ·»å 令ççæ¶é´
if (type(last_mill_second) ~= 'boolean' and last_mill_second ~= false and last_mill_second ~= nil) then
local reverse_permits = math.floor(((curr_mill_second - last_mill_second) / 1000) * rate)
local expect_curr_permits = reverse_permits + curr_permits;
local_curr_permits = math.min(expect_curr_permits, max_permits);
--- 大äº0表示ä¸æ¯ç¬¬ä¸æ¬¡è·å令çï¼ä¹æ²¡æå桶éæ·»å 令ç
if (reverse_permits > 0) then
redis.pcall("HSET", key, "last_mill_second", curr_mill_second)
redis.pcall("HSET", key, "last_mill_second", curr_mill_second)
local result = -1
if (local_curr_permits - permits >= 0) then
result = 1
redis.pcall("HSET", key, "curr_permits", local_curr_permits - permits)
redis.pcall("HSET", key, "curr_permits", local_curr_permits)
return result
å
³äºéæµç³»ç»çææå®ç°ç»èï¼æé½å·²ç»æ¾å°githubä¸ï¼gitbubå°åï¼https://github.com/wukq/rate-limiterï¼æå
´è¶£çåå¦å¯ä»¥åå¾æ¥çï¼ç±äºç¬è
ç»éªä¸ç¥è¯æéï¼ä»£ç ä¸å¦æé误æåé¢ï¼æ¬¢è¿æ¢è®¨åææ£ã
3.4.4ã管ççé¢
åé¢ç设计ä¸ï¼éæµçé
ç½®æ¯ååºç¨å
³èçï¼ä¸ºäºæ´å¤æ´å¥½ç管çé
ç½®ï¼éè¦ä¸ä¸ªç»ä¸ç管ç页é¢å»å¯¹é
ç½®è¿è¡ç®¡æ§ï¼
æåºç¨å¯¹éæµé
ç½®è¿è¡ç®¡ç
ä¸åç人åé
ä¸åçæéï¼ç¸å
³äººåææ¥çé
ç½®çæéï¼è´è´£äººæä¿®æ¹åå é¤é
ç½®çæé
å线ç¨å令çï¼Ratelimiter-clientç QPS = 250/s
10个线ç¨å令çï¼Ratelimiter-clientç QPS = 2000/s
100个线ç¨å令çï¼Ratelimiter-clientç QPS = 5000/s
éæµç³»ç»ä»è®¾è®¡å°å®ç°é½æ¯è¾ç®åï¼ä½æ¯ç¡®å®å¾å®ç¨ï¼ç¨å个åæ¥å½¢å®¹å°±æ¯ï¼çå°å¼ºæï¼å
¶ä¸æ¯è¾éè¦çæ¯ç»åå
¬å¸çæéä½ç³»åç³»ç»ç»æï¼è®¾è®¡åºç¬¦åèªå·±å
¬å¸è§èçéæµç³»ç»ã
redis æ们ç¨çæ¯åç¹redisï¼åªåäºä¸»ä»ï¼æ²¡æ使ç¨redisé«å¯ç¨é群ï¼å¯è½ä½¿ç¨redisé«å¯ç¨é群ï¼ä¼å¸¦æ¥æ°çé®é¢ï¼
éæµç³»ç»ç®ååªåäºåºç¨å±é¢çå®ç°ï¼æ²¡æåæ¥å£ç½å
³ä¸çå®ç°
çæçç¥éè¦èªå·±å®å¶ï¼å¦æå®ç°ç好ä¸ç¹ï¼å¯ä»¥ç»ä¸äºå¸¸ç¨ççæçç¥æ¨¡æ¿
æå鸣谢ä¸ä¸è´¡ç®èªå·±æ³æ³çâå人âï¼guava Ratelimiterãèèé«å¹¶åç³»ç»ä¹éæµç¹æ--å¼æ¶ãAPI è°ç¨æ¬¡æ°éå¶å®ç°--yigwoo
转载声æï¼æªç»ææä¸å¾è½¬è½½ï¼ææå转载请注æåºå¤å¹¶éä¸åæé¾æ¥ã
åè书ç±ï¼
1.ãRedis 设计ä¸å®ç°ã
2.ãLuaç¼ç¨æåã
åèæç« ï¼
https://en.wikipedia.org/wiki/Token_bucket
rediså®ç½