实战案例:如何防止超预期的高并发流量压垮系统?实战接口限流防护!代码已上传!

在互联网应用中,高并发系统会面临一个重大的挑战,那就是大量流高并发访问,比如:天猫的双十一、京东618、秒杀、抢购促销等,这些都是典型的大流量高并发场景。

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.7.3.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>1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.62.63.64.65.66.67.68.69.70.71.72.73.74.75.76.77.78.79.80.81.82.83.84.

可以看到,我在项目中除了引用了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); }1.2.3.4.5.6.7.8.9.10.
MessageService
复制
package io.mykit.limiter.service; /** * @author binghe * @version 1.0.0 * @description 模拟发送消息服务 */ public interface MessageService { boolean sendMessage(String message); }1.2.3.4.5.6.7.8.9.

接下来,创建二者的实现类,分别如下。

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 publicclass MessageServiceImpl implements MessageService { privatefinal Logger logger = LoggerFactory.getLogger(MessageServiceImpl.class); @Override public boolean sendMessage(String message) { logger.info("发送消息成功===>>" + message); returntrue; } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.
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 publicclass PayServiceImpl implements PayService { privatefinal Logger logger = LoggerFactory.getLogger(PayServiceImpl.class); @Override public int pay(BigDecimal price) { logger.info("支付成功===>>" + price); return1; } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.

由于是模拟支付和发送消息,所以,我在具体实现的方法中打印出了相关的日志,并没有实现具体的业务逻辑。

接下来,就是创建我们的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 publicclass PayController { privatefinal 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; } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.

最后,我们来创建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); } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.

至此,我们不使用注解方式实现限流的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; }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.

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; }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.
自定义注解切面实现

接下来,我们还要实现一个切面类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 publicclass 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(); returnnull; } //获取到令牌,直接执行 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(); } } } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.62.63.64.65.66.67.68.69.70.71.72.73.74.75.76.77.78.79.80.81.82.83.84.85.86.

接下来,我们改造下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; }1.2.3.4.5.6.7.8.9.10.11.12.13.
运行部署项目

部署项目比较简单,只需要运行MykitLimiterApplication类下的main()方法即可。这里,为了简单,我们还是从浏览器中直接输入链接地址来进行访问。

效果如下所示。

接下来,我们不断的刷新浏览器。会出现“消息发送失败,再试一次吧..”的字样,说明已经触发限流操作。

基于限流算法实现限流的缺点

上面介绍的限流方式都只能用于单机部署的环境中,如果将应用部署到多台服务器进行分布式、集群,则上面限流的方式就不适用了,此时,我们需要使用分布式限流。至于在分布式场景下,如何实现限流操作,我们就在下一篇中进行介绍。

THE END