现在的位置: 首页 > 算法 > 正文

注解式限流是如何实现的

2020年01月11日 算法 ⁄ 共 3281字 ⁄ 字号 评论关闭

  什么是限流?对服务器接收到的请求作出限制,只有一部分请求能真正到达服务器,其他的请求可以延迟,也可以拒绝。从而避免所有请求到数据库,打垮 DB。

  限流算法用哪个比较合适

  关于限流算法,网上的解释一大堆,漏桶算法,令牌桶算法等等,百度一下,你就知道,在这里车辙用最简单的计数器算法作为实现。

  计数器算法

  将一秒钟分为 10 个阶段,每个阶段 100ms。

  每隔 100ms 记录下接口调用的次数。

  当然随着时间的流逝,阶段会越来越多。这时候可以将最前面的 n 个阶段删除,只保留 10 个,也就是只剩 1s。

  最后一个减去第一个的次数,就是 1s 中内该接口调用的次数。

  如何用注解实现限流

  在用 nginx 限流时,是将 nginx 作为代理层拦截请求处理,那么在 Spring 中代理层就是 AOP 啦。

  AOP

  在 web 服务器中,有很多场景都是可以靠 AOP 实现的,比如:

  打印日志,记录时间类,方法,参数。

  利用反射设置分页 PageRow、PageNum 的默认值。

  游戏场景,判断游戏是否已经结束,不用每个方法都去判断。

  解密,验签等等。

  定时任务

  在计数器算法中我们提到,每隔 100ms 需要记录接口调用的次数,并保存。这时候定时任务就派上用场了。

  定时任务的实现有很多,像利用线程池的 ScheduledExecutorService,当然 Spring 的 Scheduled 也莫得问题。

  其次,用什么数据结构保存调用次数 --> LinkedList。

  另外,我们需要对多个方法限流,该如何解决呢?--> 每个方法都有唯一对应的值: package + class + methodName,于是我们将这个唯一值作为key,linkedList 作为 map,下方代码:

  /** 每个key 对应的调用次数**/

  private Map countMap = new ConcurrentHashMap<>();

  /** 每个key 对应的linkedlist**/

  private static Map> calListMap = new ConcurrentHashMap<>();

  ## 每s一次查询

  @Scheduled(cron = "*/1 * * * * ?")

  private void timeGet(){

  countMap.forEach((k,v)->{

  LinkedList calList = calListMap.get(k);

  if(calList == null){

  calList = new LinkedList<>();

  }

  # 每个方法的调用次数放入linkedList中

  calList.addLast(v);

  calListMap.put(k, calList);

  if (calList.size() > 10) {

  calList.removeFirst();

  }

  });

  }

  AOP 检查

  定义注解:

  import java.lang.annotation.*;

  @Target(ElementType.METHOD)

  @Retention(RetentionPolicy.RUNTIME)

  @Documented

  public @interface CalLimitAnno {

  String value() default "" ;

  String methodName() default "" ;

  long count() default 100;

  }

  调用接口前检查:

  @Around(value = "@annotation(around)")

  public Object initBean(ProceedingJoinPoint point, CalLimitAnno around) throws Throwable {

  /** 获取类名和方法名 **/

  MethodSignature signature = (MethodSignature) point.getSignature();

  Method method = signature.getMethod();

  String[] classNameArray = method.getDeclaringClass().getName().split("\\.");

  String methodName = classNameArray[classNameArray.length - 1] + "." + method.getName();

  String classZ = signature.getDeclaringTypeName();

  String countMapKey = classZ + "|" + methodName;

  LinkedList calList = calListMap.get(countMapKey);

  if(calList != null){

  /** 调用次数判断是否已经超过注解设置的值 **/

  if ((calList.peekLast() - calList.peekFirst()) > Long.valueOf(around.count())) {

  throw new RuntimeException("被限流了");

  }

  /** 存放**/

  countMap.putIfAbsent(countMapKey,0L);

  countMap.put(countMapKey,countMap.get(countMapKey) + 1);

  }

  Object object = point.proceed();

  return object;

  }

  方法考虑到定时任务的频率不能太小,因此我们的定时任务是每秒钟执行一次,这里我们需要设置 10s 钟的限流值,导致粒度变大了。

  @CalLimitAnno(count = 1000)

  public void testPageAnno(){

  System.out.println("成功执行");

  }

  Map 优化

  上述我们将 package + className + methodName 作为唯一 key,导致 key 的长度变得特别长,我们是不是该想个办法降低 key 的长度。

  大家有没有想到平时收到的短信,有时候会存在一个短链接,这些短连接其实就是用的发号器 --> 从某个服务中获取唯一的自增id,然后将这个 id 进行转化。比如这时候自增到 100000 了,那么将 100000 从十进制转化为 62 进制 q0U。这个和短信上的链接很相似不是吗?

  Map 持久化

  既然是自增的,那么相同的长字符通过调用服务转化成的短字符串都是不同的。在某些业务场景,可能调用比较频繁,就需要做kv存储。不然也没有必要做存储了,多做多错嘛~

  kv 存储优化

  假设我们需要做 kv 存储,童鞋们能想到的大概也就是 jvm 内存或者 redis 了。因为这个对应关系一般是不会长久存储的,通常在某个热点事件中作为查询。如果是 redis,可以设置过期时间作为驱逐。那么在 jvm 内存中,我们需要考虑到的是 LRU。即最近最常使用:

  使用过的 key 需要放到队列的队首。

  最不经常使用的一旦超过队列限制的长度,需要将其删除。

  那么我们需要用哪种数据结构实现这中条件的队列呢?

  GET

  假设这个 key 不存在,那么返回 null。

  假设 key 存在,需要返回值的同时,需要将对应的 key 删除,并且将 key 放到队首。

  在上述的这种场景下,明显底层是数组的集合如 ArrayList 是不适用的。别说你这想不通哈。。

  那就只剩下链表了如 LinkedList,但是 LinedList 查询时需要遍历链表。如果我们在存入 LinkedList 的同时,同样存入 map,那是不是就行了。当然。。。。不是啦,这个 map 有个要求,node 需要保存上一个节点,这样在查到值的同时,获取前一个节点,就可以在链表中删除对应的节点了。

  PUT

  假设 key 不存在,放入队首。

  假设 key 存在,删除这个 key,同时放到队首。

  经过 Get 的铺垫,这个不用说了吧!最终结果是 LinedHashMap。

抱歉!评论已关闭.