继夫的玩弄H辣文的小说|女人与拘性猛交视频|精品欧美高清不卡高清|一起做亏亏的事情的视频|啦啦啦在线视频观看|望月直播下载ios版本|国产日韩欧美一区二区三区

新手程序員寫秒殺接口不懂如何限流!阿里P6:我來教你高并發限流

你可能知道高并發系統需要限流這個東西,但具體是限制的什么,該如何去做,還是臨摹兩可 。我們接下來系統性的給它歸個小類,希望對你有所幫助 。
guava中提供了一個限流實現: ,這個類設計的非常精巧,可以適用于我們日常業務中大多數流控的場景 , 但鑒于使用場景的多樣性,使用時也需要相當小心 。
本篇文章將詳細的,深入的介紹限流的各種場景和屬性,然后分析guava這個限流器的核心源碼,并對其特性進行總結 。屬于稍高級的進階篇 。
限流場景
弄清楚你要限制的資源 , 是這個過程中最重要的一環 。我大體將它分為三類 。
代理層
比如SLB、nginx或者業務層等,都支持限流 , 通常是基于連接數(或者并發數)、請求數進行限流 。限流的維度通常是基于比如IP地址、資源位置、用戶標志等 。更進一步,還可以根據自身負載情況動態調整限流的策略(基準) 。
服務調用者
服務調用方,也可以叫做本地限流,客戶端可以限制某個遠端服務的調用速度,超過閾值 , 可以直接進行阻塞或者拒絕,是限流的協作方 。
服務接收方
基本同上,流量超過系統承載能力時 , 會直接拒絕服務 。通常基于應用本身的可靠性考慮,屬于限流的主體方 。我們常說的限流,一般發生在此處 。本文主要結合討論基于限流主體方的使用方式 , 其他的都類似 。
限流策略
限流策略有時候很簡單,有時候又很復雜,但常見的就三種 。其他的都是在這之上進行的改進和擴展 。
根據并發級別限流
這是一種簡單的、易于實施的限流方式,可以使用我們前面提到的java信號量實現 。它的使用場景也有著比較鮮明的特點:
1)每次請求,所需要的資源開支都比較均衡 , 比如,每個請求對CPU的消耗、IO消耗等,都差不多 , 請求的RT時間都基本接近 。
2) 請求密度或稀疏或高頻,這個我們不去 。
3)資源本身不需要繁瑣的初始化工作(預熱),或者初始化工作的開支可以忽略 。(會增加復雜度)
4)對待流量溢出的策略比較簡單,通常是直接拒絕而不是等待,因為等待往往意味著故障 。
這種策略通常在適用在流量的頂層組件上java簡單實現一個阻塞隊列,比如代理層、中間件等對并發連接數的限制 。而嘗試獲取憑證的超時時間 , 就叫做溢出等待 。很上檔次很裝b的詞,對不對?
漏桶算法
請求流量以不確定速率申請資源,程序處理以恒定的速率進行,就是漏桶算法的基本原理 。有點像制作冰激凌的過程 。-.- 有關漏桶模型 , 大家可以去研究一下相關資料 。
大體有以下幾個概念 。

請求首先嘗試進入隊列,如果隊列溢滿,則拒絕此請求 。進入隊列以后,請求則等待執行 。
由此可見 , 請求究竟何時被執行,還存在一些變數,這直接取決于隊列中的請求數 。有時候,挑剔的設計者會考慮增加有關限制請求等待的時間閾值,這個時間就是請求入隊、出隊的最大時差 。的大小設計 , 通常與速率有直接關系 。
漏:請求出隊
這個出隊 , 有些講究,不同的設計理念實現就有所不同 。有搶占式、有調度式 。其中“搶占式”就是處理線程(或者進程,比如nginx 進程)在上一個請求處理完畢之后即從隊列中poll新的請求 , 無論當前線程(或者進程)的處理速率是否超過設定的速率,這種策略下大小就限定了速率的上限 。
調度式,就比較易于理解,需要額外的調度線程(進程) , 并嚴格按照設定的速率,從中獲取請求,并輪訓的方式將請求交給其他線程,如果已有的線程繁忙,則直接創建新線程,目的就是確保速率是有保障的,這種方式下 , 大小主要取決于等待時間 。
溢出
就是因為漏桶的速率限制比較穩定 , 所以其面臨流量突發()幾乎沒有應對能力 , 簡單來說,超出,就直接拒絕 。
多么可憐的請求們 。
流量突發
盡管的設計在一定層面上兼顧流量突發,但是還是太脆弱了,比如某個瞬間,請求密度很高(最尷尬的就是,只大了一點),將溢滿,或許再“大一點點”就能夠在合理時間內被處理;對于請求方,就會有些迷惑,“我只不過是稍微超了一點,你就給了我一連串無法工作的信息,so nave!!!” 。
這種策略 , 也很常用,但是通常適用在限流的協作方,也是就客戶端層面 。請求發出之前,做流控,如果有溢出,就要用其他可靠的策略來保障結果,比如重試等;反正 “對面的服務壓垮了,別怪我java簡單實現一個阻塞隊列,我很自律” 。
令牌桶
設計模型 , 我就不再介紹 , 大家可以去wiki深入了解一下 。
令牌桶的基本思想 , 跟老一輩的集體公社時代一樣,每個月的供銷是限額的,有資源才分配給個人,不足部分下個月再說,你可以排隊賒賬 。
令牌的個數,就是可以允許獲取資源的請求個數(我們假設每個請求只需要一個令牌) 。事實上,我們并不會真的去創建令牌實體,因為這是沒有必要的,我們使用帶有時間特征的計數器來表示令牌的可用個數即可 。跟漏桶算法相比,令牌桶的“桶”不是用來請求的、而是用來計量可用資源數量(令牌)的 。雖然我們并不會創建令牌實體,但是仍然可以假想,這個桶內每隔X時間就會新增一定數量的令牌,如果沒有請求申請令牌,那么這個令牌桶是會溢出的…你會發現,這個設計跟漏桶算法從IO方向上是相反的 。
那么漏桶算法的缺點,也正好成為了令牌桶的專長:流量突發;令牌桶成了,如果請求密度低,或者處于冷卻狀態,那么令牌桶就會溢滿,此后如果流量突發,則過去積累的結余資源則可以直接被“借用” 。
令牌桶算法,使用場景很多,適應程度很高,現實中流量突發是常見的,而且從設計角度考慮,令牌桶更易于實現 。回到正題,,就是一個基于令牌桶思想的實現 。
我們的口子越縮越小 , 終于到正題了 。
使用
guava的api已經把它的設計思想闡述的比較清楚了,但是這個注釋閱讀起來還是稍微有點“哲學派”,我們先看兩個栗子,然后從源碼層面看下它的設計原理 。
//RateLimiter limiter = RateLimiter.create(10,2, TimeUnit.SECONDS);//QPS 100 RateLimiter limiter = RateLimiter.create(10); long start = System.currentTimeMillis(); for (int i= 0; i0D) {System.out.println(i + ",limited,等待:" + time + ",已開始" + after + "毫秒");} else {System.out.println(i + ",enough" + ",已開始" + after + "毫秒");}//模擬冷卻時間 , 下一次loop可以認為是bursty開始if (i == 9) {Thread.sleep(2000);} } System.out.println("total time:" + (System.currentTimeMillis() - start));
此例為簡單的流控,只有一種資源 , QPS為10;在實際業務場景中 , 可能不同的資源速率是不同的 , 我們可以創建N多個各自服務于資源 。
()方法就是獲取一個令牌(源碼中使用,許可證),如果足夠,則直接返回而無需等待,如果不足,則等待1/QPS秒 。
此外 , 你會發現, 并沒有類似于鎖機制中的()方法,這意味著“只要申請 , 總會成功”、且退出時也無需歸還 。
內部有兩種實現:(下文中,“資源”、“令牌”、“”為同一含義)
可以支持“突發流量”的限流器,即當限流器不被使用時間 , 可以額外存儲一些以備突發流量,當突發流量發生時可以更快更充分的使用資源,流量平穩后(或者冷卻期 , 積累的被使用完之后)速率處于限制狀態 。
其重點就是,冷卻期間,會積累 , 且在突發流量時,可以消耗此前積累的而且無需任何等待 。就像一個人,奔跑之后休息一段時間 , 再次起步可以有更高的速度 。
由此可見,如果你的資源,冷卻(不被使用)一段時間之后,再次被使用時可以提供比正常更高的效率,這個時候,你可以使用 。
創建方式
RateLimiter.create(double permitsPerSecond)復制代碼
結果類似
0,enough,已開始1毫秒 1,limited,等待:0.098623,已開始105毫秒 2,limited,等待:0.093421 , 已開始202毫秒 3,limited,等待:0.098287,已開始304毫秒 4,limited,等待:0.096025,已開始401毫秒 5,limited,等待:0.098969,已開始505毫秒 6,limited,等待:0.094892,已開始605毫秒 7,limited,等待:0.094945 , 已開始701毫秒 8,limited,等待:0.099145,已開始801毫秒 9,limited,等待:0.09886,已開始905毫秒 10,enough , 已開始2908毫秒 11,enough,已開始2908毫秒 12,enough,已開始2908毫秒 13,enough,已開始2908毫秒 14,enough,已開始2908毫秒 15,enough,已開始2908毫秒 16,enough,已開始2908毫秒 17,enough , 已開始2908毫秒 18,enough,已開始2908毫秒 19,enough,已開始2908毫秒 20,enough , 已開始2909毫秒 21,limited,等待:0.099283,已開始3011毫秒 22,limited,等待:0.096308,已開始3108毫秒 23,limited,等待:0.099389 , 已開始3211毫秒 24,limited,等待:0.096674,已開始3313毫秒 25,limited,等待:0.094783,已開始3411毫秒 26,limited,等待:0.097161,已開始3508毫秒 27,limited,等待:0.099877,已開始3610毫秒 28,limited,等待:0.097551,已開始3713毫秒 29,limited,等待:0.094606,已開始3809毫秒 total time:3809
具有 up(預熱)特性,即突發流量發生時,不能立即達到最大速率,而是需要指定的“預熱時間”內逐步上升最終達到閾值;它的設計哲學,與相反,當突發流量發生時,以可控的慢速、逐步使用資源(直到最高速率),流量平穩后速率處于限制狀態 。
其重點是,資源一直被使用,那么它可以持續限制穩定的速率;否則,冷卻時間越長(有效時長為間隔)獲取時等待的時間越長,需要注意 , 冷卻時間會積累,但是獲取這些仍然需要等待 。
由此可見,如果你的資源,冷卻(不被使用)一段時間之后,再次被使用時它需要一定的準備工作 , 此時它所能提供的效率比正常要低;比如鏈接池、數據庫緩存等 。
創建方式
RateLimiter.create(double permitsPerSecond,long warnupPeriod,TimeUnit unit)復制代碼
執行結果如下,可以看到有一個明顯的增長過程 。
0,enough,已開始1毫秒 1,limited,等待:0.288847,已開始295毫秒 2,limited,等待:0.263403 , 已開始562毫秒 3,limited,等待:0.247548,已開始813毫秒 4,limited,等待:0.226932,已開始1041毫秒 5,limited,等待:0.208087,已開始1250毫秒 6,limited,等待:0.189501,已開始1444毫秒 7,limited,等待:0.165301,已開始1614毫秒 8,limited,等待:0.145779,已開始1761毫秒 9,limited,等待:0.128851,已開始1891毫秒 10,enough,已開始3895毫秒 11,limited,等待:0.289809,已開始4190毫秒 12,limited,等待:0.264528 , 已開始4458毫秒 13,limited,等待:0.247363,已開始4710毫秒 14,limited,等待:0.225157,已開始4939毫秒 15,limited,等待:0.206337 , 已開始5146毫秒 16,limited,等待:0.189213,已開始5337毫秒 17,limited,等待:0.167642,已開始5510毫秒 18,limited,等待:0.145383 , 已開始5660毫秒 19,limited,等待:0.125097,已開始5786毫秒 20,limited,等待:0.109232,已開始5898毫秒 21,limited,等待:0.096613 , 已開始5999毫秒 22,limited,等待:0.096321,已開始6098毫秒 23,limited,等待:0.097558,已開始6200毫秒 24,limited,等待:0.095132,已開始6299毫秒 25,limited,等待:0.095495 , 已開始6399毫秒 26,limited,等待:0.096352,已開始6496毫秒 27,limited,等待:0.098641,已開始6597毫秒 28,limited,等待:0.097883,已開始6697毫秒 29,limited,等待:0.09839,已開始6798毫秒 total time:6798 復制代碼
方法源碼分析
上面兩個類都繼承自,最終繼承自;內部核心的方法:
1) ():獲取一個,如果充足則直接返回,否則等待1/QPS秒 。此方法返回線程等待的時間(秒),如果返回0.0表示未限流、未等待 。
2) (int n):獲取n個 , 如果充足則直接返回,否則限流并等待,等待時間為“不足的個數 / QPS” 。(暫且這么解釋)
下面就是這個方法的偽代碼啦 。
//偽代碼 public double acquire(int requiredPermits) {long waitTime = 0L;synchronized(mutex) {boolean cold = nextTicketTime > now;if (cold) {storedPermits = 根據冷卻時長計算累積的permits;nextTicketTime = now;}//根據storedPermits、requiredPermits計算需要等待的時間//bursty:如果storePermits足夠 , 則waitTime = 0//warmup:平滑預熱,storePermits越多(即冷卻時間越長),等待時間越長if(storedPermits不足) {waitTime += 欠缺的permits個數 / QPS;}if(bursty限流) {waitTime += 0;//即無需額外等待}if(warmup限流) {waitTime += requiredPermits / QPS;if(storedPermits > 0.5 * maxPermits) {waitTime += 阻尼時間;}}nextTicketTime += waitTime}if (waitTime > 0L) {Thread.sleep(waitTime);}return waitTime; }
以下內容會比較枯燥~~~非常枯燥~~~
1、 mutex:同步鎖,如上述偽代碼所示 , 在計算存量、實際申請(包括計算)的過程中全部是同步的;我們需要知道,內部確實使用了鎖同步機制 。
2、:最大可存儲的許可數量(數量),和默認實現中 , 有所不同:
1),其值為 * QPS,就是允許“突發流量持續時間” * QPS,這種設計可以理解,不過將硬編碼為1.0 , 最終此值等于QPS 。
2):默認算法值為 * QPS,簡單說就是“預熱時長” * QPS 。
此參數主要限制,無論冷卻多長時間,其不能超過此值;此值在設定QPS之后,則不會再改變 。
3、:已存儲的數量 , 此值取決于冷卻時間 , 簡單來說冷卻的時間越久,此值越大,但不會超過 , 起始值為0 。
1)當一個請求,申請之前,將會計算上一次令牌申請()的時間與now之間的時差,并根據令牌產生速率(1/QPS)計算此冷卻期間可以存儲的令牌數,也就是 。
2)申請完畢之后,將當前時間(如果需要等待,額外加上等待時間)作為下一次令牌申請的起始時間,此時冷卻時間結束 。
3)申請完畢之后,將會減去申請的個數,直到為0 。
冷卻時長和申請頻次,都決定了大?。?渲欣淙詞奔浠岬賈略黽?nbsp;, 操作將導致減少 。
4、(時間戳,微妙單位):下一個可以自由獲取的令牌的時間,此值可以為未來的某個時間,此時表示限流已經開始,部分請求已經在等待 , 此值主要用來標記“冷卻狀態” 。(賒賬)
1)如果處于冷卻期,那么此值通常是過去式,即此值小于now 。
2)如果此時有請求申請,則會通過此值與now的時差 , 計算 , 同時將此值設置為now 。
3)如果此值是未來時刻,即大于now,則無需計算,也無需重置此值 。
4)申請后,從減去需要的個數,如果觸發限速等待(比如預熱期、不足),則會將2)操作之后額外增加等待時間作為值 。
5)基于2),對于限流,冷卻期之后的首個請求是不需要等待的 , 只是將此值設置為now + 阻尼性質的等待時間(),這意味著在此后期間再有請求,則會觸發等待,并繼續延續值 。此值的延續 , 在 up期間,阻尼計算比較復雜,由1/QPS + 額外值,這個額外值 , 隨著預熱時間增長而減小 。
6)基于2),對于限流,如果大于0,則總是不需要等待,只是簡單將此值設為為now;否則,則按照正常的1/QPS間隔計算其應該被推延的時間點 。
5、對于 up限流,將 * 0.5作為一個閾值分割線,當小于此分割線時,在限流時使用正常等待時間(申請個數 / QPS);在此分割線之上時 , 則4)增加額外阻尼 , 即預熱阻尼 。
6、我們發現,內部并不會真的生成實體,而是根據冷卻時長、在申請資源時才計算存量(對應為) 。無論何種限流,都是優先使用 。
小總結
是時候總結一下了 。
是線程安全的,所以在并發環境中可以直接使用,而無需額外的lock或者同步 。
考慮到內部的同步鎖,我們通常在實際業務開發中,每個資源(比如URL)使用各自的而不是公用一個 , 占用的內存也不大 。
這個限流器內部無額外的線程,也沒有其他的數據結構用來存儲實體,所以它非常的輕量級,這也是優勢所在 。
最大的問題,就是方法總會成功 , 內部的時間點會向后推移; 如果并發很高 , 嚴重超過rate閾值時 , 后續被限流的請求 , 其等待時間將會基于時間線累加 , 導致等待時間不可控,這和信號量同病相憐 。
為了避免上面的問題,我們通常先使用檢測,如果可行再去;如果令牌不足,適當拒絕 。所以 基于,并沒有內置的拒絕策略,這一點需要我們額外開發 。
我們不能簡單依賴于方法 , 來實現限流等待,否則這可能帶來嚴重問題 。我們通常需要封裝,并使用額外的屬性記錄其是否“處于限流狀態”、“已經推延的時間點”,如果“已經推延的時間點非常遙遠”且超過可接受范圍,則直接拒絕請求 。簡單來說,封裝方法,增加對請求可能等待時間的判斷,如果超長,則直接拒絕 。
存在一個很大的問題,就是幾乎沒法擴展:子類均為 。反射除外哦 。
一個實踐
還是上一段代碼吧,能更加清晰的看到我們所做的工作: .java:流控器,如果限流開始,則只能有max個請求因此而等待,超過此值則直接拒絕
public class FollowController {private final RateLimiter rateLimiter;private int maxPermits;private Object mutex = new Object();//等待獲取permits的請求個數,原則上可以通過maxPermits推算private int maxWaitingRequests;private AtomicInteger waitingRequests = new AtomicInteger(0);public FollowController(int maxPermits,int maxWaitingRequests) {this.maxPermits = maxPermits;this.maxWaitingRequests = maxWaitingRequests;rateLimiter = RateLimiter.create(maxPermits);}public FollowController(int permits,long warmUpPeriodAsSecond,int maxWaitingRequests) {this.maxPermits = maxPermits;this.maxWaitingRequests = maxWaitingRequests;rateLimiter = RateLimiter.create(permits,warmUpPeriodAsSecond, TimeUnit.SECONDS);}public boolean acquire() {return acquire(1);}public boolean acquire(int permits) {boolean success = rateLimiter.tryAcquire(permits);if (success) {rateLimiter.acquire(permits);//可能有出入return true;}if (waitingRequests.get() > maxWaitingRequests) {return false;}waitingRequests.getAndAdd(permits);rateLimiter.acquire(permits);waitingRequests.getAndAdd(0 - permits);return true;}}
以上代碼,都可以在找到 。
https://github.com/sayhiai/example-ratelimit復制代碼
End
可以看到 , guava提供了一個非常輕量而全面的限流器 。它本身沒有使用多線程去實現,但它是線程安全的 。相比較信號量,它的使用簡單的多 。但鑒于限流場景的多樣性,使用時同樣要非常小心 。
【新手程序員寫秒殺接口不懂如何限流!阿里P6:我來教你高并發限流】本文到此結束,希望對大家有所幫助 。