註解式限流是如何實現滴

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

  一個問題每每會引出了一連串的問題,知識的盲區就這樣被本身悄悄的發現了🤣。 車轍在本身動手寫限流注解時,遇到的問題那是真一個比一個多: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的具體車轍這邊就不逼逼了,仍是百度一下,你就知道

結尾

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

相關文章
相關標籤/搜索