漏斗限流详述
本文已收錄于專欄
??《Redis之大廠必備技能包》??
上千人點贊收藏的,全套Redis學習資料,大廠必備技能!
目錄
1、需求
2、常見的錯誤設(shè)計
3、漏斗限流
3.1 解決方案
3.2 Java代碼實現(xiàn)
3.3 結(jié)合Redis實現(xiàn)
4、總結(jié)
1、需求
限定用戶的某個行為在指定時間T內(nèi),只允許發(fā)生N次。假設(shè)T為1秒鐘,N為1000次。
2、常見的錯誤設(shè)計
程序員設(shè)計了一個在每分鐘內(nèi)只允許訪問1000次的限流方案,如下圖01:00s-02:00s之間只允許訪問1000次,這種設(shè)計最大的問題在于,請求可能在01:59s-02:00s之間被請求1000次,02:00s-02:01s之間被請求了1000次,這種情況下01:59s-02:01s間隔0.02s之間被請求2000次,很顯然這種設(shè)計是錯誤的。
3、漏斗限流
3.1 解決方案
漏斗容量有限,當流水的的速度小于灌水的速度,漏斗就會水滿溢出,利用這個原理我們可以設(shè)計限流代碼!漏斗的剩余的空間就代表著當前行為(請求)可以持續(xù)進行的數(shù)量,漏斗的流水速率代表系統(tǒng)允許行為(請求)發(fā)生的最大頻率,通常安裝系統(tǒng)的處理能力權(quán)衡后進行設(shè)值。
3.2 Java代碼實現(xiàn)
package?com.lizba.redis.limit;import?java.util.Map; import?java.util.concurrent.ConcurrentHashMap;/***?<p>*??????漏斗限流*?</p>**?@Author:?Liziba*/ public?class?FunnelRateLimiter?{/**?map用于存儲多個漏斗?*/private?Map<String,?Funnel>?funnels?=?new?ConcurrentHashMap<>();/***?請求(行為)是否被允許**?@param?userId????????用戶id*?@param?actionKey?????行為key*?@param?capacity??????漏斗容量*?@param?leakingRate???剩余容量*?@param?quota?????????請求次數(shù)*?@return*/public?boolean?isActionAllowed(String?userId,?String?actionKey,?int?capacity,?float?leakingRate,?int?quota)?{String?key?=?String.format("%s:%s",?userId,?actionKey);Funnel?funnel?=?funnels.get(key);if?(funnel?==?null)?{funnel?=?new?Funnel(capacity,?leakingRate);funnels.put(key,?funnel);}return?funnel.waterLeaking(quota);}/***?漏斗類*/class?Funnel?{/**?漏斗容量?*/int?capacity;/**?漏斗流速,每毫秒允許的流速(請求)?*/float?leakingRate;/**?漏斗剩余空間?*/int?leftCapacity;/**?上次漏水時間?*/long?leakingTs;public?Funnel(int?capacity,?float?leakingRate)?{this.capacity?=?this.leftCapacity?=?capacity;this.leakingRate?=?leakingRate;leakingTs?=?System.currentTimeMillis();}/***?計算剩余空間*/void?makeSpace()?{long?nowTs?=?System.currentTimeMillis();long?intervalTs?=?nowTs?-?leakingTs;int?intervalCapacity?=?(int)?(intervalTs?*?leakingRate);//?int?溢出if?(intervalCapacity?<?0)?{this.leftCapacity?=?this.capacity;this.leakingTs?=?nowTs;return;}//?騰出空間?>=?1if?(intervalCapacity?<?1)?{return;}//?增加漏斗剩余容量this.leftCapacity?+=?intervalCapacity;this.leakingTs?=?nowTs;//?容量不允許超出漏斗容量if?(this.leftCapacity?>?this.capacity)?{this.leftCapacity?=?this.capacity;}}/***?漏斗流水**?@param?quota?????流水量*?@return*/boolean?waterLeaking(int?quota)?{//?觸發(fā)漏斗流水this.makeSpace();if?(this.leftCapacity?>=?quota)?{leftCapacity?-=?quota;return?true;}return?false;}}}測試代碼:
計算機運行如下的代碼速度會非常的塊,我通過TimeUnit.SECONDS.sleep(2);模擬客戶端過一段時間后再請求。
設(shè)置漏斗容量為10,每毫秒允許0.002次請求(2 次/秒),每次請求數(shù)量為1;
測試結(jié)果:
3.3 結(jié)合Redis實現(xiàn)
我們采用hash結(jié)構(gòu),將Funnel的屬性字段,放入hash中,并且在代碼中進行運算即可
package?com.lizba.redis.limit;import?redis.clients.jedis.Jedis;import?java.util.HashMap; import?java.util.Map;/***?<p>*??????redis?hash?漏斗限流*?</p>**?@Author:?Liziba*?@Date:?2021/9/7?23:46*/ public?class?FunnelRateLimiterByHash?{private?Jedis?client;public?FunnelRateLimiterByHash(Jedis?client)?{this.client?=?client;}/***?請求是否成功**?@param?userId*?@param?actionKey*?@param?capacity*?@param?leakingRate*?@param?quota*?@return*/public?boolean?isActionAllowed(String?userId,?String?actionKey,?int?capacity,?float?leakingRate,?int?quota)?{String?key?=?this.key(userId,?actionKey);long?nowTs?=?System.currentTimeMillis();Map<String,?String>?funnelMap?=?client.hgetAll(key);if?(funnelMap?==?null?||?funnelMap.isEmpty())?{return?initFunnel(key,?nowTs,?capacity,?quota);}long?intervalTs?=?nowTs?-?Long.parseLong(funnelMap.get("leakingTs"));int?intervalCapacity?=?(int)?(intervalTs?*?leakingRate);//?時間過長,?int可能溢出if?(intervalCapacity?<?0)?{intervalCapacity?=?0;initFunnel(key,?nowTs,?capacity,?quota);}//?騰出空間必須?>=?1if?(intervalCapacity?<?1)?{intervalCapacity?=?0;}int?leftCapacity?=?Integer.parseInt(funnelMap.get("leftCapacity"))?+?intervalCapacity;if?(leftCapacity?>?capacity)?{leftCapacity?=?capacity;}return?initFunnel(key,?nowTs,?leftCapacity,?quota);}/***?存入redis,初始funnel**?@param?key*?@param?nowTs*?@param?capacity*?@param?quota*?@return*/private?boolean?initFunnel(String?key,long?nowTs,?int?capacity,?int?quota)?{Map<String,?String>?funnelMap?=?new?HashMap<>();funnelMap.put("leftCapacity",?String.valueOf((capacity?>?quota)???(capacity?-?quota)?:?0));funnelMap.put("leakingTs",?String.valueOf(nowTs));client.hset(key,?funnelMap);return?capacity?>=?quota;}/***?限流key**?@param?userId*?@param?actionKey*?@return*/private?String?key(String?userId,?String?actionKey)?{return?String.format("limit:%s:%s",?userId,?actionKey);}}測試代碼:
package?com.lizba.redis.limit;import?redis.clients.jedis.Jedis;import?java.util.concurrent.TimeUnit;/***?@Author:?Liziba*/ public?class?TestFunnelRateLimiterByHash?{public?static?void?main(String[]?args)?throws?InterruptedException?{Jedis?jedis?=?new?Jedis("192.168.211.108",?6379);FunnelRateLimiterByHash?limiter?=?new?FunnelRateLimiterByHash(jedis);for?(int?i?=?1;?i?<=?20;?i++)?{if?(i?==?15)?{TimeUnit.SECONDS.sleep(2);}boolean?success?=?limiter.isActionAllowed("liziba",?"view",?10,?0.002f,?1);System.out.println("第"?+?i?+?"請求"?+?(success???"成功"?:?"失敗"));}jedis.close();}}測試結(jié)果:
與上面的java代碼結(jié)構(gòu)一致
4、總結(jié)
上述說了兩種實現(xiàn)漏斗限流的方式,其實思想都是一樣的,但是這兩者都無法在分布式環(huán)境中使用,即便是在單機環(huán)境中也是不準確的,存在線程安全問題/原子性問題,因此我們一般使用Redis提供的限流模塊Redis-Cell來限流,Redis-Cell提供了原子的限流指令cl.throttle,這個留到后續(xù)在詳細說吧,我要睡覺去了!
總結(jié)
- 上一篇: Gdevops北京站归来
- 下一篇: Abnova ProteoScreen