@[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();
}
}
使用方式
查看結果 (這裡使用了自定義異常)