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