12.02 註解式限流是如何實現的?

一個問題往往會引出了一連串的問題,知識的盲區就這樣被自己悄悄的發現了。車轍在自己動手寫限流注解時,遇到的問題那是真一個比一個多:

  1. 限流算法用哪個比較合適。
  2. 如何用註解實現限流。
  3. 如何對每個方法單獨限流。
  4. 長字符串如何轉換成短字符串。
  5. 64 進制 or 62進制。
  6. LRU 是什麼,如何用簡單的數據結構實現。

什麼是限流

對服務器接收到的請求作出限制,只有一部分請求能真正到達服務器,其他的請求可以延遲,也可以拒絕。從而避免所有請求到數據庫,打垮 DB。

舉個生活中大家可能遇到的場景,特別是北上廣深或者新一線城市,杭州一號線地鐵,鳳起路站,在客流量到達一定峰值時,警察叔叔‍♀可能就不讓你進地鐵,讓使用其他交通工具了️。。。都是淚啊!

限流算法用哪個比較合適

關於限流算法,網上的解釋一大堆,漏桶算法,令牌桶算法等等,百度一下,你就知道,在這裡車轍用最簡單的計數器算法作為實現。

計數器算法

  1. 將一秒鐘分為 10 個階段,每個階段 100ms。
  2. 每隔 100ms 記錄下接口調用的次數。
  3. 當然隨著時間的流逝,階段會越來越多。這時候可以將最前面的 n 個階段刪除,只保留 10 個,也就是隻剩 1s。
  4. 最後一個減去第一個的次數,就是 1s 中內該接口調用的次數。
註解式限流是如何實現的?


如何用註解實現限流

在用 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> countMap = new ConcurrentHashMap<>();
3
4 /** 每個key 對應的linkedlist**/
5 private static Map<string>> 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 }
/<long>/<string>/<string>

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 }
/<long>

方法

考慮到定時任務的頻率不能太小,因此我們的定時任務是每秒鐘執行一次,這裡我們需要設置 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 的具體車轍這邊就不逼逼了,還是自己看歷史文章吧!

結尾

這邊不考慮併發導致的線程不安全哈,只是一個參考~~ 講了大半天,大家應該還是有些會看不明白的,請下方留言。沒辦法,語文差啊。


分享到:


相關文章: