Java高并發(fā)系統(tǒng)限流算法的實(shí)現(xiàn)
1 概述
在開發(fā)高并發(fā)系統(tǒng)時(shí)有三把利器用來(lái)保護(hù)系統(tǒng):緩存、降級(jí)和限流。限流可以認(rèn)為服務(wù)降級(jí)的一種,限流是對(duì)系統(tǒng)的一種保護(hù)措施。即限制流量請(qǐng)求的頻率(每秒處理多少個(gè)請(qǐng)求)。一般來(lái)說(shuō),當(dāng)請(qǐng)求流量超過(guò)系統(tǒng)的瓶頸,則丟棄掉多余的請(qǐng)求流量,保證系統(tǒng)的可用性。即要么不放進(jìn)來(lái),放進(jìn)來(lái)的就保證提供服務(wù)。比如:延遲處理,拒絕處理,或者部分拒絕處理等等。
2 計(jì)數(shù)器限流
2.1 概述
計(jì)數(shù)器采用簡(jiǎn)單的計(jì)數(shù)操作,到一段時(shí)間節(jié)點(diǎn)后自動(dòng)清零
2.2 實(shí)現(xiàn)
package com.oldlu.limit;
import java.util.concurrent.*;
public class Counter {
public static void main(String[] args) {
//計(jì)數(shù)器,這里用信號(hào)量實(shí)現(xiàn)
final Semaphore semaphore = new Semaphore(3);
//定時(shí)器,到點(diǎn)清零
ScheduledExecutorService service = Executors.newScheduledThreadPool(1);
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
semaphore.release(3);
}
},3000,3000,TimeUnit.MILLISECONDS);
//模擬無(wú)數(shù)個(gè)請(qǐng)求從天而降
while (true) {
try {
//判斷計(jì)數(shù)器
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
//如果準(zhǔn)許響應(yīng),打印一個(gè)ok
System.out.println("ok");
}
}
}
2.3 結(jié)果分析
3個(gè)ok一組呈現(xiàn),到下一個(gè)計(jì)數(shù)周期之前被阻斷
2.4 優(yōu)缺點(diǎn)
實(shí)現(xiàn)起來(lái)非常簡(jiǎn)單。
控制力度太過(guò)于簡(jiǎn)略,假如1s內(nèi)限制3次,那么如果3次在前100ms內(nèi)已經(jīng)用完,后面的900ms將只能處于阻塞狀態(tài),白白浪費(fèi)掉。
2.5 應(yīng)用
使用計(jì)數(shù)器限流的場(chǎng)景較少,因?yàn)樗奶幚磉壿嫴粔蜢`活。最常見的可能在web的登錄密碼驗(yàn)證,輸入錯(cuò)誤次數(shù)凍結(jié)一段時(shí)間的場(chǎng)景。如果網(wǎng)站請(qǐng)求使用計(jì)數(shù)器,那么惡意攻擊者前100ms吃掉流量計(jì)數(shù),使得后續(xù)正常的請(qǐng)求被全部阻斷,整個(gè)服務(wù)很容易被搞垮。
3 漏桶算法
3.1 概述
漏桶算法將請(qǐng)求緩存在桶中,服務(wù)流程勻速處理。超出桶容量的部分丟棄。漏桶算法主要用于保護(hù)內(nèi)部的處理業(yè)務(wù),保障其穩(wěn)定有節(jié)奏的處理請(qǐng)求,但是無(wú)法根據(jù)流量的波動(dòng)彈性調(diào)整響應(yīng)能力。現(xiàn)實(shí)中,類似容納人數(shù)有限的服務(wù)大廳開啟了固定的服務(wù)窗口。

3.2 實(shí)現(xiàn)
package com.oldlu.limit;
import java.util.concurrent.*;
public class Barrel {
public static void main(String[] args) {
//桶,用阻塞隊(duì)列實(shí)現(xiàn),容量為3
final LinkedBlockingQueue<Integer> que = new LinkedBlockingQueue(3);
//定時(shí)器,相當(dāng)于服務(wù)的窗口,2s處理一個(gè)
ScheduledExecutorService service = Executors.newScheduledThreadPool(1);
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
int v = que.poll();
System.out.println("處理:"+v);
}
},2000,2000,TimeUnit.MILLISECONDS);
//無(wú)數(shù)個(gè)請(qǐng)求,i 可以理解為請(qǐng)求的編號(hào)
int i=0;
while (true) {
i++;
try {
System.out.println("put:"+i);
//如果是put,會(huì)一直等待桶中有空閑位置,不會(huì)丟棄
// que.put(i);
//等待1s如果進(jìn)不了桶,就溢出丟棄
que.offer(i,1000,TimeUnit.MILLISECONDS);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
3.3 結(jié)果分析

put任務(wù)號(hào)按照順序入桶
執(zhí)行任務(wù)勻速的1s一個(gè)被處理
因?yàn)橥暗娜萘恐挥?,所以1-3完美執(zhí)行,4被溢出丟棄,5正常執(zhí)行
3.4 優(yōu)缺點(diǎn)
有效的擋住了外部的請(qǐng)求,保護(hù)了內(nèi)部的服務(wù)不會(huì)過(guò)載
內(nèi)部服務(wù)勻速執(zhí)行,無(wú)法應(yīng)對(duì)流量洪峰,無(wú)法做到彈性處理突發(fā)任務(wù)
任務(wù)超時(shí)溢出時(shí)被丟棄?,F(xiàn)實(shí)中可能需要緩存隊(duì)列輔助保持一段時(shí)間
5)應(yīng)用
nginx中的限流是漏桶算法的典型應(yīng)用,配置案例如下:
http {
#$binary_remote_addr 表示通過(guò)remote_addr這個(gè)標(biāo)識(shí)來(lái)做key,也就是限制同一客戶端ip地址。
#zone=one:10m 表示生成一個(gè)大小為10M,名字為one的內(nèi)存區(qū)域,用來(lái)存儲(chǔ)訪問(wèn)的頻次信息。
#rate=1r/s 表示允許相同標(biāo)識(shí)的客戶端每秒1次訪問(wèn)
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
server {
location /limited/ {
#zone=one 與上面limit_req_zone 里的name對(duì)應(yīng)。
#burst=5 緩沖區(qū),超過(guò)了訪問(wèn)頻次限制的請(qǐng)求可以先放到這個(gè)緩沖區(qū)內(nèi),類似代碼中的隊(duì)列長(zhǎng)度。
#nodelay 如果設(shè)置,超過(guò)訪問(wèn)頻次而且緩沖區(qū)也滿了的時(shí)候就會(huì)直接返回503,如果沒(méi)有設(shè)置,則所有請(qǐng)求
會(huì)等待排隊(duì),類似代碼中的put還是offer。
limit_req zone=one burst=5 nodelay;
}
}4 令牌桶算法
4.1 概述
令牌桶算法可以認(rèn)為是漏桶算法的一種升級(jí),它不但可以將流量做一步限制,還可以解決漏桶中無(wú)法彈性伸縮處理請(qǐng)求的問(wèn)題。體現(xiàn)在現(xiàn)實(shí)中,類似服務(wù)大廳的門口設(shè)置門禁卡發(fā)放。發(fā)放是勻速的,請(qǐng)求較少時(shí),令牌可以緩存起來(lái),供流量爆發(fā)時(shí)一次性批量獲取使用。而內(nèi)部服務(wù)窗口不設(shè)限。

4.2 實(shí)現(xiàn)
package com.oldlu.limit;
import java.util.concurrent.*;
public class Token {
public static void main(String[] args) throws InterruptedException {
//令牌桶,信號(hào)量實(shí)現(xiàn),容量為3
final Semaphore semaphore = new Semaphore(3);
//定時(shí)器,1s一個(gè),勻速頒發(fā)令牌
ScheduledExecutorService service = Executors.newScheduledThreadPool(1);
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
if (semaphore.availablePermits() < 3){
semaphore.release();
}
// System.out.println("令牌數(shù):"+semaphore.availablePermits());
}
},1000,1000,TimeUnit.MILLISECONDS);
//等待,等候令牌桶儲(chǔ)存
Thread.sleep(5);
//模擬洪峰5個(gè)請(qǐng)求,前3個(gè)迅速響應(yīng),后兩個(gè)排隊(duì)
for (int i = 0; i < 5; i++) {
semaphore.acquire();
System.out.println("洪峰:"+i);
}
//模擬日常請(qǐng)求,2s一個(gè)
for (int i = 0; i < 3; i++) {
Thread.sleep(1000);
semaphore.acquire();
System.out.println("日常:"+i);
Thread.sleep(1000);
}
//再次洪峰
for (int i = 0; i < 5; i++) {
semaphore.acquire();
System.out.println("洪峰:"+i);
}
//檢查令牌桶的數(shù)量
for (int i = 0; i < 5; i++) {
Thread.sleep(2000);
System.out.println("令牌剩余:"+semaphore.availablePermits());
}
}
}
4.3 結(jié)果分析

注意結(jié)果出現(xiàn)的節(jié)奏!
洪峰0-2迅速被執(zhí)行,說(shuō)明桶中暫存了3個(gè)令牌,有效應(yīng)對(duì)了洪峰
洪峰3,4被間隔性執(zhí)行,得到了有效的限流
日常請(qǐng)求被勻速執(zhí)行,間隔均勻
第二波洪峰來(lái)臨,和第一次一樣
請(qǐng)求過(guò)去后,令牌最終被均勻頒發(fā),積累到3個(gè)后不再上升
4.4 應(yīng)用
springcloud中g(shù)ateway可以配置令牌桶實(shí)現(xiàn)限流控制,案例如下:
cloud:
gateway:
routes:
‐ id: limit_route
uri: http://localhost:8080/test
filters:
‐ name: RequestRateLimiter
args:
#限流的key,ipKeyResolver為spring中托管的Bean,需要擴(kuò)展KeyResolver接口
key‐resolver: '#{@ipResolver}'
#令牌桶每秒填充平均速率,相當(dāng)于代碼中的發(fā)放頻率
redis‐rate‐limiter.replenishRate: 1
#令牌桶總?cè)萘?,相?dāng)于代碼中,信號(hào)量的容量
redis‐rate‐limiter.burstCapacity: 35 滑動(dòng)窗口
5.1 概述
滑動(dòng)窗口可以理解為細(xì)分之后的計(jì)數(shù)器,計(jì)數(shù)器粗暴的限定1分鐘內(nèi)的訪問(wèn)次數(shù),而滑動(dòng)窗口限流將1分鐘拆為多個(gè)段,不但要求整個(gè)1分鐘內(nèi)請(qǐng)求數(shù)小于上限,而且要求每個(gè)片段請(qǐng)求數(shù)也要小于上限。相當(dāng)于將原來(lái)的計(jì)數(shù)周期做了多個(gè)片段拆分。更為精細(xì)。

5.2 實(shí)現(xiàn)

package com.oldlu.limit;
import java.util.LinkedList;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class Window {
//整個(gè)窗口的流量上限,超出會(huì)被限流
final int totalMax = 5;
//每片的流量上限,超出同樣會(huì)被拒絕,可以設(shè)置不同的值
final int sliceMax = 5;
//分多少片
final int slice = 3;
//窗口,分3段,每段1s,也就是總長(zhǎng)度3s
final LinkedList<Long> linkedList = new LinkedList<>();
//計(jì)數(shù)器,每片一個(gè)key,可以使用HashMap,這里為了控制臺(tái)保持有序性和可讀性,采用TreeMap
Map<Long,AtomicInteger> map = new TreeMap();
//心跳,每1s跳動(dòng)1次,滑動(dòng)窗口向前滑動(dòng)一步,實(shí)際業(yè)務(wù)中可能需要手動(dòng)控制滑動(dòng)窗口的時(shí)機(jī)。
ScheduledExecutorService service = Executors.newScheduledThreadPool(1);
//獲取key值,這里即是時(shí)間戳(秒)
private Long getKey(){
return System.currentTimeMillis()/1000;
}
public Window(){
//初始化窗口,當(dāng)前時(shí)間指向的是最末端,前兩片其實(shí)是過(guò)去的2s
Long key = getKey();
for (int i = 0; i < slice; i++) {
linkedList.addFirst(key‐i);
map.put(key‐i,new AtomicInteger(0));
}
//啟動(dòng)心跳任務(wù),窗口根據(jù)時(shí)間,自動(dòng)向前滑動(dòng),每秒1步
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
Long key = getKey();
//隊(duì)尾添加最新的片
linkedList.addLast(key);
map.put(key,new AtomicInteger());
//將最老的片移除
map.remove(linkedList.getFirst());
linkedList.removeFirst();
System.out.println("step:"+key+":"+map);;
}
},1000,1000,TimeUnit.MILLISECONDS);
}
//檢查當(dāng)前時(shí)間所在的片是否達(dá)到上限
public boolean checkCurrentSlice(){
long key = getKey();
AtomicInteger integer = map.get(key);
if (integer != null){
return integer.get() < sliceMax ;
}
//默認(rèn)允許訪問(wèn)
return true;
}
//檢查整個(gè)窗口所有片的計(jì)數(shù)之和是否達(dá)到上限
public boolean checkAllCount(){
return map.values().stream().mapToInt(value ‐> value.get()).sum() < totalMax;
}
//請(qǐng)求來(lái)臨....
public void req(){
Long key = getKey();
//如果時(shí)間窗口未到達(dá)當(dāng)前時(shí)間片,稍微等待一下
//其實(shí)是一個(gè)保護(hù)措施,放置心跳對(duì)滑動(dòng)窗口的推動(dòng)滯后于當(dāng)前請(qǐng)求
while (linkedList.getLast()<key){
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//開始檢查,如果未達(dá)到上限,返回ok,計(jì)數(shù)器增加1
//如果任意一項(xiàng)達(dá)到上限,拒絕請(qǐng)求,達(dá)到限流的目的
//這里是直接拒絕?,F(xiàn)實(shí)中可能會(huì)設(shè)置緩沖池,將請(qǐng)求放入緩沖隊(duì)列暫存
if (checkCurrentSlice() && checkAllCount()){
map.get(key).incrementAndGet();
System.out.println(key+"=ok:"+map);
}else {
System.out.println(key+"=reject:"+map);
}
}
public static void main(String[] args) throws InterruptedException {
Window window = new Window();
//模擬10個(gè)離散的請(qǐng)求,相對(duì)之間有200ms間隔。會(huì)造成總數(shù)達(dá)到上限而被限流
for (int i = 0; i < 10; i++) {
Thread.sleep(200);
window.req();
}
//等待一下窗口滑動(dòng),讓各個(gè)片的計(jì)數(shù)器都置零
Thread.sleep(3000);
//模擬突發(fā)請(qǐng)求,單個(gè)片的計(jì)數(shù)器達(dá)到上限而被限流
System.out.println("‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐");
for (int i = 0; i < 10; i++) {
window.req();
}
}
}
5.3 結(jié)果分析
模擬零零散散的請(qǐng)求,會(huì)造成每個(gè)片里均有計(jì)數(shù),總數(shù)達(dá)到上限后,不再響應(yīng),限流生效:

再模擬突發(fā)的流量請(qǐng)求,會(huì)造成單片流量計(jì)數(shù)達(dá)到上限,不再響應(yīng)而被限流

5.4 應(yīng)用
滑動(dòng)窗口算法,在tcp協(xié)議發(fā)包過(guò)程中被使用。在web現(xiàn)實(shí)場(chǎng)景中,可以將流量控制做更細(xì)化處理,解決計(jì)數(shù)器模型控制力度太粗暴的問(wèn)題。
到此這篇關(guān)于Java高并發(fā)系統(tǒng)限流算法的應(yīng)用的文章就介紹到這了,更多相關(guān)Java高并發(fā)限流算法內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于Pinpoint對(duì)SpringCloud微服務(wù)項(xiàng)目實(shí)現(xiàn)全鏈路監(jiān)控的問(wèn)題
這篇文章主要介紹了基于Pinpoint對(duì)SpringCloud微服務(wù)項(xiàng)目實(shí)現(xiàn)全鏈路監(jiān)控的問(wèn)題,本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-02-02
Java實(shí)現(xiàn)PDF轉(zhuǎn)圖片的三種方法
有些時(shí)候我們需要在項(xiàng)目中展示PDF,所以我們可以將PDF轉(zhuǎn)為圖片,然后已圖片的方式展示,效果很好,Java使用各種技術(shù)將pdf轉(zhuǎn)換成圖片格式,并且內(nèi)容不失幀,本文給大家介紹了三種方法實(shí)現(xiàn)PDF轉(zhuǎn)圖片的案例,需要的朋友可以參考下2023-10-10
Java編程用兩個(gè)棧實(shí)現(xiàn)隊(duì)列代碼分享
這篇文章主要介紹了Java編程用兩個(gè)棧實(shí)現(xiàn)隊(duì)列代碼分享,具有一定參考價(jià)值,這里給大家分享下,供需要的朋友了解。2017-10-10
使用React和springboot做前后端分離項(xiàng)目的步驟方式
這篇文章主要介紹了使用React和springboot做前后端分離項(xiàng)目的步驟方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-08-08
Mybatis結(jié)果集映射一對(duì)多簡(jiǎn)單入門教程
本文給大家介紹Mybatis結(jié)果集映射一對(duì)多簡(jiǎn)單入門教程,包括搭建數(shù)據(jù)庫(kù)環(huán)境的過(guò)程,idea搭建maven項(xiàng)目的代碼詳解,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友參考下吧2021-06-06
MyBatis-Plus實(shí)現(xiàn)2種分頁(yè)方法(QueryWrapper查詢分頁(yè)和SQL查詢分頁(yè))
本文主要介紹了MyBatis-Plus實(shí)現(xiàn)2種分頁(yè)方法,主要包括QueryWrapper查詢分頁(yè)和SQL查詢分頁(yè),具有一定的參考價(jià)值,感興趣的可以了解一下2021-08-08

