@[toc]
## 为什么要接口限流
- 在我们项目开发过程中,有些接口是暴露在用户的常用中,包括一些高危接口,如 (支付,开发票,订单),这些接口 都是高危接口,且被用户经常使用,在高并发的情况下,io阻塞,不可避免的出现重复提交,或者点击频繁的操作,所以我们就要加入限流,避免用户多次点击,减少我们接口的压力,把整数据不会重复,接口压力减小
为什么要做分布式
- 在我们做项目负载均衡的时候, 分布式,微服务架构的时候,不可避免的多个节点,这个时候我们就要考虑会被随机分配到各个节点,如果 我们使用 令牌桶 或者 漏斗桶 算法到话,存到 本地,各个节点不会共享,所以
我们要考虑模块,节点间的共享
实现方式
1. 算法实现(无分布式,单体架构,单节点)
- 自定义注解
package com.yxl.annotation;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* <p>
* 限流注解,
* </p>
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter {
int NOT_LIMITED = 0;
/**
* qps
*/
@AliasFor("qps") double value() default NOT_LIMITED;
/**
* qps
*/
@AliasFor("value") double qps() default NOT_LIMITED;
/**
* 超时时长
*/
int timeout() default 0;
/**
* 超时时间单位
*/
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
}
- AOP实现切面 + 令牌桶算法实现
package com.yxl.aspect;
import com.yxl.annotation.RateLimiter;
import lombok.extern.slf4j.Slf4j;
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.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* <p>
* 限流切面
* </p>
*
* @author yxl
* @date Created in 2019/9/12 14:27
*/
@Slf4j
@Aspect
@Component
public class RateLimiterAspect {
private static final ConcurrentMap<String, com.google.common.util.concurrent.RateLimiter> RATE_LIMITER_CACHE = new ConcurrentHashMap<>();
@Pointcut("@annotation(com.yxl.annotation.RateLimiter)")
public void rateLimit() {
}
@Around("rateLimit()")
public Object pointcut(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
// 通过 AnnotationUtils.findAnnotation 获取 RateLimiter 注解
RateLimiter rateLimiter = AnnotationUtils.findAnnotation(method, RateLimiter.class);
if (rateLimiter != null && rateLimiter.qps() > RateLimiter.NOT_LIMITED) {
double qps = rateLimiter.qps();
if (RATE_LIMITER_CACHE.get(method.getName()) == null) {
// 初始化 QPS
RATE_LIMITER_CACHE.put(method.getName(), com.google.common.util.concurrent.RateLimiter.create(qps));
}
log.debug("【{}】的QPS设置为: {}", method.getName(), RATE_LIMITER_CACHE.get(method.getName()).getRate());
// 尝试获取令牌
if (RATE_LIMITER_CACHE.get(method.getName()) != null && !RATE_LIMITER_CACHE.get(method.getName()).tryAcquire(rateLimiter.timeout(), rateLimiter.timeUnit())) {
throw new RuntimeException("手速太快了,慢点儿吧~");
}
}
return point.proceed();
}
}
使用方式
查看结果(这里使用了自定义异常)
2. 分布式实现
package com.yxzapp.annotation;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* <p>
* 限流注解,
* </p>
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter {
int NOT_LIMITED = 0;
/**
* 类名
* @return
*/
String className() default "";
/**
* qps
*/
@AliasFor("qps") double value() default NOT_LIMITED;
/**
* qps
*/
@AliasFor("value") double qps() default NOT_LIMITED;
/**
* 限流时间
*/
int timeout() default 0;
/**
* 超时时间单位
*/
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
}
使用 AOP + redis 实现
package com.yxzapp.aspect;
import com.yxzapp.annotation.RateLimiter;
import com.yxzapp.commons.constant.MessageConstant;
import com.yxzapp.exception.BizException;
import com.yxzapp.modules.sys.entity.SysUser;
import com.yxzapp.utils.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
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.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* <p>
* 限流切面
* </p>
*
* @author yxl
* @date 2020/6/19
*/
@Slf4j
@Aspect
@Component
public class RateLimiterAspect {
@Autowired
private RedisUtils redisUtils;
@Pointcut("@annotation(com.yxzapp.annotation.RateLimiter)")
public void rateLimit() {
}
@Around("rateLimit()")
public Object pointcut(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
Class aClass = signature.getClass();
// 获取方法上的@RateLimiter注解
RateLimiter rateLimiter = AnnotationUtils.findAnnotation(method, RateLimiter.class);
if (rateLimiter != null && rateLimiter.qps() > RateLimiter.NOT_LIMITED) {
//获取qps
double qps = rateLimiter.qps();
String key = "RateLimiter:" rateLimiter.className() + +':'+ method.getName();
if(!redisUtils.hasKey(key)){
redisUtils.setMillisecond(key,rateLimiter.qps(),rateLimiter.timeout());
}else if(redisUtils.get(key) != null) {
throw new BizException(MessageConstant.MSG_STATUS,"手速太快了,慢点儿吧~");
}
log.debug("【{}】的QPS设置为: {}", key, redisUtils.get(key));
}
return point.proceed();
}
}
使用方式
查看结果 (这里使用了自定义异常)