一个问题往往会引出了一连串的问题,知识的盲区就这样被自己悄悄的发现了。车辙在自己动手写限流注解时,遇到的问题那是真一个比一个多:
什么是限流
对服务器接收到的请求作出限制,只有一部分请求能真正到达服务器,其他的请求可以延迟,也可以拒绝。从而避免所有请求到数据库,打垮 DB。
举个生活中大家可能遇到的场景,特别是北上广深或者新一线城市,杭州一号线地铁,凤起路站,在客流量到达一定峰值时,警察叔叔♀可能就不让你进地铁,让使用其他交通工具了️。。。都是泪啊!
限流算法用哪个比较合适
关于限流算法,网上的解释一大堆,漏桶算法,令牌桶算法等等,百度一下,你就知道,在这里车辙用最简单的计数器算法作为实现。
计数器算法
如何用注解实现限流
在用 Nginx 限流时,是将 nginx 作为代理层拦截请求处理,那么在 Spring 中代理层就是 AOP 啦。
AOP
在 web 服务器中,有很多场景都是可以靠 AOP 实现的,比如:
定时任务
在计数器算法中我们提到,每隔 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。即最近最常使用:
那么我们需要用哪种数据结构实现这中条件的队列呢?
GET
在上述的这种场景下,明显底层是数组的集合如 ArrayList 是不适用的。别说你这想不通哈。。
那就只剩下链表了如 LinkedList,但是 LinedList 查询时需要遍历链表。如果我们在存入 LinkedList 的同时,同样存入 map,那是不是就行了。当然。。。。不是啦,这个 map 有个要求,node 需要保存上一个节点,这样在查到值的同时,获取前一个节点,就可以在链表中删除对应的节点了。
PUT
经过 Get 的铺垫,这个不用说了吧!最终结果是 LinedHashMap。LinkedHashMap 的具体车辙这边就不逼逼了,还是自己看历史文章吧!
这边不考虑并发导致的线程不安全哈,只是一个参考~~ 讲了大半天,大家应该还是有些会看不明白的,请下方留言。没办法,语文差啊。