注解式限流是如何实现的?

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

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

什么是限流

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

举个生活中大家可能遇到的场景,特别是北上广深或者新一线城市,杭州一号线地铁,凤起路站,在客流量到达一定峰值时,警察叔叔👮‍♀可能就不让你进地铁,让使用其他交通工具了️。。。都是泪啊!

限流算法用哪个比较合适

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

计数器算法

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

如何用注解实现限流

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

AOP

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

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

定时任务

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

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

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

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

 1    /** 每个key 对应的调用次数**/
 2    private Map<String, Long> countMap = new ConcurrentHashMap<>();
 3
 4    /** 每个key 对应的linkedlist**/
 5    private static Map<String, LinkedList<Long>> calListMap = new ConcurrentHashMap<>();
 6
 7    ## 每s一次查询
 8    @Scheduled(cron = "*/1 * * * * ?")
 9    private void timeGet(){
10        countMap.forEach((k,v)->{
11            LinkedList<Long> calList = calListMap.get(k);
12            if(calList == null){
13                calList = new LinkedList<>();
14            }
15            # 每个方法的调用次数放入linkedList中
16            calList.addLast(v);
17            calListMap.put(k, calList);
18
19            if (calList.size() > 10) {
20                calList.removeFirst();
21            }
22        });
23    }

AOP 检查

定义注解:
 1import java.lang.annotation.*;
 2
 3
 4@Target(ElementType.METHOD)
 5@Retention(RetentionPolicy.RUNTIME)
 6@Documented
 7public @interface CalLimitAnno {
 8
 9    String value() default "" ;
10
11    String methodName() default "" ;
12
13    long count() default 100;
14}

调用接口前检查:

 1@Around(value = "@annotation(around)")
 2    public Object initBean(ProceedingJoinPoint point, CalLimitAnno around) throws Throwable {
 3        /** 获取类名和方法名 **/
 4        MethodSignature signature = (MethodSignature) point.getSignature();
 5        Method method = signature.getMethod();
 6        String[] classNameArray = method.getDeclaringClass().getName().split("\\.");
 7        String methodName = classNameArray[classNameArray.length - 1] + "." + method.getName();
 8        String classZ = signature.getDeclaringTypeName();
 9        String countMapKey =  classZ + "|" + methodName;
10
11
12        LinkedList<Long> calList = calListMap.get(countMapKey);
13        if(calList != null){
14            /** 调用次数判断是否已经超过注解设置的值 **/
15            if ((calList.peekLast() - calList.peekFirst()) > Long.valueOf(around.count())) {
16                throw new RuntimeException("被限流了");
17            }
18            /** 存放**/
19            countMap.putIfAbsent(countMapKey,0L);
20            countMap.put(countMapKey,countMap.get(countMapKey) + 1);
21        }
22        Object object = point.proceed();
23        return object;
24    }

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

1@CalLimitAnno(count = 1000)
2    public void testPageAnno(){
3        System.out.println("成功执行");
4    }

Map 优化

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

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

Map 持久化

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

kv 存储优化

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

  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 的具体车辙这边就不逼逼了,还是自己看历史文章吧!

结尾

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

欢迎关注公众号:老男孩的成长之路

11561958-2866fee3666c8625.jpg
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值