信號量限流,高併發場景不得不說的祕密

更多關注小姐姐味道公衆號。java

限流能夠認爲是一種降級,通常是根據後臺的負載提早預估的一個閾值(也能夠動態調整)。超過了這個值,就要進行一些旁路處理。根據業務形態,會有直接拒絕、延遲處理、保持等待、部分穿透、默認返回等響應方式。web

concurrent包中的信號量,因爲使用簡單,易於理解,被普遍應用。可是,你要是直接用了網友們分享的簡單代碼而不通過認真測試,那能夠送你一部電影觀賞一下:《當故障來敲門》。spring

看下面簡單的代碼,acquire和release是一對同命鴛鴦,咱們把release貼心的放在了finally塊中,一切顯得很是和諧。
1)模擬的業務請求,耗時大約是100毫秒
2)acquire的參數5表明同一時間容許5個線程進行處理 3)每次執行完畢,輸出一下本次執行的具體耗時,加上等待時間tomcat

咱們啓動1000個線程去執行req方法。

SemaphoreLimiterBadChecker limiter = new SemaphoreLimiterBadChecker();
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < 1000; i++) {
    executor.submit(() -> {
        while (true) {
            System.out.println(limiter.req());
        }
    });
}
複製代碼

下面是執行結果。bash

能夠看到,雖然咱們的接口耗時只有100ms,實際的執行時間,卻長的多,並且並無出現fail的狀況。運行稍長一點時間,可以發現有大量的線程處於餓死的狀態。改成公平鎖並不能改善這一狀況。服務器

這就是故障。多線程

緣由就在於。web端(如tomcat)的資源也是有限的。當咱們的限流器產生了做用,而實際併發請求比處理能力高的時候,這種線程阻塞狀況就會逐級傳遞。服務器的響應可能會有如下過程:
1) 壓力普通,正常服務,耗時正常 。
2) 壓力上升,服務開始出現大面積超時,因爲使用不公平鎖競爭,偶爾會有正常耗時的需求。
3) 壓力繼續增大,服務器開始進入假死狀態,幾乎不能再接受新的請求。併發

表如今用戶端,既不能出現服務不能處理的提示,也沒法中斷請求,全部的請求都在轉圈。繼續加大tomcat的鏈接數和線程數,並不會起到多大的做用。分佈式


acquire改爲tryAcquire?依然不能解決問題。tryAcquire返回的是bool類型,失敗的時候依然可以往下執行,包括finally塊。有個毛用?高併發

if(!tryAcquire()){
    return TOO_MANY_REQUESTS;
}
複製代碼

上面多加了一個判斷,這個纔是正途。tryAcquire還能夠加超時參數,不至於立馬返回失敗,也不至於讓調用者無限等待,而是將成功的請求控制在一個合理的響應時間。

響應時間=超時時間+業務處理時間
複製代碼

具體作法,拿spring來講,你能夠在preHandle中獲取這個許可,而後在postHandle中釋放它;也可使用定時器以必定的頻率去重製信號量。

固然你也要區別對待。
一、像上面提到的web服務,能夠直接拒絕服務。快速響應纔是重要的
二、像一些秒殺、下單等,能夠經過排隊或者等待解決(部分的)
三、像消息消費等,若是沒有順序需求,我以爲,無限等待還多是個好的方式
四、對於大多數無關緊要的業務結果,使用一些默認值直接返回,效果會好的多。雖然是限流,但乾的是熔斷的活

使用者必定要注意區分。

End

很是讓人奇怪的是,java抽象了使用場景並非很高(相對)的CyclicBarrier,可是並無一個通用的限流方法。信號量雖然能夠模擬實現這個過程,但它不太友好,太容易出錯。限流仍是使用guava的組件進行控制比較好(非分佈式),咱們會在後面的文章來探討它。

更多:

沒有預熱,不叫高併發,叫併發高

這樣的高可用,我不要!

餘額,危險的操做,給996留點福報

JAVA多線程使用場景和注意事項簡版

相關文章
相關標籤/搜索