- 業務背景介紹
對於web應用的限流,光看標題,彷佛過於抽象,難以理解,那咱們仍是以具體的某一個應用場景來引入這個話題吧。
在平常生活中,咱們確定收到過很多很多這樣的短信,「雙11約嗎?,千款….」,「您有幸得到唱讀卡,趕快戳連接…」。這種類型的短信是屬於推廣性質的短信。爲何我要說這個呢?聽我慢慢道來。
通常而言,對於推廣營銷類短信,它們針對某一羣體(譬如註冊會員)進行定點推送,有時這個羣體的成員量比較大,譬如京東的會員,能夠達到千萬級別。所以相應的,發送推廣短信的量也會增大。然而,要完成這些短信發送,咱們是須要調用服務商的接口來完成的。假若一次發送的量在200萬條,而咱們的服務商接口每秒能處理的短信發送量有限,只能達到200條每秒。那麼這個時候就會產生問題了,咱們如何能控制好程序發送短信時的速度暱?因而限流這個功能就得加上了 - 生產環境背景
一、服務商接口所能提供的服務上限是400條/s
二、業務方調用短信發送接口的速度未知,QPS可能達到800/s,1200/s,或者更高
三、當服務商接口訪問頻率超過400/s時,超過的量將拒絕服務,多出的信息將會丟失
四、線上爲多節點佈置,但調用的是同一個服務商接口 - 需求分析
一、鑑於業務方對短信發送接口的調用頻率未知,而服務商的接口服務有上限,爲保證服務的可用性,業務層須要對接口調用方的流量進行限制—–接口限流 - 需求設計
方案1、在提供給業務方的Controller層進行控制。
一、使用guava提供工具庫裏的RateLimiter類(內部採用令牌捅算法實現)進行限流
<!--核心代碼片斷--> private RateLimiter rateLimiter = RateLimiter.create(400);//400表示每秒容許處理的量是400 if(rateLimiter.tryAcquire()) { //短信發送邏輯能夠在此處 }
- 1
- 2
- 3
- 4
- 5
- 6
二、使用Java自帶delayqueue的延遲隊列實現(編碼過程相對麻煩,此處省略代碼)java
三、使用redis實現,存儲兩個key,一個用於計時,一個用於計數。請求每調用一次,計數器增長1,若在計時器時間內計數器未超過閾值,則能夠處理任務web
if(!cacheDao.hasKey(API_WEB_TIME_KEY)) { cacheDao.putToValue(API_WEB_TIME_KEY,0,(long)1, TimeUnit.SECONDS); } if(cacheDao.hasKey(API_WEB_TIME_KEY)&&cacheDao.incrBy(API_WEB_COUNTER_KEY,(long)1) > (long)400) { LOGGER.info("調用頻率過快"); } //短信發送邏輯
- 1
- 2
- 3
- 4
- 5
- 6
方案2、在短信發送至服務商時作限流處理
方案3、同時使用方案一和方案二redis
-
可行性分析
最快捷且有效的方式是使用RateLimiter實現,可是這很容易踩到一個坑,單節點模式下,使用RateLimiter進行限流一點問題都沒有。可是…線上是分佈式系統,佈署了多個節點,並且多個節點最終調用的是同一個短信服務商接口。雖然咱們對單個節點能作到將QPS限制在400/s,可是多節點條件下,若是每一個節點均是400/s,那麼到服務商那邊的總請求就是節點數x400/s,因而限流效果失效。使用該方案對單節點的閾值控制是難以適應分佈式環境的,至少目前我還沒想到更爲合適的方式。
對於第二種,使用delayqueue方式。其實主要存在兩個問題,1:短信系統自己就用了一層消息隊列,有用kafka,或者rabitmq,若是再加一層延遲隊列,從設計上來講是不太合適的。2:實現delayqueue的過程相對較麻煩,耗時可能比較長,並且達不到精準限流的效果
對於第三種,使用redis進行限流,其很好地解決了分佈式環境下多實例所致使的併發問題。由於使用redis設置的計時器和計數器均是全局惟一的,無論多少個節點,它們使用的都是一樣的計時器和計數器,所以能夠作到很是精準的流控。同時,這種方案編碼並不複雜,可能須要的代碼不超過10行。算法 -
實施方案
根據可行性分析可知,整個系統採起redis限流處理是成本最低且最高效的。
具體實現markdown一、在Controller層設置兩個全局key,一個用於計數,另外一個用於計時併發
private static final String API_WEB_TIME_KEY = "time_key"; private static final String API_WEB_COUNTER_KEY = "counter_key";
- 1
- 2
- 3
二、對時間key的存在與否進行判斷,並對計數器是否超過閾值進行判斷分佈式
if(!cacheDao.hasKey(API_WEB_TIME_KEY)) { cacheDao.putToValue(API_WEB_TIME_KEY,0,(long)1, TimeUnit.SECONDS); cacheDao.putToValue(API_WEB_COUNTER_KEY,0,(long)2, TimeUnit.SECONDS);//時間到就從新初始化爲 } if(cacheDao.hasKey(API_WEB_TIME_KEY)&&cacheDao.incrBy(API_WEB_COUNTER_KEY,(long)1) > (long)400) { LOGGER.info("調用頻率過快"); } //短信發送邏輯
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
實施結果
能夠達到很是精準的流控,截圖會在後續的過程當中貼出來。歡迎有疑問的小夥伴們在評論區提出問題,我看到後儘可能抽時間回答的工具