1. 程式人生 > >【高併發】億級流量場景下如何為HTTP介面限流?看完我懂了!!

【高併發】億級流量場景下如何為HTTP介面限流?看完我懂了!!

## 寫在前面 > 在網際網路應用中,高併發系統會面臨一個重大的挑戰,那就是大量流高併發訪問,比如:天貓的雙十一、京東618、秒殺、搶購促銷等,這些都是典型的大流量高併發場景。關於秒殺,小夥伴們可以參見我的另一篇文章《[【高併發】高併發秒殺系統架構解密,不是所有的秒殺都是秒殺!](https://mp.weixin.qq.com/s?__biz=Mzg3MzE1NTIzNA==&mid=2247484357&idx=1&sn=23e6e38143704db0fa4588186b534e13&chksm=cee51c08f992951e5b883c55b788588f9cbc822e41694b5b4a334ea5d2dc0ae62a5d64e39dc2&token=1388808518&lang=zh_CN#rd)》 > > 關於【冰河技術】微信公眾號,解鎖更多【高併發】專題文章。 > > 注意:由於原文篇幅比較長,所以被拆分為:理論、演算法、實戰(HTTP介面實戰+分散式限流實戰)三大部分。 > > 理論篇:《[【高併發】如何實現億級流量下的分散式限流?這些理論你必須掌握!!](https://mp.weixin.qq.com/s?__biz=Mzg3MzE1NTIzNA==&mid=2247485706&idx=1&sn=c7d71c0c6b9b15c3b330766f1083e29c&chksm=cee516c7f9929fd170ce636a63fc3764d5ef62eb7ef67a7601664d3d56b0d04c8759e666bc87&token=378924601&lang=zh_CN#rd)》 > > 演算法篇:《[【高併發】如何實現億級流量下的分散式限流?這些演算法你必須掌握!!](https://mp.weixin.qq.com/s?__biz=Mzg3MzE1NTIzNA==&mid=2247485719&idx=1&sn=8659791a07a55ae4b646679846d0264f&chksm=cee516daf9929fcc33961276715980832d0b2d875cf563121052592141f132dfa31bbd669ebf&token=378924601&lang=zh_CN#rd)》 > > 專案原始碼已提交到github:https://github.com/sunshinelyz/mykit-ratelimiter ## HTTP介面限流實戰 這裡,我們實現Web介面限流,具體方式為:使用自定義註解封裝基於令牌桶限流演算法實現介面限流。 ## 不使用註解實現介面限流 ### 搭建專案 這裡,我們使用SpringBoot專案來搭建Http介面限流專案,SpringBoot專案本質上還是一個Maven專案。所以,小夥伴們可以直接建立一個Maven專案,我這裡的專案名稱為mykit-ratelimiter-test。接下來,在pom.xml檔案中新增如下依賴使專案構建為一個SpringBoot專案。 ```xml ``` 可以看到,我在專案中除了引用了SpringBoot相關的Jar包外,還引用了guava框架,版本為28.2-jre。 ### 建立核心類 這裡,我主要是模擬一個支付介面的限流場景。首先,我們定義一個PayService介面和MessageService介面。PayService介面主要用於模擬後續的支付業務,MessageService介面模擬傳送訊息。介面的定義分別如下所示。 * PayService ```java package io.mykit.limiter.service; import java.math.BigDecimal; /** * @author binghe * @version 1.0.0 * @description 模擬支付 */ public interface PayService { int pay(BigDecimal price); } ``` * MessageService ```java package io.mykit.limiter.service; /** * @author binghe * @version 1.0.0 * @description 模擬傳送訊息服務 */ public interface MessageService { boolean sendMessage(String message); } ``` 接下來,建立二者的實現類,分別如下。 * MessageServiceImpl ```java package io.mykit.limiter.service.impl; import io.mykit.limiter.service.MessageService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; /** * @author binghe * @version 1.0.0 * @description 模擬實現傳送訊息 */ @Service public class MessageServiceImpl implements MessageService { private final Logger logger = LoggerFactory.getLogger(MessageServiceImpl.class); @Override public boolean sendMessage(String message) { logger.info("傳送訊息成功===>>" + message); return true; } } ``` * PayServiceImpl ```java package io.mykit.limiter.service.impl; import io.mykit.limiter.service.PayService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import java.math.BigDecimal; /** * @author binghe * @version 1.0.0 * @description 模擬支付 */ @Service public class PayServiceImpl implements PayService { private final Logger logger = LoggerFactory.getLogger(PayServiceImpl.class); @Override public int pay(BigDecimal price) { logger.info("支付成功===>>" + price); return 1; } } ``` 由於是模擬支付和傳送訊息,所以,我在具體實現的方法中打印出了相關的日誌,並沒有實現具體的業務邏輯。 接下來,就是建立我們的Controller類PayController,在PayController類的介面pay()方法中使用了限流,每秒鐘向桶中放入2個令牌,並且客戶端從桶中獲取令牌,如果在500毫秒內沒有獲取到令牌的話,我們可以則直接走服務降級處理。 PayController的程式碼如下所示。 ```java package io.mykit.limiter.controller; import com.google.common.util.concurrent.RateLimiter; import io.mykit.limiter.service.MessageService; import io.mykit.limiter.service.PayService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.math.BigDecimal; import java.util.concurrent.TimeUnit; /** * @author binghe * @version 1.0.0 * @description 測試介面限流 */ @RestController public class PayController { private final Logger logger = LoggerFactory.getLogger(PayController.class); /** * RateLimiter的create()方法中傳入一個引數,表示以固定的速率2r/s,即以每秒2個令牌的速率向桶中放入令牌 */ private RateLimiter rateLimiter = RateLimiter.create(2); @Autowired private MessageService messageService; @Autowired private PayService payService; @RequestMapping("/boot/pay") public String pay(){ //記錄返回介面 String result = ""; //限流處理,客戶端請求從桶中獲取令牌,如果在500毫秒沒有獲取到令牌,則直接走服務降級處理 boolean tryAcquire = rateLimiter.tryAcquire(500, TimeUnit.MILLISECONDS); if (!tryAcquire){ result = "請求過多,降級處理"; logger.info(result); return result; } int ret = payService.pay(BigDecimal.valueOf(100.0)); if(ret > 0){ result = "支付成功"; return result; } result = "支付失敗,再試一次吧..."; return result; } } ``` 最後,我們來建立mykit-ratelimiter-test專案的核心啟動類,如下所示。 ```java package io.mykit.limiter; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * @author binghe * @version 1.0.0 * @description 專案啟動類 */ @SpringBootApplication public class MykitLimiterApplication { public static void main(String[] args){ SpringApplication.run(MykitLimiterApplication.class, args); } } ``` 至此,我們不使用註解方式實現限流的Web應用就基本完成了。 ### 執行專案 專案建立完成後,我們來執行專案,執行SpringBoot專案比較簡單,直接執行MykitLimiterApplication類的main()方法即可。 專案執行成功後,我們在瀏覽器位址列輸入連結:http://localhost:8080/boot/pay。頁面會輸出“支付成功”的字樣,說明專案搭建成功了。如下所示。 ![](https://img-blog.csdnimg.cn/20200730003953908.png#pic_center) 此時,我只訪問了一次,並沒有觸發限流。接下來,我們不停的刷瀏覽器,此時,瀏覽器會輸出“支付失敗,再試一次吧...”的字樣,如下所示。 ![](https://img-blog.csdnimg.cn/20200730004004789.png#pic_center) 在PayController類中還有一個sendMessage()方法,模擬的是傳送訊息的介面,同樣使用了限流操作,具體程式碼如下所示。 ```java @RequestMapping("/boot/send/message") public String sendMessage(){ //記錄返回介面 String result = ""; //限流處理,客戶端請求從桶中獲取令牌,如果在500毫秒沒有獲取到令牌,則直接走服務降級處理 boolean tryAcquire = rateLimiter.tryAcquire(500, TimeUnit.MILLISECONDS); if (!tryAcquire){ result = "請求過多,降級處理"; logger.info(result); return result; } boolean flag = messageService.sendMessage("恭喜您成長值+1"); if (flag){ result = "訊息傳送成功"; return result; } result = "訊息傳送失敗,再試一次吧..."; return result; } ``` sendMessage()方法的程式碼邏輯和執行效果與pay()方法相同,我就不再瀏覽器訪問 http://localhost:8080/boot/send/message 地址的訪問效果了,小夥伴們可以自行驗證。 ### 不使用註解實現限流缺點 通過對專案的編寫,我們可以發現,當在專案中對介面進行限流時,不使用註解進行開發,會導致程式碼出現大量冗餘,每個方法中幾乎都要寫一段相同的限流邏輯,程式碼十分冗餘。 如何解決程式碼冗餘的問題呢?我們可以使用自定義註解進行實現。 ## 使用註解實現介面限流 使用自定義註解,我們可以將一些通用的業務邏輯封裝到註解的切面中,在需要添加註解業務邏輯的方法上加上相應的註解即可。針對我們這個限流的例項來說,可以基於自定義註解實現。 ### 實現自定義註解 實現,我們來建立一個自定義註解,如下所示。 ```java package io.mykit.limiter.annotation; import java.lang.annotation.*; /** * @author binghe * @version 1.0.0 * @description 實現限流的自定義註解 */ @Target(value = ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface MyRateLimiter { //向令牌桶放入令牌的速率 double rate(); //從令牌桶獲取令牌的超時時間 long timeout() default 0; } ``` ### 自定義註解切面實現 接下來,我們還要實現一個切面類MyRateLimiterAspect,如下所示。 ```java package io.mykit.limiter.aspect; import com.google.common.util.concurrent.RateLimiter; import io.mykit.limiter.annotation.MyRateLimiter; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.util.concurrent.TimeUnit; /** * @author binghe * @version 1.0.0 * @description 一般限流切面類 */ @Aspect @Component public class MyRateLimiterAspect { private RateLimiter rateLimiter = RateLimiter.create(2); @Pointcut("execution(public * io.mykit.limiter.controller.*.*(..))") public void pointcut(){ } /** * 核心切面方法 */ @Around("pointcut()") public Object process(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{ MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature(); //使用反射獲取方法上是否存在@MyRateLimiter註解 MyRateLimiter myRateLimiter = signature.getMethod().getDeclaredAnnotation(MyRateLimiter.class); if(myRateLimiter == null){ //程式正常執行,執行目標方法 return proceedingJoinPoint.proceed(); } //獲取註解上的引數 //獲取配置的速率 double rate = myRateLimiter.rate(); //獲取客戶端等待令牌的時間 long timeout = myRateLimiter.timeout(); //設定限流速率 rateLimiter.setRate(rate); //判斷客戶端獲取令牌是否超時 boolean tryAcquire = rateLimiter.tryAcquire(timeout, TimeUnit.MILLISECONDS); if(!tryAcquire){ //服務降級 fullback(); return null; } //獲取到令牌,直接執行 return proceedingJoinPoint.proceed(); } /** * 降級處理 */ private void fullback() { response.setHeader("Content-type", "text/html;charset=UTF-8"); PrintWriter writer = null; try { writer = response.getWriter(); writer.println("出錯了,重試一次試試?"); writer.flush();; } catch (IOException e) { e.printStackTrace(); }finally { if(writer != null){ writer.close(); } } } } ``` 自定義切面的功能比較簡單,我就不細說了,大家有啥問題可以關注【冰河技術】微信公眾號來進行提問。 接下來,我們改造下PayController類中的sendMessage()方法,修改後的方法片段程式碼如下所示。 ```java @MyRateLimiter(rate = 1.0, timeout = 500) @RequestMapping("/boot/send/message") public String sendMessage(){ //記錄返回介面 String result = ""; boolean flag = messageService.sendMessage("恭喜您成長值+1"); if (flag){ result = "訊息傳送成功"; return result; } result = "訊息傳送失敗,再試一次吧..."; return result; } ``` ### 執行部署專案 部署專案比較簡單,只需要執行MykitLimiterApplication類下的main()方法即可。這裡,為了簡單,我們還是從瀏覽器中直接輸入連結地址來進行訪問 效果如下所示。 ![](https://img-blog.csdnimg.cn/20200730004025440.png#pic_center) 接下來,我們不斷的重新整理瀏覽器。會出現“訊息傳送失敗,再試一次吧..”的字樣,說明已經觸發限流操作。 ![](https://img-blog.csdnimg.cn/20200730004035949.png#pic_center) ## 基於限流演算法實現限流的缺點 上面介紹的限流方式都只能用於單機部署的環境中,如果將應用部署到多臺伺服器進行分散式、叢集,則上面限流的方式就不適用了,此時,我們需要使用分散式限流。至於在分散式場景下,如何實現限流操作,我們就在下一篇中進行介紹。 ## 重磅福利 關注「 **冰河技術** 」微信公眾號,後臺回覆 “**設計模式**” 關鍵字領取《**深入淺出Java 23種設計模式**》PDF文件。回覆“**Java8**”關鍵字領取《**Java8新特性教程**》PDF文件。兩本PDF均是由冰河原創並整理的超硬核教程,面試必備!! **好了,今天就聊到這兒吧!別忘了點個贊,給個在看和轉發,讓更多的人看到,一起學習,一起進步!!** ## 寫在最後 > 如果你覺得冰河寫的還不錯,請微信搜尋並關注「 **冰河技術** 」微信公眾號,跟冰河學習高併發、分散式、微服務、大資料、網際網路和雲原生技術,「 **冰河技術** 」微信公眾號更新了大量技術專題,每一篇技術文章乾貨滿滿!不少讀者已經通過閱讀「 **冰河技術** 」微信公眾號文章,吊打面試官,成功跳槽到大廠;也有不少讀者實現了技術上的飛躍,成為公司的技術骨幹!如果你也想像他們一樣提升自己的能力,實現技術能力的飛躍,進大廠,升職加薪,那就關注「 **冰河技術** 」微信公眾號吧,每天更新超硬核技術乾貨,讓你對如何提升技術能力不再迷茫! ![](https://img-blog.csdnimg.cn/20200716220443647.png#pic_