對於業務系統來講高併發就是支撐「海量用戶請求」,QPS 會是平時的幾百倍甚至更高。算法
若是不考慮高併發的狀況,即便業務系統平時運行得好好的,併發量一旦增長就會頻繁出現各類詭異的業務問題,好比,在電商業務中,可能會出現用戶訂單丟失、庫存扣減異常、超賣等問題。編程
限流是服務降級的一種手段,顧名思義,經過限制系統的流量,從而實現保護系統的目的。微信
合理的限流配置,須要瞭解系統的吞吐量,因此,限流通常須要結合容量規劃和壓測來進行。markdown
當外部請求接近或者達到系統的最大閾值時,觸發限流,採起其餘的手段進行降級,保護系統不被壓垮。常見的降級策略包括延遲處理、拒絕服務、隨機拒絕等。架構
限流後的策略,其實和 Java 併發編程中的線程池很是相似,咱們都知道,線程池在任務滿的狀況下,能夠配置不一樣的拒絕策略,好比:併發
AbortPolicy,會丟棄任務並拋出異常app
DiscardPolicy,丟棄任務,不拋出異常分佈式
DiscardOldestPolicy 等,固然也能夠本身實現拒絕策略ide
Java 的線程池是開發中一個小的功能點,可是見微知著,也能夠引伸到系統的設計和架構上,將知識進行合理地遷移複用。高併發
限流方案中有一點很是關鍵,那就是如何判斷當前的流量已經達到咱們設置的最大值,具體有不一樣的實現策略,下面進行簡單分析。
通常來講,咱們進行限流時使用的是單位時間內的請求數,也就是日常說的 QPS,統計 QPS 最直接的想法就是實現一個計數器。
計數器法是限流算法裏最簡單的一種算法,咱們假設一個接口限制 100 秒內的訪問次數不能超過 10000 次,維護一個計數器,每次有新的請求過來,計數器加 1。
這時候判斷,
下面的代碼裏使用 AtomicInteger
做爲計數器,能夠做爲參考:
public class CounterLimiter {
//初始時間
private static long startTime = System.currentTimeMillis();
//初始計數值
private static final AtomicInteger ZERO = new AtomicInteger(0);
//時間窗口限制
private static final int interval = 10000;
//限制經過請求
private static int limit = 100;
//請求計數
private AtomicInteger requestCount = ZERO;
//獲取限流
public boolean tryAcquire() {
long now = System.currentTimeMillis();
//在時間窗口內
if (now < startTime + interval) {
//判斷是否超過最大請求
if (requestCount.get() < limit) {
requestCount.incrementAndGet();
return true;
}
return false;
} else {
//超時重置
requestCount = ZERO;
startTime = now;
return true;
}
}
}
複製代碼
計數器策略進行限流,能夠從單點擴展到集羣,適合應用在分佈式環境中。
單點限流使用內存便可,若是擴展到集羣限流,能夠用一個單獨的存儲節點,好比 Redis 或者 Memcached 來進行存儲,在固定的時間間隔內設置過時時間,就能夠統計集羣流量,進行總體限流。
計數器策略有一個很大的缺點,對臨界流量不友好,限流不夠平滑。
假設這樣一個場景,咱們限制用戶一分鐘下單不超過 10 萬次,如今在兩個時間窗口的交匯點,先後一秒鐘內,分別發送 10 萬次請求。也就是說,窗口切換的這兩秒鐘內,系統接收了 20 萬下單請求,這個峯值可能會超過系統閾值,影響服務穩定性。
對計數器算法的優化,就是避免出現兩倍窗口限制的請求,可使用滑動窗口算法實現,感興趣的同窗能夠去了解一下。
漏桶算法和令牌桶算法,在實際應用中更加普遍,也常常被拿來對比。
漏桶算法能夠用漏桶來對比,假設如今有一個固定容量的桶,底部鑽一個小孔能夠漏水,咱們經過控制漏水的速度,來控制請求的處理,實現限流功能。
漏桶算法的拒絕策略很簡單:若是外部請求超出當前閾值,則會在水桶裏積蓄,一直到溢出,系統並不關心溢出的流量。
漏桶算法是從出口處限制請求速率,並不存在上面計數器法的臨界問題,請求曲線始終是平滑的。
它的一個核心問題是對請求的過濾太精準了,咱們常說「水至清則無魚」,其實在限流裏也是同樣的,咱們限制每秒下單 10 萬次,那 10 萬零 1 次請求呢?是否是必須拒絕掉呢?
大部分業務場景下這個答案是否認的,雖然限流了,但仍是但願系統容許必定的突發流量,這時候就須要令牌桶算法。
在令牌桶算法中,假設咱們有一個大小恆定的桶,這個桶的容量和設定的閾值有關,桶裏放着不少令牌,經過一個固定的速率,往裏邊放入令牌,若是桶滿了,就把令牌丟掉,最後桶中能夠保存的最大令牌數永遠不會超過桶的大小。當有請求進入時,就嘗試從桶裏取走一個令牌,若是桶裏是空的,那麼這個請求就會被拒絕。
不知道你有沒有使用過 Google 的 Guava 開源工具包?在 Guava 中有限流策略的工具類 RateLimiter,RateLimiter 基於令牌桶算法實現流量限制,使用很是方便。
RateLimiter 會按照必定的頻率往桶裏扔令牌,線程拿到令牌才能執行,RateLimter 的 API 能夠直接應用,主要方法是 acquire
和 tryAcquire
。
acquire
會阻塞,tryAcquire
方法則是非阻塞的。
下面是一個簡單的示例:
public class LimiterTest {
public static void main(String[] args) throws InterruptedException {
//容許10個,permitsPerSecond
RateLimiter limiter = RateLimiter.create(100);
for(int i=1;i<200;i++){
if (limiter.tryAcquire(1)){
System.out.println("第"+i+"次請求成功");
}else{
System.out.println("第"+i+"次請求拒絕");
}
}
}
}
複製代碼
計數器算法實現比較簡單,特別適合集羣狀況下使用,可是要考慮臨界狀況,能夠應用滑動窗口策略進行優化,固然也是要看具體的限流場景。
漏桶算法和令牌桶算法,漏桶算法提供了比較嚴格的限流,令牌桶算法在限流以外,容許必定程度的突發流量。在實際開發中,咱們並不須要這麼精準地對流量進行控制,因此令牌桶算法的應用更多一些。
若是咱們設置的流量峯值是 permitsPerSecond=N
,也就是每秒鐘的請求量,計數器算法會出現 2N 的流量,漏桶算法會始終限制 N 的流量,而令牌桶算法容許大於 N,但不會達到 2N 這麼高的峯值。
歡迎大佬們關注公衆號 勾勾的Java宇宙(微信號:Javagogo),拒絕水文,收穫乾貨!