导入依赖
<dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>30.1.1-jre</version></dependency>
自定义注解
import java.lang.annotation.*;//表示该注解被用于方法上@Target(ElementType.METHOD)//表示该注解保留到运行时@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface RateLimit {/*** 该注解实际作用,限流* rate 代表每秒钟多少请求** @return*/int rate() default 100;/*** 平滑突发限流(SmoothBursty)和平滑预热限流(SmoothWarmingUp)* 平滑突发限流,允许突发流量,后面请求速率控制会趋于平稳* 平滑预热限流,开始先于小于最大速率执行,慢慢趋于平稳,接近最大速率** @return*/String mode() default RateLimitMode.SMOOTH_BURSTY;}
自定义注解的切面处理
import com.google.common.util.concurrent.RateLimiter;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.Signature;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;import org.aspectj.lang.reflect.MethodSignature;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.core.annotation.Order;import org.springframework.stereotype.Component;import java.util.Objects;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.TimeUnit;@Aspect@Component@Order(100)public class RateLimitAspect {/*** Guava RateLimiter提供的令牌桶算法可用于平滑突发限流(SmoothBursty)和平滑预热限流(SmoothWarmingUp)实现。* 设计思路----------------------------* 进入切面后,先进入环绕通知,环绕通知里取出注解限定的速度,并以该速度生产令牌放入桶中,(令牌生产速度为 每秒多少个),在ConcurrentHashMap中以类名+方法名作为key,以RateLimit作为value* * before执行时先去桶中获取令牌,获取不到阻塞一段时间。*/private static Logger LOGGER = LoggerFactory.getLogger(RateLimitAspect.class);public static final int PERMITS_PER_SECOND = 100;private static ConcurrentHashMap<String, RateLimiter> RateLimiterMap = new ConcurrentHashMap<>();/*** 切入点*/@Pointcut("@annotation(com.example.aop.RateLimit)")public void rateLimitCutPoint() {LOGGER.info("编入切入点");}@Before("rateLimitCutPoint()")public void rateLimitBefore(JoinPoint joinPoint) {String methodName = joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName();RateLimiter rateLimiter = RateLimiterMap.getOrDefault(methodName, RateLimiter.create(PERMITS_PER_SECOND));//获取令牌并计算等待时间 此处可以根据waitTime超过阈值就抛出异常(配合全局异常捕获封装指定返回值,提示接口已经限流了)double waitTime = rateLimiter.acquire();LOGGER.info("acquire spend {} seconds", waitTime);}/*** 环绕通知必须有返回值, 返回值即为目标方法的返回值** @param proceedingJoinPoint* @throws Throwable*/@Around("rateLimitCutPoint()")public Object rateLimitAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {try {Signature signature = proceedingJoinPoint.getSignature();//返回进行方法的全名称String methodName = proceedingJoinPoint.getTarget().getClass().getName() + "." + signature.getName();RateLimit rateLimitAnnotation = ((MethodSignature) signature).getMethod().getDeclaredAnnotation(RateLimit.class);//注解上的速率int rate = rateLimitAnnotation.rate();String mode = rateLimitAnnotation.mode();putRate(methodName, rate, mode);LOGGER.info("限流方法名{},执行速率{}", methodName, RateLimiterMap.get(methodName).getRate());} catch (Exception e) {LOGGER.error("限流切面出现异常,异常提示{}", e.getMessage());}return proceedingJoinPoint.proceed();}private void putRate(String methodName, int rate, String mode) {if (Objects.isNull(RateLimiterMap.get(methodName))) {if (mode.equals(RateLimitMode.SMOOTH_BURSTY)) {//SmoothBursty模式下,未使用RateLimit最多保存1秒的令牌,即maxBurstSeconds为1秒RateLimiterMap.putIfAbsent(methodName, RateLimiter.create(rate));} else {//SmoothWarmingUp模式// warmupPeriod表示从冷启动速率过渡到平均速率所需要的时间间隔,设置为1分钟RateLimiterMap.putIfAbsent(methodName, RateLimiter.create(rate, 1, TimeUnit.MINUTES));}}}}
使用到的常量
public class RateLimitMode {/*** 平滑突发限流(SmoothBursty)和平滑预热限流(SmoothWarmingUp)* 平滑突发限流,允许突发流量,后面请求速率控制会趋于平稳* 平滑预热限流,开始先于小于最大速率执行,慢慢趋于平稳,接近最大速率** @return*/public static final String SMOOTH_BURSTY = "smooth-bursty";public static final String SMOOTH_WARMING_UP = "smooth-warming-up";}
测试接口
@GetMapping("limit")@RateLimit(rate = 50)public String test2() {return "访问成功";}
Jmeter测试结果
- 当 @RateLimit(rate = 50),接口吞吐量(throughput)=47
 - 当 @RateLimit(rate = 10),接口吞吐量(throughput)=9.7
 - 当 @RateLimit(rate = 5),接口吞吐量(throughput)=5
 
限流成功
