使用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();
}