使用guava和redis兩種方式來實現限流器
1. redis方式
redis方式主要是靠incr這個操作,通過過期時間和遞增數來判斷是否允許通過請求。
public void apply(String key, int limitCount, int limitPeriod, String descName) {
double limitPerSec = limitCount * 1.0 / limitPeriod;
Long n = redisManager.incr(key);
if (limitPerSec < 1){
//如果qps小於1,不限制單位時間,限制單位時間的數量爲1個,如qps=0.1,就是10s通過1個
if (n == 1L) {
//加上過期時間
redisManager.expire(key, limitPeriod);
} else if (n >= 1) {
log.info("限制訪問key爲 {},描述爲 [{}] 的接口",key, descName);
throw new LimitAccessException("接口訪問超出頻率限制");
}
}else{
//如果qps大於等於1,則反過來,不限制單位時間的數量,而是限制單位時間爲1s,如qps=5,就是1s通過5個
if (n == 1L) {
//加上過期時間
redisManager.expire(key, 1);
} else if (n >= limitCount) {
log.info("限制訪問key爲 {},描述爲 [{}] 的接口",key, descName);
throw new LimitAccessException("接口訪問超出頻率限制");
}
}
}
- 不足之處
使用redis來實現有個問題,就是不好保證精確的限流速度。
2. guava限流器
guava是谷歌提供的一套框架,我們這裏需要用到的是它的限流器:
Limiter和本地緩存Cache。這兩個都不在這裏介紹了,大家可以自行百度。
// 根據key分不同的令牌桶, 每3分鐘自動清理緩存
private static Cache<String, RateLimiter> caches = CacheBuilder.newBuilder()
//在訪問後1分鐘清除
.expireAfterAccess(1, TimeUnit.SECONDS)
//最大值,超過這個值會清除掉最近沒使用到的緩存
.maximumSize(1024)
.build();
@Override
public void apply(String key, int limitCount, int limitPeriod, String descName) {
double limitPerSec = limitCount * 1.0 / limitPeriod;
RateLimiter limiter = null;
try {
limiter = caches.get(key, () -> RateLimiter.create(limitPerSec));
//1秒沒獲取到
} catch (ExecutionException e) {
log.error("獲取限流出錯: ",e);
}
if (limiter != null){
boolean b = limiter.tryAcquire(1, TimeUnit.SECONDS);
if (!b){
log.info("限制訪問key爲 {},描述爲 [{}] 的接口",key, descName);
throw new LimitAccessException("接口訪問超出頻率限制");
}
}
}
RateLimiter,我們指定一個qps的值,請求來需要acquire獲取令牌,直到令牌重新填充纔得到放行。tryAcquire方法的話,可以指定一個等待時間,並返回一個Boolea值。
- 不足之處
但是這裏有個不足就是所有的請求進來都是調用acquire。無法根據ip或者其他的類型關鍵字來區分。
所以我們引入了緩存,類似HashMap,針對不同的關鍵字生成不同的限流器
3. 兩者整合切換
如果我們想靈活切換兩種方式,即可按下面的配置實現
yml配置
limiter:
type: redis # redis或guava
aop註解
需要給哪個方法加限制,在方法上加這個註解即可
import java.lang.annotation.*;
/**
* @description: 限流注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface RateLimit {
// 資源名稱,用於描述接口功能
String name() default "";
// 資源 key
String key() default "";
// key prefix
String prefix() default "";
// 時間的,單位秒
int period() default 60;
// 限制訪問次數
int count() default 60;
// 限制類型
LimitType limitType() default LimitType.IP_AND_KEY;
enum LimitType {
/**
* 根據ip來作爲限流的根據
*/
IP,
/**
* 根據key來,不填默認以類名+方法名爲key
*/
KEY,
/**
* 同時根據ip和key來限流
*/
IP_AND_KEY
}
}
aop切面
在這個切面中,使用我們的限流器處理,需要更改切入點的註解的類路徑
@Aspect
@ConditionalOnProperty(name = "limiter.type")
@Component
@Slf4j
public class RateLimitAspect {
private final static String KEY_PREFIX = "limit";
@Autowired
private IRateLimiter rateLimiter;
//更換註解的class路徑
@Pointcut("@annotation(com.xxx.RateLimit)")
public void pointcut() {
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
RateLimit limitAnnotation = method.getAnnotation(RateLimit.class);
int limitPeriod = limitAnnotation.period();
int limitCount = limitAnnotation.count();
String name = limitAnnotation.name();
//獲取限流的key
String key = getKey(request, signature);
String redisKey = StringUtils.joinWith(":", KEY_PREFIX,limitAnnotation.prefix(), key,
HashUtil.md5Hex(request.getRequestedSessionId() == null ? "":request.getRequestedSessionId()));
rateLimiter.apply(redisKey,limitCount,limitPeriod,name);
return point.proceed();
}
/**
* 獲取限流的key
*
* @param request
* @param signature
* @return
*/
private String getKey(HttpServletRequest request, MethodSignature signature) {
Method method = signature.getMethod();
RateLimit limitAnnotation = method.getAnnotation(RateLimit.class);
RateLimit.LimitType limitType = limitAnnotation.limitType();
String key;
String customerKey = limitAnnotation.key();
if (StringUtils.isEmpty(customerKey)) {
//獲取類名
String className = signature.getClass().getSimpleName();
//獲取方法名
String methodName = method.getName();
customerKey = className + "@" + methodName;
}
switch (limitType) {
case IP:
key = IPUtils.getIpAddr(request);
break;
case KEY:
key = customerKey;
break;
case IP_AND_KEY:
key = IPUtils.getIpAddr(request) + "-" + customerKey;
break;
default:
key = "";
}
return key.replace(":",".");
}
}
兩個限流器,實現一個接口
/**
* IRateLimitService
*
* @author zgd
* @date 2020/1/2 17:50
*/
public interface IRateLimiter {
void apply(String key, int limitCount, int limitPeriod, String descName);
}
redis的
/**
* RedisRateLimitServiceImpl
*
* @author zgd
* @date 2020/1/2 17:50
*/
@ConditionalOnBean(RedisManager.class)
@ConditionalOnProperty(prefix = "limiter",name = "type",havingValue = "redis")
@Slf4j
@Component
public class RedisRateLimiter implements IRateLimiter {
@Autowired
//使用項目中的redis客戶端即可
private RedisManager redisManager;
@Override
public void apply(String key, int limitCount, int limitPeriod, String descName) {
double limitPerSec = limitCount * 1.0 / limitPeriod;
Long n = redisManager.incr(key);
if (limitPerSec < 1){
//如果qps小於1,不限制單位時間,限制單位時間的數量爲1個,如qps=0.1,就是10s通過1個
if (n == 1L) {
//加上過期時間
redisManager.expire(key, limitPeriod);
} else if (n >= 1) {
log.info("限制訪問key爲 {},描述爲 [{}] 的接口",key, descName);
throw new LimitAccessException("接口訪問超出頻率限制");
}
}else{
//如果qps大於等於1,則反過來,不限制單位時間的數量,而是限制單位時間爲1s,如qps=5,就是1s通過5個
if (n == 1L) {
//加上過期時間
redisManager.expire(key, 1);
} else if (n >= limitCount) {
log.info("限制訪問key爲 {},描述爲 [{}] 的接口",key, descName);
throw new LimitAccessException("接口訪問超出頻率限制");
}
}
}
}
guava的:
/**
* GuavaRateLimit
*
* @author zgd
* @date 2020/1/2 17:53
*/
@ConditionalOnProperty(prefix = "limiter", name = "type",havingValue = "guava")
@Component
@Slf4j
public class GuavaRateLimiter implements IRateLimiter {
// 根據key分不同的令牌桶, 每3分鐘自動清理緩存
private static Cache<String, RateLimiter> caches = CacheBuilder.newBuilder()
//在訪問後1分鐘清除
.expireAfterAccess(1, TimeUnit.SECONDS)
//最大值,超過這個值會清除掉最近沒使用到的緩存
.maximumSize(1024)
.build();
@Override
public void apply(String key, int limitCount, int limitPeriod, String descName) {
double limitPerSec = limitCount * 1.0 / limitPeriod;
RateLimiter limiter = null;
try {
limiter = caches.get(key, () -> RateLimiter.create(limitPerSec));
//1秒沒獲取到
} catch (ExecutionException e) {
log.error("獲取限流出錯: ",e);
}
if (limiter != null){
boolean b = limiter.tryAcquire(1, TimeUnit.SECONDS);
if (!b){
log.info("限制訪問key爲 {},描述爲 [{}] 的接口",key, descName);
throw new LimitAccessException("接口訪問超出頻率限制");
}
}
}
}
使用代碼:
@GetMapping("/test")
@ApiOperation("測試")
@RateLimit(name = "測試限流", prefix = "test", count = 30, period = 60)
public R testHttp() {
return R.ok(en.toString();
}