寫在前面
在互聯網應用中,高並發系統會面臨一個重大的挑戰,那就是大量流高並發訪問,比如:天貓的雙十一、京東618、秒殺、搶購促銷等,這些都是典型的大流量高並發場景。關於秒殺,小伙伴們可以參見我的另一篇文章《【高並發】高並發秒殺系統架構解密,不是所有的秒殺都是秒殺!》
關於【冰河技術】微信公眾號,解鎖更多【高並發】專題文章。
注意:由於原文篇幅比較長,所以被拆分為:理論、算法、實戰(HTTP接口實戰+分布式限流實戰)三大部分。
理論篇:《【高並發】如何實現億級流量下的分布式限流?這些理論你必須掌握!!》
算法篇:《【高並發】如何實現億級流量下的分布式限流?這些算法你必須掌握!!》
項目源碼已提交到github:https://github.com/sunshinelyz/mykit-ratelimiter
HTTP接口限流實戰
這里,我們實現Web接口限流,具體方式為:使用自定義注解封裝基於令牌桶限流算法實現接口限流。
不使用注解實現接口限流
搭建項目
這里,我們使用SpringBoot項目來搭建Http接口限流項目,SpringBoot項目本質上還是一個Maven項目。所以,小伙伴們可以直接創建一個Maven項目,我這里的項目名稱為mykit-ratelimiter-test。接下來,在pom.xml文件中添加如下依賴使項目構建為一個SpringBoot項目。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>io.mykit.limiter</groupId>
<artifactId>mykit-ratelimiter-test</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>mykit-ratelimiter-test</name>
<properties>
<guava.version>28.2-jre</guava.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version><!--$NO-MVN-MAN-VER$-->
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
</plugins>
</build>
可以看到,我在項目中除了引用了SpringBoot相關的Jar包外,還引用了guava框架,版本為28.2-jre。
創建核心類
這里,我主要是模擬一個支付接口的限流場景。首先,我們定義一個PayService接口和MessageService接口。PayService接口主要用於模擬后續的支付業務,MessageService接口模擬發送消息。接口的定義分別如下所示。
- PayService
package io.mykit.limiter.service;
import java.math.BigDecimal;
/**
* @author binghe
* @version 1.0.0
* @description 模擬支付
*/
public interface PayService {
int pay(BigDecimal price);
}
- MessageService
package io.mykit.limiter.service;
/**
* @author binghe
* @version 1.0.0
* @description 模擬發送消息服務
*/
public interface MessageService {
boolean sendMessage(String message);
}
接下來,創建二者的實現類,分別如下。
- MessageServiceImpl
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
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的代碼如下所示。
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項目的核心啟動類,如下所示。
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。頁面會輸出“支付成功”的字樣,說明項目搭建成功了。如下所示。
此時,我只訪問了一次,並沒有觸發限流。接下來,我們不停的刷瀏覽器,此時,瀏覽器會輸出“支付失敗,再試一次吧...”的字樣,如下所示。
在PayController類中還有一個sendMessage()方法,模擬的是發送消息的接口,同樣使用了限流操作,具體代碼如下所示。
@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 地址的訪問效果了,小伙伴們可以自行驗證。
不使用注解實現限流缺點
通過對項目的編寫,我們可以發現,當在項目中對接口進行限流時,不使用注解進行開發,會導致代碼出現大量冗余,每個方法中幾乎都要寫一段相同的限流邏輯,代碼十分冗余。
如何解決代碼冗余的問題呢?我們可以使用自定義注解進行實現。
使用注解實現接口限流
使用自定義注解,我們可以將一些通用的業務邏輯封裝到注解的切面中,在需要添加注解業務邏輯的方法上加上相應的注解即可。針對我們這個限流的實例來說,可以基於自定義注解實現。
實現自定義注解
實現,我們來創建一個自定義注解,如下所示。
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,如下所示。
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()方法,修改后的方法片段代碼如下所示。
@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()方法即可。這里,為了簡單,我們還是從瀏覽器中直接輸入鏈接地址來進行訪問
效果如下所示。
接下來,我們不斷的刷新瀏覽器。會出現“消息發送失敗,再試一次吧..”的字樣,說明已經觸發限流操作。
基於限流算法實現限流的缺點
上面介紹的限流方式都只能用於單機部署的環境中,如果將應用部署到多台服務器進行分布式、集群,則上面限流的方式就不適用了,此時,我們需要使用分布式限流。至於在分布式場景下,如何實現限流操作,我們就在下一篇中進行介紹。
重磅福利
關注「 冰河技術 」微信公眾號,后台回復 “設計模式” 關鍵字領取《深入淺出Java 23種設計模式》PDF文檔。回復“Java8”關鍵字領取《Java8新特性教程》PDF文檔。兩本PDF均是由冰河原創並整理的超硬核教程,面試必備!!
好了,今天就聊到這兒吧!別忘了點個贊,給個在看和轉發,讓更多的人看到,一起學習,一起進步!!
寫在最后
如果你覺得冰河寫的還不錯,請微信搜索並關注「 冰河技術 」微信公眾號,跟冰河學習高並發、分布式、微服務、大數據、互聯網和雲原生技術,「 冰河技術 」微信公眾號更新了大量技術專題,每一篇技術文章干貨滿滿!不少讀者已經通過閱讀「 冰河技術 」微信公眾號文章,吊打面試官,成功跳槽到大廠;也有不少讀者實現了技術上的飛躍,成為公司的技術骨干!如果你也想像他們一樣提升自己的能力,實現技術能力的飛躍,進大廠,升職加薪,那就關注「 冰河技術 」微信公眾號吧,每天更新超硬核技術干貨,讓你對如何提升技術能力不再迷茫!