漏斗限流详述
本文已收錄于專欄
??《Redis之大廠必備技能包》??
上千人點贊收藏的,全套Redis學習資料,大廠必備技能!
目錄
1、需求
2、常見的錯誤設計
3、漏斗限流
3.1 解決方案
3.2 Java代碼實現
3.3 結合Redis實現
4、總結
1、需求
限定用戶的某個行為在指定時間T內,只允許發生N次。假設T為1秒鐘,N為1000次。
2、常見的錯誤設計
程序員設計了一個在每分鐘內只允許訪問1000次的限流方案,如下圖01:00s-02:00s之間只允許訪問1000次,這種設計最大的問題在于,請求可能在01:59s-02:00s之間被請求1000次,02:00s-02:01s之間被請求了1000次,這種情況下01:59s-02:01s間隔0.02s之間被請求2000次,很顯然這種設計是錯誤的。
3、漏斗限流
3.1 解決方案
漏斗容量有限,當流水的的速度小于灌水的速度,漏斗就會水滿溢出,利用這個原理我們可以設計限流代碼!漏斗的剩余的空間就代表著當前行為(請求)可以持續進行的數量,漏斗的流水速率代表系統允許行為(請求)發生的最大頻率,通常安裝系統的處理能力權衡后進行設值。
3.2 Java代碼實現
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?????????請求次數*?@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)?{//?觸發漏斗流水this.makeSpace();if?(this.leftCapacity?>=?quota)?{leftCapacity?-=?quota;return?true;}return?false;}}}測試代碼:
計算機運行如下的代碼速度會非常的塊,我通過TimeUnit.SECONDS.sleep(2);模擬客戶端過一段時間后再請求。
設置漏斗容量為10,每毫秒允許0.002次請求(2 次/秒),每次請求數量為1;
測試結果:
3.3 結合Redis實現
我們采用hash結構,將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();}}測試結果:
與上面的java代碼結構一致
4、總結
上述說了兩種實現漏斗限流的方式,其實思想都是一樣的,但是這兩者都無法在分布式環境中使用,即便是在單機環境中也是不準確的,存在線程安全問題/原子性問題,因此我們一般使用Redis提供的限流模塊Redis-Cell來限流,Redis-Cell提供了原子的限流指令cl.throttle,這個留到后續在詳細說吧,我要睡覺去了!
總結
- 上一篇: Gdevops北京站归来
- 下一篇: Abnova ProteoScreen