參考文章:Guava-RateLimiter詳解
常用的限流算法有漏桶算法和令牌桶算法,guava的RateLimiter使用的是令牌桶算法,也就是以固定的頻率向桶中放入令牌,例如一秒鐘10枚令牌,實際業務在每次響應請求之前都從桶中獲取令牌,只有取到令牌的請求才會被成功響應,獲取的方式有兩種:阻塞等待令牌或者取不到立即返回失敗,下圖來自網上:
ratelimite原理圖
本次實戰,我們用的是guava的RateLimiter,場景是spring mvc在處理請求時候,從桶中申請令牌,申請到了就成功響應,申請不到時直接返回失敗。
實例
1、添加guava jar包
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
2、AccessLimitService.java 限流服務封裝到一個類中AccessLimitService,提供tryAcquire()方法,用來嘗試獲取令牌,返回true表示獲取到
@Service
public class AccessLimitService {
//每秒只發出5個令牌
RateLimiter rateLimiter = RateLimiter.create(5.0);
/**
* 嘗試獲取令牌
* @return
*/
public boolean tryAcquire(){
return rateLimiter.tryAcquire();
}
}
3、Controller層每次收到請求的時候都嘗試去獲取令牌,獲取成功和失敗打印不同的信息
@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()){
//模擬業務執行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、測試:十個線程併發訪問接口
public class AccessClient {
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);
/**
* get請求
* @param realUrl
* @return
*/
public static String sendGet(URL realUrl) {
String result = "";
BufferedReader in = null;
try {
// 打開和URL之間的連接
URLConnection connection = realUrl.openConnection();
// 設置通用的請求屬性
connection.setRequestProperty("accept", "*/*");
connection.setRequestProperty("connection", "Keep-Alive");
connection.setRequestProperty("user-agent",
"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
// 建立實際的連接
connection.connect();
// 定義 BufferedReader輸入流來讀取URL的響應
in = new BufferedReader(new InputStreamReader(
connection.getInputStream()));
String line;
while ((line = in.readLine()) != null) {
result += line;
}
} catch (Exception e) {
System.out.println("發送GET請求出現異常!" + e);
e.printStackTrace();
}
// 使用finally塊來關閉輸入流
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();
}
}
部分請求由於獲取的令牌可以成功執行,其餘請求沒有拿到令牌,我們可以根據實際業務來做區分處理。還有一點要注意,我們通過RateLimiter.create(5.0)配置的是每一秒5枚令牌,但是限流的時候發出的是6枚,改用其他值驗證,也是實際的比配置的大1。
以上就是快速實現限流的實戰過程,此處僅是單進程服務的限流,而實際的分佈式服務中會考慮更多因素,會複雜很多。
RateLimiter方法摘要
修飾符和類型 | 方法和描述 |
---|---|
double | acquire() 從RateLimiter獲取一個許可,該方法會被阻塞直到獲取到請求 |
double | acquire(int permits)從RateLimiter獲取指定許可數,該方法會被阻塞直到獲取到請求 |
static RateLimiter | create(double permitsPerSecond)根據指定的穩定吞吐率創建RateLimiter,這裏的吞吐率是指每秒多少許可數(通常是指QPS,每秒多少查詢) |
static RateLimiter | create(double permitsPerSecond, long warmupPeriod, TimeUnit unit)根據指定的穩定吞吐率和預熱期來創建RateLimiter,這裏的吞吐率是指每秒多少許可數(通常是指QPS,每秒多少個請求量),在這段預熱時間內,RateLimiter每秒分配的許可數會平穩地增長直到預熱期結束時達到其最大速率。(只要存在足夠請求數來使其飽和) |
double | getRate()返回RateLimiter 配置中的穩定速率,該速率單位是每秒多少許可數 |
void | setRate(double permitsPerSecond)更新RateLimite的穩定速率,參數permitsPerSecond 由構造RateLimiter的工廠方法提供。 |
String | toString()返回對象的字符表現形式 |
boolean | tryAcquire()從RateLimiter 獲取許可,如果該許可可以在無延遲下的情況下立即獲取得到的話 |
boolean | tryAcquire(int permits)從RateLimiter 獲取許可數,如果該許可數可以在無延遲下的情況下立即獲取得到的話 |
boolean | tryAcquire(int permits, long timeout, TimeUnit unit)從RateLimiter 獲取指定許可數如果該許可數可以在不超過timeout的時間內獲取得到的話,或者如果無法在timeout 過期之前獲取得到許可數的話,那麼立即返回false (無需等待) |
boolean | tryAcquire(long timeout, TimeUnit unit)從RateLimiter 獲取許可如果該許可可以在不超過timeout的時間內獲取得到的話,或者如果無法在timeout 過期之前獲取得到許可的話,那麼立即返回false(無需等待) |
- 舉例來說明如何使用RateLimiter,想象下我們需要處理一個任務列表,但我們不希望每秒的任務提交超過兩個:
//速率是每秒兩個許可
final RateLimiter rateLimiter = RateLimiter.create(2.0);
void submitTasks(List tasks, Executor executor) {
for (Runnable task : tasks) {
rateLimiter.acquire(); // 也許需要等待
executor.execute(task);
}
}