注解式限流是如何实现滴

知道的越多,不知道的越多

  一个问题每每会引出了一连串的问题,知识的盲区就这样被本身悄悄的发现了🤣。 车辙在本身动手写限流注解时,遇到的问题那是真一个比一个多:java

  1. 限流算法用哪一个比较合适
  2. 如何用注解实现限流
  3. 如何对每一个方法单独限流
  4. 长字符串如何转换成短字符串
  5. 64进制or62进制
  6. LRU是什么,如何用简单的数据结构实现

实践

什么是限流

  对服务器接收到的请求做出限制,只有一部分请求能真正到达服务器,其余的请求能够延迟,也能够拒绝。从而避免全部请求到数据库,打垮DB。
  举个生活中你们可能遇到的场景,特别是北上广深或者新一线城市,杭州一号线地铁,凤起路站,在客流量到达必定峰值时,警察叔叔👮‍♀可能就不让你进地铁,让使用其余交通工具了️。。。都是泪啊node

限流算法用哪一个比较合适

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

计数器算法

  1. 将一秒钟分为10个阶段,每一个阶段100ms。
  2. 每隔100ms记录下接口调用的次数。
  3. 固然随着时间的流逝,阶段会愈来愈多。这时候能够将最前面的n个阶段删除,只保留10个,也就是只剩1s。
  4. 最后一个减去第一个的次数,就是1s中内该接口调用的次数

如何用注解实现限流

  在用nginx限流时,是将nginx做为代理层拦截请求,处理,那么在Spring中代理层就是AOPweb

AOP

在web服务器中,有不少场景都是能够靠AOP实现的,好比redis

  1. 打印日志,记录时间类,方法,参数
  2. 利用反射设置分页PageRow,PageNum的默认值
  3. 游戏场景,判断游戏是否已经结束,不用每一个方法都去判断
  4. 解密,验签等等

定时任务

  在计数器算法中咱们提到,每隔100ms须要记录接口调用的次数,并保存。这时候定时任务就派上用场了。
  定时任务的实现有不少,像利用线程池的ScheduledExecutorService,固然SpringScheduled也莫得问题。
  其次,用什么数据结构保存调用次数 -->LinkedList。
  另外,咱们须要对多个方法限流,该如何解决呢?-->每一个方法都有惟一对应的值: package + class + methodName,因而咱们将这个惟一值做为key,linkedList做为map,下方代码算法

/** 每一个key 对应的调用次数**/
    private Map<String, Long> countMap = new ConcurrentHashMap<>();
    
    /** 每一个key 对应的linkedlist**/
    private static Map<String, LinkedList<Long>> calListMap = new ConcurrentHashMap<>();

    ## 每s一次查询
    @Scheduled(cron = "*/1 * * * * ?")
    private void timeGet(){
        countMap.forEach((k,v)->{
            LinkedList<Long> 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<Long> 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。即最近最常使用bash

  1. 使用过的key须要放到队列的队首
  2. 最不常用的一旦超过队列限制的长度,须要将其删除。
    那么咱们须要用哪一种数据结构实现这中条件的队列呢?

GET

  1. 假设这个key不存在,那么返回null
  2. 假设key存在,须要返回值的同时,须要将对应的key删除,而且将key放到队首。

  在上述的这种场景下,明显底层是数组的集合如ArrayList是不适用的。别说你这想不通哈。。
  那就只剩下链表了如LinkedList,可是LinedList查询时须要遍历链表。若是咱们在存入LinkedList的同时,一样存入map,那是否是就好了。固然。。。。不是啦,这个map有个要求,node须要保存上一个节点。这样在查到值的同时,获取前一个节点,就能够在链表中删除对应的节点了

PUT

  1. 假设key不存在,放入队首
  2. 假设key存在,删除这个key,同时放到队首

通过Get的铺垫,这个不用说了吧

最终结果:LinedHashMap

LinkedHashMap的具体车辙这边就不逼逼了,仍是百度一下,你就知道

结尾

  这边不考虑并发致使的线程不安全哈,只是一个参考~~~   讲了大半天,你们应该仍是有些会看不明白的,请下方留言。没办法,语文差啊😂。

相关文章
相关标签/搜索