常用的限流算法有漏桶算法和令牌桶算法置逻,guava的RateLimiter使用的是令牌桶算法推沸,也就是以固定的頻率向桶中放入令牌,例如一秒鐘10枚令牌,實(shí)際業(yè)務(wù)在每次響應(yīng)請(qǐng)求之前都從桶中獲取令牌鬓催,只有取到令牌的請(qǐng)求才會(huì)被成功響應(yīng)肺素,獲取的方式有兩種:阻塞等待令牌或者取不到立即返回失敗,下圖來(lái)自網(wǎng)上:
本次實(shí)戰(zhàn)宇驾,我們用的是guava的RateLimiter倍靡,場(chǎng)景是spring mvc在處理請(qǐng)求時(shí)候,從桶中申請(qǐng)令牌课舍,申請(qǐng)到了就成功響應(yīng)塌西,申請(qǐng)不到時(shí)直接返回失敗。
實(shí)例
1筝尾、添加guava jar包
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
2捡需、AccessLimitService.java 限流服務(wù)封裝到一個(gè)類中AccessLimitService,提供tryAcquire()方法筹淫,用來(lái)嘗試獲取令牌站辉,返回true表示獲取到
@Service
public class AccessLimitService {
//每秒只發(fā)出5個(gè)令牌
RateLimiter rateLimiter = RateLimiter.create(5.0);
/**
* 嘗試獲取令牌
* @return
*/
public boolean tryAcquire(){
return rateLimiter.tryAcquire();
}
}
3、Controller層每次收到請(qǐng)求的時(shí)候都嘗試去獲取令牌损姜,獲取成功和失敗打印不同的信息
@Controller
public class HelloController {
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Autowired
private AccessLimitService accessLimitService;
@RequestMapping("/access")
@ResponseBody
public String access(){
//嘗試獲取令牌
if(accessLimitService.tryAcquire()){
//模擬業(yè)務(wù)執(zhí)行500毫秒
try {
Thread.sleep(500);
}catch (InterruptedException e){
e.printStackTrace();
}
return "aceess success [" + sdf.format(new Date()) + "]";
}else{
return "aceess limit [" + sdf.format(new Date()) + "]";
}
}
}
4饰剥、測(cè)試:十個(gè)線程并發(fā)訪問接口
public class AccessClient {
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);
/**
* get請(qǐng)求
* @param realUrl
* @return
*/
public static String sendGet(URL realUrl) {
String result = "";
BufferedReader in = null;
try {
// 打開和URL之間的連接
URLConnection connection = realUrl.openConnection();
// 設(shè)置通用的請(qǐng)求屬性
connection.setRequestProperty("accept", "*/*");
connection.setRequestProperty("connection", "Keep-Alive");
connection.setRequestProperty("user-agent",
"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
// 建立實(shí)際的連接
connection.connect();
// 定義 BufferedReader輸入流來(lái)讀取URL的響應(yīng)
in = new BufferedReader(new InputStreamReader(
connection.getInputStream()));
String line;
while ((line = in.readLine()) != null) {
result += line;
}
} catch (Exception e) {
System.out.println("發(fā)送GET請(qǐng)求出現(xiàn)異常!" + e);
e.printStackTrace();
}
// 使用finally塊來(lái)關(guān)閉輸入流
finally {
try {
if (in != null) {
in.close();
}
} catch (Exception e2) {
e2.printStackTrace();
}
}
return result;
}
public void access() throws Exception{
final URL url = new URL("http://localhost:8080/guavalimitdemo/access");
for(int i=0;i<10;i++) {
fixedThreadPool.submit(new Runnable() {
public void run() {
System.out.println(sendGet(url));
}
});
}
fixedThreadPool.shutdown();
fixedThreadPool.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
}
public static void main(String[] args) throws Exception{
AccessClient accessClient = new AccessClient();
accessClient.access();
}
}
部分請(qǐng)求由于獲取的令牌可以成功執(zhí)行摧阅,其余請(qǐng)求沒有拿到令牌捐川,我們可以根據(jù)實(shí)際業(yè)務(wù)來(lái)做區(qū)分處理。還有一點(diǎn)要注意逸尖,我們通過RateLimiter.create(5.0)配置的是每一秒5枚令牌古沥,但是限流的時(shí)候發(fā)出的是6枚,改用其他值驗(yàn)證娇跟,也是實(shí)際的比配置的大1岩齿。
以上就是快速實(shí)現(xiàn)限流的實(shí)戰(zhàn)過程,此處僅是單進(jìn)程服務(wù)的限流苞俘,而實(shí)際的分布式服務(wù)中會(huì)考慮更多因素盹沈,會(huì)復(fù)雜很多。
RateLimiter方法摘要
修飾符和類型 | 方法和描述 |
---|---|
double | acquire() 從RateLimiter獲取一個(gè)許可吃谣,該方法會(huì)被阻塞直到獲取到請(qǐng)求 |
double | acquire(int permits)從RateLimiter獲取指定許可數(shù)乞封,該方法會(huì)被阻塞直到獲取到請(qǐng)求 |
static RateLimiter | create(double permitsPerSecond)根據(jù)指定的穩(wěn)定吞吐率創(chuàng)建RateLimiter,這里的吞吐率是指每秒多少許可數(shù)(通常是指QPS岗憋,每秒多少查詢) |
static RateLimiter | create(double permitsPerSecond, long warmupPeriod, TimeUnit unit)根據(jù)指定的穩(wěn)定吞吐率和預(yù)熱期來(lái)創(chuàng)建RateLimiter肃晚,這里的吞吐率是指每秒多少許可數(shù)(通常是指QPS,每秒多少個(gè)請(qǐng)求量)仔戈,在這段預(yù)熱時(shí)間內(nèi)关串,RateLimiter每秒分配的許可數(shù)會(huì)平穩(wěn)地增長(zhǎng)直到預(yù)熱期結(jié)束時(shí)達(dá)到其最大速率拧廊。(只要存在足夠請(qǐng)求數(shù)來(lái)使其飽和) |
double | getRate()返回RateLimiter 配置中的穩(wěn)定速率,該速率單位是每秒多少許可數(shù) |
void | setRate(double permitsPerSecond)更新RateLimite的穩(wěn)定速率晋修,參數(shù)permitsPerSecond 由構(gòu)造RateLimiter的工廠方法提供吧碾。 |
String | toString()返回對(duì)象的字符表現(xiàn)形式 |
boolean | tryAcquire()從RateLimiter 獲取許可,如果該許可可以在無(wú)延遲下的情況下立即獲取得到的話 |
boolean | tryAcquire(int permits)從RateLimiter 獲取許可數(shù)墓卦,如果該許可數(shù)可以在無(wú)延遲下的情況下立即獲取得到的話 |
boolean | tryAcquire(int permits, long timeout, TimeUnit unit)從RateLimiter 獲取指定許可數(shù)如果該許可數(shù)可以在不超過timeout的時(shí)間內(nèi)獲取得到的話倦春,或者如果無(wú)法在timeout 過期之前獲取得到許可數(shù)的話,那么立即返回false (無(wú)需等待) |
boolean | tryAcquire(long timeout, TimeUnit unit)從RateLimiter 獲取許可如果該許可可以在不超過timeout的時(shí)間內(nèi)獲取得到的話落剪,或者如果無(wú)法在timeout 過期之前獲取得到許可的話溅漾,那么立即返回false(無(wú)需等待) |
- 舉例來(lái)說明如何使用RateLimiter,想象下我們需要處理一個(gè)任務(wù)列表著榴,但我們不希望每秒的任務(wù)提交超過兩個(gè):
//速率是每秒兩個(gè)許可
final RateLimiter rateLimiter = RateLimiter.create(2.0);
void submitTasks(List tasks, Executor executor) {
for (Runnable task : tasks) {
rateLimiter.acquire(); // 也許需要等待
executor.execute(task);
}
}