【高并发】亿级流量场景下如何为HTTP接口限流?看完我懂了!!

org.springframework.boot

spring-boot-starter-aop

com.google.guava

guava

${guava.version}

org.apache.maven.plugins

maven-compiler-plugin

3.1

${java.version}

${java.version}

可以看到,我在项目中除了引用了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){

总结:心得体会

既然选择这个行业,选择了做一个程序员,也就明白只有不断学习,积累实战经验才有资格往上走,拿高薪,为自己,为父母,为以后的家能有一定的经济保障。

学习时间都是自己挤出来的,短时间或许很难看到效果,一旦坚持下来了,必然会有所改变。不如好好想想自己为什么想进入这个行业,给自己内心一个答案。

面试大厂,最重要的就是夯实的基础,不然面试官随便一问你就凉了;其次会问一些技术原理,还会看你对知识掌握的广度,最重要的还是你的思路,这是面试官比较看重的。

最后,上面这些大厂面试真题都是非常好的学习资料,通过这些面试真题能够看看自己对技术知识掌握的大概情况,从而能够给自己定一个学习方向。包括上面分享到的学习指南,你都可以从学习指南里理顺学习路线,避免低效学习。

大厂Java架构核心笔记(适合中高级程序员阅读):

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){

总结:心得体会

既然选择这个行业,选择了做一个程序员,也就明白只有不断学习,积累实战经验才有资格往上走,拿高薪,为自己,为父母,为以后的家能有一定的经济保障。

学习时间都是自己挤出来的,短时间或许很难看到效果,一旦坚持下来了,必然会有所改变。不如好好想想自己为什么想进入这个行业,给自己内心一个答案。

面试大厂,最重要的就是夯实的基础,不然面试官随便一问你就凉了;其次会问一些技术原理,还会看你对知识掌握的广度,最重要的还是你的思路,这是面试官比较看重的。

最后,上面这些大厂面试真题都是非常好的学习资料,通过这些面试真题能够看看自己对技术知识掌握的大概情况,从而能够给自己定一个学习方向。包括上面分享到的学习指南,你都可以从学习指南里理顺学习路线,避免低效学习。

大厂Java架构核心笔记(适合中高级程序员阅读):

[外链图片转存中…(img-oXm9hAWq-1721166083921)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值