一個問題每每會引出了一連串的問題,知識的盲區就這樣被本身悄悄的發現了🤣。 車轍在本身動手寫限流注解
時,遇到的問題那是真一個比一個多:java
or
62進制 對服務器接收到的請求做出限制,只有一部分請求能真正到達服務器,其餘的請求能夠延遲,也能夠拒絕。從而避免全部請求到數據庫,打垮DB。
舉個生活中你們可能遇到的場景,特別是北上廣深或者新一線城市,杭州一號線地鐵,鳳起路站,在客流量到達必定峯值時,警察叔叔👮♀可能就不讓你進地鐵,讓使用其餘交通工具了️。。。都是淚啊node
關於限流算法,網上的解釋一大堆,漏桶算法,令牌桶算法等等,百度一下,你就知道
,在這裏車轍用最簡單的計數器算法做爲實現。nginx
在用nginx
限流時,是將nginx
做爲代理層攔截請求,處理,那麼在Spring
中代理層就是AOP
啦web
在web服務器中,有不少場景都是能夠靠AOP實現的,好比redis
在計數器算法中咱們提到,每隔100ms須要記錄接口調用的次數,並保存。這時候定時任務就派上用場了。
定時任務的實現有不少,像利用線程池的ScheduledExecutorService
,固然Spring
的Scheduled
也莫得問題。
其次,用什麼數據結構保存調用次數 -->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(); } }); } 複製代碼
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("成功執行"); } 複製代碼
上述咱們將package + className + methodName
做爲惟一key,致使key的長度變得特別長,咱們是否是該想個辦法下降key的長度。
有些同窗會想到壓縮,但這根本是不現實的,具體緣由見連接。
這也不能用,那也不能用,還讓不讓人活了🥺。你們有沒有想到平時收到的短信,有時候會存在一個短連接,這些短鏈接其實就是用的發號器--> 從某個服務中獲取惟一的自增id,而後將這個id進行轉化。好比這時候自增到100000了,那麼將100000從十進制轉化爲62進制q0U
。這個和短信上的連接很類似不是嗎?數組
既然是自增的,那麼相同的長字符經過調用服務轉化成的短字符串都是不一樣的。在某些業務場景,可能調用比較頻繁,就須要作kv存儲。否則也沒有必要作存儲了,多作多錯嘛~安全
假設咱們須要作kv存儲,童鞋們能想到的大概也就是jvm
內存或者redis
了。由於這個對應關係通常是不會長久存儲的,一般在某個熱點事件中做爲查詢。若是是redis
,能夠設置過時時間做爲驅逐。那麼在jvm
內存中,咱們須要考慮到的是LRU
。即最近最常使用bash
在上述的這種場景下,明顯底層是數組的集合如ArrayList是不適用的。別說你這想不通哈。。
那就只剩下鏈表瞭如LinkedList,可是LinedList查詢時須要遍歷鏈表。若是咱們在存入LinkedList的同時,一樣存入map,那是否是就好了。固然。。。。不是啦,這個map
有個要求,node
須要保存上一個節點。這樣在查到值的同時,獲取前一個節點,就能夠在鏈表中刪除對應的節點了
通過Get
的鋪墊,這個不用說了吧
LinkedHashMap
的具體車轍這邊就不逼逼了,仍是百度一下,你就知道
這邊不考慮併發致使的線程不安全哈,只是一個參考~~~ 講了大半天,你們應該仍是有些會看不明白的,請下方留言。沒辦法,語文差啊😂。