使用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();
  }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章