漏斗算法
它有點像我們生活中用到的漏斗,液體倒進去以后,總是從下端的小口中以固定速率流出,漏斗算法也類似,不管突然流量有多大,漏斗都保證了流量的常速率輸出,也可以類比于調用量,比如,不管服務調用多么不穩定,我們只固定進行服務輸出,比如每10毫秒接受一次服務調用。既然是一個桶,那就肯定有容量,由于調用的消費速率已經固定,那么當桶的容量堆滿了,則只能丟棄了,漏斗算法如下圖:
缺點
漏斗算法其實是悲觀的,因為它嚴格限制了系統的吞吐量,從某種角度上來說,它的效果和并發量限流很類似。漏斗算法也可以用于大多數場景,但由于它對服務吞吐量有著嚴格固定的限制,如果在某個大的服務網絡中只對某些服務進行漏斗算法限流,這些服務可能會成為瓶頸。其實對于可擴展的大型服務網絡,上游的服務壓力可以經過多重下游服務進行擴散,過多的漏斗限流似乎意義不大。
實現:
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
/**
* <p>
* 對請求做限速流控
* </p>
*
* @author wangguangdong
* @version 1.0
* @Date 17/3/3
*/
public class LimitRequestByTime {
long limit = 1000;
Map<Long, AtomicLong> map = Collections.synchronizedMap(new LinkedHashMap<>(8));
public boolean limitReq() {
// 統計當前秒數
long currentTimeMillis = System.currentTimeMillis() / 1000;
map.putIfAbsent(currentTimeMillis, new AtomicLong(0));
// 獲取秒速流控
AtomicLong currentAtomicLong = map.get(currentTimeMillis);
return !(currentAtomicLong.incrementAndGet() >= limit);
}
public static void main(String[] args) {
LimitRequestByTime limitRequestByTime = new LimitRequestByTime();
// 統計所有的請求次數
Map<Long, AtomicLong> totalMap = new ConcurrentHashMap<>();
Map<Long, AtomicLong> successTotalMap = new ConcurrentHashMap<>();
for (int i = 0; i < 100000000; i++) {
// 統計當前這一秒的請求書
long currentTimeMillis = System.currentTimeMillis() / 1000;
totalMap.putIfAbsent(currentTimeMillis, new AtomicLong(0));
// 自增加1
totalMap.get(currentTimeMillis).incrementAndGet();
successTotalMap.putIfAbsent(currentTimeMillis, new AtomicLong(0));
if (limitRequestByTime.limitReq()) {
successTotalMap.get(currentTimeMillis).incrementAndGet();
}
}
for (Map.Entry<Long, AtomicLong> total : totalMap.entrySet()) {
Long totalKey = total.getKey();
System.out.println(String
.format("在%d這一秒一共發送了%d次請求,通過的請求數量為%d", totalKey, totalMap.get(totalKey).get(),
successTotalMap.get(totalKey).get()));
}
}
}
令牌桶算法
令牌桶算法從某種程度上來說是漏桶算法的一種改進,漏桶算法能夠強行限制請求調用的速率,而令牌桶算法能夠在限制調用的平均速率的同時還允許某種程度的突發調用。在令牌桶算法中,桶中會有一定數量的令牌,每次請求調用需要去桶中拿取一個令牌,拿到令牌后才有資格執行請求調用,否則只能等待能拿到足夠的令牌數,大家看到這里,可能就認為是不是可以把令牌比喻成信號量,那和前面說的并發量限流不是沒什么區別嘛?其實不然,令牌桶算法的精髓就在于“拿令牌”和“放令牌”的方式,這和單純的并發量限流有明顯區別,采用并發量限流時,當一個調用到來時,會先獲取一個信號量,當調用結束時,會釋放一個信號量,但令牌桶算法不同,因為每次請求獲取的令牌數不是固定的,比如當桶中的令牌數還比較多時,每次調用只需要獲取一個令牌,隨著桶中的令牌數逐漸減少,當到令牌的使用率(即使用中的令牌數/令牌總數)達某個比例,可能一次請求需要獲取兩個令牌,當令牌使用率到了一個更高的比例,可能一次請求調用需要獲取更多的令牌數。同時,當調用使用完令牌后,有兩種令牌生成方法,第一種就是直接往桶中放回使用的令牌數,第二種就是不做任何操作,有另一個額外的令牌生成步驟來將令牌勻速放回桶中。如下圖:
代碼實現
import java.io.BufferedWriter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import com.google.common.base.Preconditions;
/**
* <p>
* 令牌桶算法
* </p>
*
* @author wangguangdong
* @version 1.0
* @Date 17/3/23
*/
public class TokenBucket {
// 默認桶大小個數 即最大瞬間流量是64M
private static final int DEFAULT_BUCKET_SIZE = 1024 * 1024 * 64;
// 一個桶的單位是1字節
private int everyTokenSize = 1;
// 瞬間最大流量
private int maxFlowRate;
// 平均流量
private int avgFlowRate;
// 隊列來緩存桶數量:最大的流量峰值就是 = everyTokenSize*DEFAULT_BUCKET_SIZE 64M = 1 * 1024 * 1024 * 64
private ArrayBlockingQueue<Byte> tokenQueue = new ArrayBlockingQueue<Byte>(DEFAULT_BUCKET_SIZE);
private ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
private volatile boolean isStart = false;
private ReentrantLock lock = new ReentrantLock(true);
private static final byte A_CHAR = 'a';
public TokenBucket() {
}
public TokenBucket(int maxFlowRate, int avgFlowRate) {
this.maxFlowRate = maxFlowRate;
this.avgFlowRate = avgFlowRate;
}
public TokenBucket(int everyTokenSize, int maxFlowRate, int avgFlowRate) {
this.everyTokenSize = everyTokenSize;
this.maxFlowRate = maxFlowRate;
this.avgFlowRate = avgFlowRate;
}
public void addTokens(Integer tokenNum) {
// 若是桶已經滿了,就不再家如新的令牌
for (int i = 0; i < tokenNum; i++) {
tokenQueue.offer(A_CHAR);
}
}
public TokenBucket build() {
start();
return this;
}
/**
* 獲取足夠的令牌個數
*
* @return
*/
public boolean getTokens(byte[] dataSize) {
Preconditions.checkNotNull(dataSize);
Preconditions.checkArgument(isStart, "please invoke start method first !");
int needTokenNum = dataSize.length / everyTokenSize + 1;// 傳輸內容大小對應的桶個數
final ReentrantLock lock = this.lock;
lock.lock();
try {
boolean result = needTokenNum <= tokenQueue.size(); // 是否存在足夠的桶數量
if (!result) {
return false;
}
int tokenCount = 0;
for (int i = 0; i < needTokenNum; i++) {
Byte poll = tokenQueue.poll();
if (poll != null) {
tokenCount++;
}
}
return tokenCount == needTokenNum;
} finally {
lock.unlock();
}
}
public void start() {
// 初始化桶隊列大小
if (maxFlowRate != 0) {
tokenQueue = new ArrayBlockingQueue<Byte>(maxFlowRate);
}
// 初始化令牌生產者
TokenProducer tokenProducer = new TokenProducer(avgFlowRate, this);
scheduledExecutorService.scheduleAtFixedRate(tokenProducer, 0, 1, TimeUnit.SECONDS);
isStart = true;
}
public void stop() {
isStart = false;
scheduledExecutorService.shutdown();
}
public boolean isStarted() {
return isStart;
}
class TokenProducer implements Runnable {
private int avgFlowRate;
private TokenBucket tokenBucket;
public TokenProducer(int avgFlowRate, TokenBucket tokenBucket) {
this.avgFlowRate = avgFlowRate;
this.tokenBucket = tokenBucket;
}
@Override
public void run() {
tokenBucket.addTokens(avgFlowRate);
}
}
public static TokenBucket newBuilder() {
return new TokenBucket();
}
public TokenBucket everyTokenSize(int everyTokenSize) {
this.everyTokenSize = everyTokenSize;
return this;
}
public TokenBucket maxFlowRate(int maxFlowRate) {
this.maxFlowRate = maxFlowRate;
return this;
}
public TokenBucket avgFlowRate(int avgFlowRate) {
this.avgFlowRate = avgFlowRate;
return this;
}
private String stringCopy(String data, int copyNum) {
StringBuilder sbuilder = new StringBuilder(data.length() * copyNum);
for (int i = 0; i < copyNum; i++) {
sbuilder.append(data);
}
return sbuilder.toString();
}
public static void main(String[] args) throws IOException, InterruptedException {
tokenTest();
}
private static void arrayTest() {
ArrayBlockingQueue<Integer> tokenQueue = new ArrayBlockingQueue<Integer>(10);
tokenQueue.offer(1);
tokenQueue.offer(1);
tokenQueue.offer(1);
System.out.println(tokenQueue.size());
System.out.println(tokenQueue.remainingCapacity());
}
private static void tokenTest() throws InterruptedException, IOException {
TokenBucket tokenBucket = TokenBucket.newBuilder().avgFlowRate(512).maxFlowRate(1024).build();
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("/tmp/ds_test")));
String data = "xxxx";// 四個字節
for (int i = 1; i <= 1000; i++) {
Random random = new Random();
int i1 = random.nextInt(100);
boolean tokens = tokenBucket.getTokens(tokenBucket.stringCopy(data, i1).getBytes());
TimeUnit.MILLISECONDS.sleep(100);
if (tokens) {
bufferedWriter.write("token pass --- index:" + i1);
System.out.println("token pass --- index:" + i1);
} else {
bufferedWriter.write("token rejuect --- index" + i1);
System.out.println("token rejuect --- index" + i1);
}
bufferedWriter.newLine();
bufferedWriter.flush();
}
bufferedWriter.close();
}
}
漏斗算法和令牌桶的異同點
漏斗算法會限制平均的qps,對每個時間段的流控都是一樣的,如果突然一瞬間的大流量進來,那么有可能會有大量請求被攔截住,
令牌桶算法的話除了能夠限制數據的平均傳輸速率外,還允許一定時間內的大流量涌入,相當于漏斗算法的升級版本。