一個問題往往會引出了一連串的問題,知識的盲區(qū)就這樣被自己悄悄的發(fā)現(xiàn)了。車轍在自己動手寫限流注解時,遇到的問題那是真一個比一個多:
- 限流算法用哪個比較合適。
- 如何用注解實現(xiàn)限流。
- 如何對每個方法單獨限流。
- 長字符串如何轉換成短字符串。
- 64 進制 or 62進制。
- LRU 是什么,如何用簡單的數(shù)據結構實現(xiàn)。
什么是限流
對服務器接收到的請求作出限制,只有一部分請求能真正到達服務器,其他的請求可以延遲,也可以拒絕。從而避免所有請求到數(shù)據庫,打垮 DB。
舉個生活中大家可能遇到的場景,特別是北上廣深或者新一線城市,杭州一號線地鐵,鳳起路站,在客流量到達一定峰值時,警察叔叔♀可能就不讓你進地鐵,讓使用其他交通工具了?。。。都是淚啊!
限流算法用哪個比較合適
關于限流算法,網上的解釋一大堆,漏桶算法,令牌桶算法等等,百度一下,你就知道,在這里車轍用最簡單的計數(shù)器算法作為實現(xiàn)。
計數(shù)器算法
- 將一秒鐘分為 10 個階段,每個階段 100ms。
- 每隔 100ms 記錄下接口調用的次數(shù)。
- 當然隨著時間的流逝,階段會越來越多。這時候可以將最前面的 n 個階段刪除,只保留 10 個,也就是只剩 1s。
- 最后一個減去第一個的次數(shù),就是 1s 中內該接口調用的次數(shù)。

如何用注解實現(xiàn)限流
在用 Nginx 限流時,是將 nginx 作為代理層攔截請求處理,那么在 Spring 中代理層就是 AOP 啦。
AOP
在 web 服務器中,有很多場景都是可以靠 AOP 實現(xiàn)的,比如:
- 打印日志,記錄時間類,方法,參數(shù)。
- 利用反射設置分頁 PageRow、PageNum 的默認值。
- 游戲場景,判斷游戲是否已經結束,不用每個方法都去判斷。
- 解密,驗簽等等。
定時任務
在計數(shù)器算法中我們提到,每隔 100ms 需要記錄接口調用的次數(shù),并保存。這時候定時任務就派上用場了。
定時任務的實現(xiàn)有很多,像利用線程池的 ScheduledExecutorService,當然 Spring 的 Scheduled 也莫得問題。
其次,用什么數(shù)據結構保存調用次數(shù) --> LinkedList。
另外,我們需要對多個方法限流,該如何解決呢?--> 每個方法都有唯一對應的值: package + class + methodName,于是我們將這個唯一值作為key,linkedList 作為 map,下方代碼:
1 /** 每個key 對應的調用次數(shù)**/ 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 # 每個方法的調用次數(shù)放入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 /** 調用次數(shù)判斷是否已經超過注解設置的值 **/ 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 }
方法考慮到定時任務的頻率不能太小,因此我們的定時任務是每秒鐘執(zhí)行一次,這里我們需要設置 10s 鐘的限流值,導致粒度變大了。
1@CalLimitAnno(count = 1000) 2 public void testPageAnno(){ 3 System.out.println("成功執(zhí)行"); 4 }
Map 優(yōu)化
上述我們將 package + className + methodName 作為唯一 key,導致 key 的長度變得特別長,我們是不是該想個辦法降低 key 的長度。
大家有沒有想到平時收到的短信,有時候會存在一個短鏈接,這些短連接其實就是用的發(fā)號器 --> 從某個服務中獲取唯一的自增id,然后將這個 id 進行轉化。比如這時候自增到 100000 了,那么將 100000 從十進制轉化為 62 進制 q0U。這個和短信上的鏈接很相似不是嗎?
Map 持久化
既然是自增的,那么相同的長字符通過調用服務轉化成的短字符串都是不同的。在某些業(yè)務場景,可能調用比較頻繁,就需要做kv存儲。不然也沒有必要做存儲了,多做多錯嘛~
kv 存儲優(yōu)化
假設我們需要做 kv 存儲,童鞋們能想到的大概也就是 jvm 內存或者 redis 了。因為這個對應關系一般是不會長久存儲的,通常在某個熱點事件中作為查詢。如果是 redis,可以設置過期時間作為驅逐。那么在 jvm 內存中,我們需要考慮到的是 LRU。即最近最常使用:
- 使用過的 key 需要放到隊列的隊首。
- 最不經常使用的一旦超過隊列限制的長度,需要將其刪除。
那么我們需要用哪種數(shù)據結構實現(xiàn)這中條件的隊列呢?
GET
- 假設這個 key 不存在,那么返回 null。
- 假設 key 存在,需要返回值的同時,需要將對應的 key 刪除,并且將 key 放到隊首。
在上述的這種場景下,明顯底層是數(shù)組的集合如 ArrayList 是不適用的。別說你這想不通哈。。
那就只剩下鏈表了如 LinkedList,但是 LinedList 查詢時需要遍歷鏈表。如果我們在存入 LinkedList 的同時,同樣存入 map,那是不是就行了。當然。。。。不是啦,這個 map 有個要求,node 需要保存上一個節(jié)點,這樣在查到值的同時,獲取前一個節(jié)點,就可以在鏈表中刪除對應的節(jié)點了。
PUT
- 假設 key 不存在,放入隊首。
- 假設 key 存在,刪除這個 key,同時放到隊首。
經過 Get 的鋪墊,這個不用說了吧!最終結果是 LinedHashMap。LinkedHashMap 的具體車轍這邊就不逼逼了,還是自己看歷史文章吧!
結尾
這邊不考慮并發(fā)導致的線程不安全哈,只是一個參考~~ 講了大半天,大家應該還是有些會看不明白的,請下方留言。沒辦法,語文差啊。