Java面經整理
資料結構
幾種常見的資料結構
樹、圖、鏈表、優先佇列(堆)、跳表(鏈表+多級索引)
跳表(redis的zset使用跳表):
跳表的查詢的時間復雜度為 O(logn),因為找到位置之后插入和洗掉的時間復雜度很低,為 O(1),所以最終插入和洗掉的時間復雜度也為 O(logn) 洗掉操作: * 如果這個結點在索引中也有出現,我們除了要洗掉原始鏈表中的結點,還要洗掉索引中的, * 同時我們洗掉節點時需要獲得前驅節點(雙向鏈表除外) 插入操作 * 插入元素過多,可能導致兩個索引間節點過多,效率降低,我們需要維護索引與原始鏈表的大小平衡,, * 跳表是通過一個隨機函式來維護這個平衡的,當我們向跳表中插入資料時,我們可以選擇同時把這個資料插入到索引里,那我們插入到哪一級的索引呢,這就需要隨機函式,來決定我們插入到哪一級的索引中,這樣可以很有效的防止跳表退化,而造成效率變低,優先佇列實作:
// 清楚大頂堆、小頂堆定義 // Java PriorityQueue 通過陣列實作 // 插入元素 從末尾開始找 private void siftUpComparable(int k, E x) { // k代表當前元素個數、x為需要插入的元素 Comparable<? super E> key = (Comparable<? super E>) x; while (k > 0) { // 找到父節點下標 int parent = (k - 1) >>> 1; Object e = queue[parent]; // 如果是小頂堆 則判斷 x >= queue[parent] 如果符合說明找到了需要插入的位置 break // 進行插入該節點,并將該parent節點放入index為k的位置 if (key.compareTo((E) e) >= 0) break; // 否則 將該節點下降 并令k為parent繼續回圈 queue[k] = e; k = parent; } // 插入到找到的位置 queue[k] = key; } // poll方法 移除最小元素 // 傳入index為size的元素 此時根節點為空 k從0開始 // 即index為k的元素被取走后,拿出陣列最后的元素x 不斷尋找位置進行插入 如果不滿足 會將空節點的子節點放入空節點 然后對其子節點進行繼續操作直到找到能插入的位置或者回圈結束 private void siftDownComparable(int k, E x) { // x為堆中最后的元素 Comparable<? super E> key = (Comparable<? super E>)x; int half = size >>> 1; // loop while a non-leaf while (k < half) { // x 從根節點開始 每次和 index為2k+1和2k+2中更小的元素進行比較 int child = (k << 1) + 1; // assume left child is least Object c = queue[child]; int right = child + 1; if (right < size && ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0) // 小頂堆:c為2k+1和2k+2中較小的一個 也就是k對應節點的左右節點中小的那個 c = queue[child = right]; // 小頂堆:如果x比較c大,我們就和c交換位置 否則我們的數就應該在k位置上,k從0開始, if (key.compareTo((E) c) <= 0) break; // 交換位置 queue[k] = c; k = child; } // 找到了位置 queue[k] = key; } // remove方法 移除指定元素 E removeAt(int i) { // i為指定元素的index 是通過回圈遍歷得到的 // assert i >= 0 && i < size; modCount++; int s = --size; // s為最后一個元素的下標 if (s == i) // removed last element queue[i] = null; else { // 將s對應的節點也就是最后一個節點拿到 需要進行樹的修復 E moved = (E) queue[s]; // 將其置為null queue[s] = null; // 以queue[i]為根節點的樹(堆) 進行樹的修復 // 小頂堆:將moved元素作為上文的x元素 和上文的poll方法類似 // 只是此處不是k=0開始,而是k=i開始 因為移除的是index=i的元素 siftDown(i, moved); // 此時以queue[i]為根節點的樹(堆)已經修復完成了 if (queue[i] == moved) { // 如果moved在i位置上 說明moved元素較小 還可以繼續向上調整 siftUp(i, moved); // 向上調整 ???? if (queue[i] != moved) return moved; } } return null; }
判斷鏈表是否存在環
快慢指標判斷是否重合(如果需要找到入環點,在第一次相遇后一指標指向頭部,然都保持慢指標速度繼續前進,再次相遇的地方即為入環點)
二叉搜索樹、平衡二叉樹、紅黑樹
搜索二叉樹:左節點 < 根節點 < 右節點,且左子樹所有節點 < 根節點 < 右子樹所有節點
平衡二叉樹:它是一 棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,并且左右兩個子樹都是一棵平衡二叉樹,對于插入節點和洗掉節點,如果破壞了平衡,那么都需要進行旋轉以重新平衡,AVL樹的查找、插入、洗掉操作在平均和最壞的情況下都是O(logn),代碼可參考下方:
// 只會存在4種旋轉方式:左旋、右旋、先左再右、先右再左, // 遞回進行判斷即可 class Node { int val; Node left, right; // 構造方法... /* 左旋,右旋省略 */ public void leftRotate() { Node newNode = new Node(this.val); newNode.left = this.left; newNode.right = this.right.left; this.val = this.right.val; this.left = newNode; this.right = this.right.right; } /* 添加節點 */ public void add(Node node) { // 首先進行節點添加 if (node.val < this.lval) { if (this.left == null) this.left = node; else this.left.add(node); } else if (node.val > this.lval) { if (this.right == null) this.right = node; else this.right.add(node); } // 判斷是否需要旋轉 if (this.getLeftHeight() - this.getRightHeight() > 1) { // 獲得子樹高度的get方法 略 // 需要右旋 但需要先判斷左子樹是否需要先左旋 if (this.left != null && this.left.getLeftHeight() < this.left.getRightHeight()) { this.left.leftRotate(); } this.rightRotate(); } // 左旋同理... // 洗掉節點 /* 1、查找到對應節點 2、判斷該節點是否為葉節點,如果為葉節點,則直接洗掉 如果不是葉節點,則判斷該節點是否存在左右子樹 如果只有左子樹 則獲得左子樹的最大值leftMax 并將需要洗掉的節點的值置為leftMax 然后遞回洗掉左子樹中的該節點 如果只有右子樹 則獲得右子樹的最小值rigthMin 并將需要洗掉的節點的值置為rigthMin 然后遞回洗掉右子樹中的該節點 如果左右子樹均存在,也一樣獲得右子樹的最小值rigthMin 并將需要洗掉的節點的值置為rigthMin 然后遞回洗掉右子樹中的該節點 3、洗掉完成后 判斷是否平衡 即和上面add方法的后半部分類似 然后進行旋轉平衡接即可 */ } }紅黑樹:
- 任何一個節點都有顏色,黑色或者紅色
- 根節點是黑色的
- 空節點被認為是黑色的(即NIL結點)
- 每個紅色節點必須有兩個黑色的子節點,(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點,)
- 從任一節點到其每個葉子的所有簡單路徑都包含相同數目的黑色節點,(這也叫做完美黑色平衡,與2-3樹的完美平衡有些不同)
只有黑色節點的紅黑樹其實就是平衡二叉樹,我們將紅色點想成是填充節點,即每次插入都以紅色插入,且將黑色想成主要節點,紅色節點做填充,紅色節點的作用就是使得樹的高度更加靈活,不至于像平衡二叉樹那樣每次插入都需要 做平衡操作(減少了需要平衡的概率),**(紅色節點的子節點是黑色節點)**條件就使得高度受到限制,極限情況就是一個紅色一個黑色串聯,最多的查找次數 不會超過2倍的 最少的查找數,
紅黑樹其實就是 二叉查找樹和平衡二叉樹 兩者優缺點的一種折中,可以防止出現二叉查找樹那種極差的情況,也可以減少插入時平衡的次數,
// 旋轉操作和平衡樹保持一致 /* 插入操作有三種情況是不會破壞性質的 且插入的節點一定是紅色 1、插入節點作為根節點 2、插入節點作為根節點的子節點 3、插入節點的父結點是黑色的 如果會破壞性質 則說明不滿足以上情況 也就是說明 新節點必定存在祖父節點、新節點父節點為紅色 1、叔節點為紅色——重繪,父節點和叔節點繪制為黑色,祖父節點繪制為紅色 2、叔節點為黑色,祖-父-子節點在同側——將父繪黑,祖繪紅,然后以祖為樞紐進行向相反側的方向旋轉 3、叔節點為黑色,祖-父-子節點不在同側——先根據子父所在的那一側的方向,以父為樞向相反方向旋轉,這樣就實作了將子父祖異側轉換為了父子祖同側,第二步是發現它符合上一種情況了,那就將現在的父(原來的子)繪黑,祖(原來的祖)繪紅,然后再次旋轉, 注意:需要修復時,還需要遞回進行,即在一次修復后將指標指向此次操作的祖父節點 然后繼續判斷是否需要修復 最后仍然需要將根節點繪制為紅色, */![]()
線段樹、樹狀陣列
樹狀陣列是一種特殊的線段樹
B樹、B+樹
每個節點不止一個資料
B樹:每個節點上都存了key-data
B+樹:只有葉子節點存了data,其他節點可存更多key,且所有葉子節點之間存在順序指標關系,
紅黑樹和B+樹
在資料量較小時,不需要磁盤io,紅黑樹的性能高于b+樹
資料插入時,紅黑樹性能更好,
OS
開機程序
BIOS自檢->加載bootloader到記憶體->bootloader加載OS到記憶體->從OS起始位置開始執行指令
BIOS位于RAM;bootstrap位于磁盤第一個主引導扇區;OS位于磁盤
以x86為例,bootloader一般會被加載到于記憶體0x7c00處
記憶體管理
段頁式存盤、虛擬記憶體、系統抖動,
行程和執行緒區別,什么是協程
行程是資源分配的基本單位,執行緒是CPU調度的基本單位,一個行程中可包含多個執行緒,
協程是輕量級的用戶執行緒,一個執行緒可包含多個協程,只在用戶態運行,開銷小,
行程調度的方式(CFS?)
Linux的調度器類主要實作兩類行程調度演算法:實時調度演算法和完全公平調度演算法*(CFS)*,實時調度演算法SCHED_FIFO和SCHED_RR,按優先級執行,一般不會被搶占,直到實時行程執行完,才會執行普通行程,而大多數的普通行程,用的就是CFS演算法,
CFS 調度程式并不采用嚴格規則來為一個優先級分配某個長度的時間片,而是為每個任務分配一定比例的 CPU 處理時間,每個任務分配的具體比例是根據友好值來計算的,友好值的范圍從 -20 到 +19,數值較低的友好值表示較高的相對優先級,具有較低友好值的任務,與具有較高友好值的任務相比,會得到更高比例的處理器處理時間,默認友好值為 0,
友好一詞源自如下想法:當一個任務增加了它的友好值,如從 0 至 +10,該任務通過降低優先級,進而對其他任務更加友好,
CFS 沒有使用離散的時間片,而是采用目標延遲,這是每個可運行任務應當運行一次的時間間隔,根據目標延遲,按比例分配 CPU 時間,除了默認值和最小值外,隨著系統內的活動任務數量超過了一定閾值,目標延遲可以增加,
CFS 調度程式沒有直接分配優先級,相反,它通過每個任務的變數 vruntime 以便維護虛擬運行時間,進而記錄每個任務運行多久,虛擬運行時間與基于任務優先級的衰減因子有關,更低優先級的任務比更高優先級的任務具有更高衰減速率,對于正常優先級的任務(友好值為 0),虛擬運行時間與實際物理運行時間是相同的,
因此,如果一個默認優先級的任務運行 200ms,則它的虛擬運行時間也為 200ms,然而,如果一個較低優先級的任務運行 200ms,則它的虛擬運行時間將大于 200ms,同樣,如果一個更高優先級的任務運行 200ms,則它的虛擬運行時間將小于 200ms,當決定下步運行哪個任務時,調度程式只需選擇具有最小虛擬運行時間的任務,此外,一個更高優先級的任務如成為可運行,就會搶占低優先級任務,
下面分析一下 CFS 調度程式是如何作業的,假設有兩個任務,它們具有相同的友好值,一個任務是 I/O 密集型而另一個為 CPU 密集型,通常,I/O 密集型任務在運行很短時間后就會阻塞以便等待更多的 I/O;而 CPU 密集型任務只要有在處理器上運行的機會,就會用完它的時間片,
因此,I/O 密集型任務的虛擬運行時間最終將會小于 CPU 密集型任務的,從而使得 I/O 密集型任務具有更高的優先級,這時,如果 CPU 密集型任務在運行,而 I/O 密集型任務變得有資格可以運行(如該任務所等待的 I/O 已成為可用),那么 I/O 密集型任務就會搶占 CPU 密集型任務,
死鎖
兩個執行緒互相持有對方所需要的資源,互相等待,誰也無法繼續執行下去,
條件:互斥、持有并等待、不可搶占、回圈等待
處理方法:死鎖預防(破壞四個條件中的一個)、死鎖避免(銀行家)、死鎖檢測與消除
死鎖預防: 互斥-使得共享資源其他行程也能訪問,但會導致系統無法正常運行,無法確定執行 占用并等待-行程要么擁有所有資源要么不拿資源,但會導致資源利用率低且易導致饑餓 不搶占-行程可以搶占其他行程的資源,但和互斥條件沖突,只能將其他行程kill 回圈等待-對資源進行排序,要求每個行程按照資源的順序進行申請
行程、執行緒、協程的區別(詳細)
行程、執行緒,都是有內核進行調度,有 CPU 時間片的概念,進行 搶占式調度,
協程(用戶級執行緒)完全由用戶自己的程式進行調度(協作式調度),需要協程自己主動把控制權轉讓出去之后,其他協程才能被執行到,
協程,是在應用層模擬的執行緒,他避免了背景關系切換的額外耗費,兼顧了多執行緒的優點,簡化了高并發程式的復雜度,協程還是通過共享記憶體通訊.
目前的協程框架一般都是設計成 1:N 模式, 所謂 1:N 就是一個執行緒作為一個容器里面放置多個協程, 那么誰來適時的切換這些協程?答案是有協程自己主動讓出CPU, 也就是每個協程池里面有一個調度器,這個調度器是被動調度的, 意思就是他不會主動調度, 而且當一個協程發現自己執行不下去了(比如異步等待網路的資料回來,但是當前還沒有資料到), 這個時候就可以由這個協程通知調度器, 這個時候執行到調度器的代碼,調度器根據事先設計好的調度演算法找到當前最需要CPU的協程, 切換這個協程的CPU背景關系把CPU的運行權交個這個協程,直到這個協程出現執行不下去需要等等的情況, 或者它呼叫主動讓出CPU的API之類,觸發下一次調度,對的沒錯就是類似于 領匯入模式那么這個實作有沒有問題? 其實是有問題的,假設這個執行緒中有一個協程是CPU密集型的他沒有IO操作,也就是自己不會主動觸發調度器調度的程序, 那么就會出現其他協程得不到執行的情況,所以這種情況下需要程式員自己避免,
深拷貝、淺拷貝(java)
淺拷貝:只是增加了一個指標指向原記憶體,
深拷貝:深拷貝是增加了一個指標并且申請了一個新的記憶體,使這個增加的指標指向這個新的記憶體,
寫時復制
系統呼叫 fork() 創建了父行程的一個復制,以作為子行程,傳統上,fork() 為子行程創建一個父行程地址空間的副本,復制屬于父行程的頁面,然而,考慮到許多子行程在創建之后立即呼叫系統呼叫 exec(),父行程地址空間的復制可能沒有必要,
因此,可以采用一種稱為寫時復制的技術,它通過允許父行程和子行程最初共享相同的頁面來作業,這些共享頁面標記為寫時復制,這意味著如果任何一個行程寫入共享頁面,那么就創建共享頁面的副本,
當使用寫時復制技術時,僅復制任何一行程修改的頁面,所有未修改的頁面可以由父行程和子行程共享,
注意:采用 vfork(),父行程被掛起,子行程使用父行程的地址空間,因為 vfork() 不采用寫時復制,如果子行程修改父地址空間的任何頁面,那么這些修改過的頁面對于恢復的父行程是可見的,因此,應謹慎使用 vfork(),以確保子行程不會修改父行程的地址空間,
mmap記憶體映射
記憶體映射,簡而言之就是將用戶空間的一段記憶體區域映射到內核空間,映射成功后,用戶對這段記憶體區域的修改可以直接反映到內核空間,同樣,內核空間對這段區域的修改也直接反映用戶空間,那么對于內核空間<---->用戶空間兩者之間需要大量資料傳輸等操作的話效率是非常高的,
緩沖區溢位
例如堆疊溢位導致回傳值或者方法回傳地址被修改,進而導致被攻擊,
為何需要用戶態、內核態
應用程式不能直接訪問外設,需要內核在其中充當被信任的第三方,只有內核才能執行特權指令,同時也是方便應用程 序通過內核提供的介面來更方便撰寫操作外設的程式,即不用關注和外設具體打交道的細節,其應該由作業系統來完成,
read ahead檔案預讀
所謂預讀,是指檔案系統為應用程式一次讀出比預期更多的檔案內容并快取在page cache中,這樣下一次讀請求到來時部分頁面直接從page cache讀取即可,當然,這個細節對應用程式透明,應用程式可能的感覺就是下次讀的速度會更快
x86是大端還是小端,為什么
小端
select、poll、epoll
select:bitmap、遍歷bitmap獲取資料、bitmap不可重用
poll:結構體陣列(三個欄位存在狀態位)、遍歷判斷狀態位獲取資料、結構體陣列可重用
epoll:類似poll結構體(兩個欄位 不存在狀態位)、由內核對其進行排序并回傳數量,直接遍歷數量即可獲得socket
管道底層實作
無名管道:記憶體中實作(內核中的快取),如|,有名管道:磁盤實作,如mkfifo,只有父子行程之間可以通過匿名管道,
管道采用半雙工通信,使用一個管道一般的規則是讀管道資料的行程關閉管道寫入端,而寫管道行程關
閉其讀出端,管道傳輸的資料是無格式的且大小受限,
父子行程之間的匿名管道,因為子行程復制父行程創建的檔案描述符,所以各自擁有兩個fd[0]、fd[1],這樣就實作了行程間通信,
系統呼叫的具體程序,如何實作
應用程式主動向作業系統發起服務請求,應用程式請求作業系統提供服務,切換到內核態,內核態回應服務然后完成后回傳,
select、poll、epoll機制
select采用輪詢機制,耗時較大,且監聽的socket有限(可以改變)
select和poll只支持LT作業模式,epoll的默認的作業模式是LT模式,還支持ET(邊緣觸發)模式,
水平觸發:①對于讀操作,只要緩沖內容不為空,LT模式回傳讀就緒,②對于寫操作,只要緩沖區還不滿,LT模式會回傳寫就緒,
邊緣觸發:在ET模式下, 緩沖區從不可讀變成可讀,會喚醒應用行程,緩沖區資料變少的情況,則不會再喚醒應用行程
舉例1: 讀緩沖區剛開始是空的 讀緩沖區寫入2KB資料 水平觸發和邊緣觸發模式此時都會發出可讀信號 收到信號通知后,讀取了1KB的資料,讀緩沖區還剩余1KB資料 水平觸發會再次進行通知,而邊緣觸發不會再進行通知 舉例2:(以脈沖的高低電平為例) 水平觸發:0為無資料,1為有資料,緩沖區有資料則一直為1,則一直觸發, 邊緣觸發發:0為無資料,1為有資料,只要在0變到1的上升沿才觸發,
epoll紅黑樹
epoll和poll的一個很大的區別在于,poll每次呼叫時都會存在一個將pollfd結構體陣列中的每個結構體元素從用戶態向內核態中的一個鏈表節點拷貝的程序,而內核中的這個鏈表并不會一直保存,當poll運行一次就會重新執行一次上述的拷貝程序,這說明一個問題:poll并不會在內核中為要監聽的檔案描述符長久的維護一個資料結構來存放他們,而epoll內核中維護了一個內核事件表,它是將所有的檔案描述符全部都存放在內核中,系統去檢測有事件發生的時候觸發回呼,當你要添加新的檔案描述符的時候也是呼叫epoll_ctl函式使用EPOLL_CTL_ADD宏來插入,epoll_wait也不是每次呼叫時都會重新拷貝一遍所有的檔案描述符到內核態,當我現在要在內核中長久的維護一個資料結構來存放檔案描述符,并且時常會有插入,查找和洗掉的操作發生,這對內核的效率會產生不小的影響,因此需要一種插入,查找和洗掉效率都不錯的資料結構來存放這些檔案描述符,那么紅黑樹當然是不二的人選,
epoll與select、poll的對比
1. 用戶態將檔案描述符傳入內核的方式
select:創建3個檔案描述符集并拷貝到內核中,分別監聽讀、寫、例外動作,這里受到單個行程可以打開的fd數量限制,默認是1024,poll:將傳入的struct pollfd結構體陣列拷貝到內核中進行監聽,epoll:執行epoll_create會在內核的高速cache區中建立一顆紅黑樹以及就緒鏈表(該鏈表存盤已經就緒的檔案描述符),接著用戶執行的epoll_ctl函式添加檔案描述符會在紅黑樹上增加相應的結點,
2. 內核態檢測檔案描述符讀寫狀態的方式
select:采用輪詢方式,遍歷所有fd,最后回傳一個描述符讀寫操作是否就緒的mask掩碼,根據這個掩碼給fd_set賦值,poll:同樣采用輪詢方式,查詢每個fd的狀態,如果就緒則在等待佇列中加入一項并繼續遍歷,epoll:采用回呼機制,在執行epoll_ctl的add操作時,不僅將檔案描述符放到紅黑樹上,而且也注冊了回呼函式,內核在檢測到某檔案描述符可讀/可寫時會呼叫回呼函式,該回呼函式將檔案描述符放在就緒鏈表中,
3. 找到就緒的檔案描述符并傳遞給用戶態的方式
select:將之前傳入的fd_set拷貝傳出到用戶態并回傳就緒的檔案描述符總數,用戶態并不知道是哪些檔案描述符處于就緒態,需要遍歷來判斷,poll:將之前傳入的fd陣列拷貝傳出用戶態并回傳就緒的檔案描述符總數,用戶態并不知道是哪些檔案描述符處于就緒態,需要遍歷來判斷,epoll:epoll_wait只用觀察就緒鏈表中有無資料即可,最后將鏈表的資料回傳給陣列并回傳就緒的數量,內核將就緒的檔案描述符放在傳入的陣列中,所以只用遍歷依次處理即可,這里回傳的檔案描述符是通過mmap讓內核和用戶空間共享同一塊記憶體實作傳遞的,減少了不必要的拷貝,
4. 重復監聽的處理方式
select:將新的監聽檔案描述符集合拷貝傳入內核中,繼續以上步驟,poll:將新的struct pollfd結構體陣列拷貝傳入內核中,繼續以上步驟,epoll:無需重新構建紅黑樹,直接沿用已存在的即可,
epoll更高效的原因
select和poll的動作基本一致,只是poll采用鏈表來進行檔案描述符的存盤,而select采用fd標注位來存放,所以select會受到最大連接數的限制,而poll不會,select、poll、epoll雖然都會回傳就緒的檔案描述符數量,但是select和poll并不會明確指出是哪些檔案描述符就緒,而epoll會,造成的區別就是,系統呼叫回傳后,呼叫select和poll的程式需要遍歷監聽的整個檔案描述符找到是誰處于就緒,而epoll則直接處理即可,select、poll都需要將有關檔案描述符的資料結構拷貝進內核,最后再拷貝出來,而epoll創建的有關檔案描述符的資料結構本身就存于內核態中,系統呼叫回傳時利用mmap()檔案映射記憶體加速與內核空間的訊息傳遞:即epoll使用mmap減少復制開銷,select、poll采用輪詢的方式來檢查檔案描述符是否處于就緒態,而epoll采用回呼機制,造成的結果就是,隨著fd的增加,select和poll的效率會線性降低,而epoll不會受到太大影響,除非活躍的socket很多,epoll的邊緣觸發模式效率高,系統不會充斥大量不關心的就緒檔案描述符雖然epoll的性能最好,但是在連接數少并且連接都十分活躍的情況下,select和poll的性能可能比epoll好,畢竟epoll的通知機制需要很多函式回呼,
計網
TCP
特點:可靠、面向連接、點對點通信、全雙工通信、擁塞控制、流量控制,(ARQ)
close_wait:防止被動關閉方仍然存在資料沒有發送完,
time_wait為何等待兩個MSL:①保證自己發送的ACK能夠到達被動方,2MSL保證能夠超時重傳;②保證本次連接中產生的所有報文都已經在網路中消失,不會影響下一個連接,
time_wait太多:調整內核引數,即:①重用處于time_wait的socket;②快速回收處于time_wait的socket;③降低socket處于time_wait的時間④降低系統默認設定的time_wait的socket最大數量;⑤擴大可用于socket的埠范圍
net.ipv4.tcp_syncookies = 1 表示開啟SYN Cookies,當出現SYN等待佇列溢位時,啟用cookies來處理,可防范少量SYN攻擊,默認為0,表示關閉; --用于防御半連接攻擊 net.ipv4.tcp_tw_reuse = 1 表示開啟重用,允許將TIME_WAIT sockets重新用于新的TCP連接,默認為0,表示關閉; net.ipv4.tcp_tw_recycle = 1 表示開啟TCP連接中TIME_WAIT sockets的快速回收,默認為0,表示關閉, net.ipv4.tcp_fin_timeout 修改系統默認的 TIMEOUT 時間流量控制:發送視窗不能大于接收視窗,
擁塞控制:慢開始(小于門限,指數增長)、擁塞避免(大于門限、線性增長)、快重傳(收到三個重復確認時,直接重傳丟失報文而非等待超時)、快恢復(門限值減半,擁塞視窗減半,直接開始擁塞避免),對于超時的情況,門限減半,擁塞視窗直接從0開始執行慢開始演算法,
查看time_wait連接數
netstat -ae | grep “TIME_WAIT” | wc -l
擁塞控制為何是3個ACK才快重傳
TCP segment亂序有2/5= 40%的概率會造成A收到三次duplicated ACK(N);而如果N丟了,則會100%概率A會收到三次duplicated ACK(N);
UDP
不可靠、無連接、RIP協議使用、實作可靠傳輸需要上層應用層幫助(RUDPS、RTP、UDT模仿TCP),不對資料包做任何操作,直接將其發送,也不考慮接收方,
丟包問題一般發生在哪一層
各層均可能
ICMP協議作用
型別:目標不可到達、源抑制和超時報文
是ip報文的組成部分,
電腦聯網失敗,發生在哪一層
各層均可能
TCP和UDP的報頭長度
udp8位元組,tcp20位元組
怎么構建一個請求超時?
后端接收到請求不處理也不拒絕
SCTP了解過嗎, 介紹一下
流控制傳輸協議(SCTP,Stream Control Transmission Protocol)是一種在網路連接兩端之間同時傳輸多個資料流的協議,
SCTP是面向訊息的(message-oriented),它提供各個記錄的按序遞送服務,與UDP一樣,由發送端寫入的每一條記錄的長度隨資料一道傳遞給接收端應用,
SCTP能給在所連接的端點之間提供多個流,每個流各自可靠地按序遞送訊息,一個流上某個訊息的丟失不會阻塞同一關聯其他流上訊息的投遞,這種做法與TCP正好相反,就TCP而言,在單一位元組流中任何位置的位元組丟失都將在阻塞該連接上其后所有資料的遞送,直到該丟失被修復為止,
TELNET
Telnet協議是TCP/IP協議家族中的一員,是Internet遠程登陸服務的標準協議和主要方式,它為用戶提供了在本地計算機上完成遠程主機作業的能力,在終端使用者的電腦上使用telnet程式,用它連接到服務器,終端使用者可以在telnet程式中輸入命令,這些命令會在服務器上運行,就像直接在服務器的控制臺上輸入一樣,可以在本地就能控制服務器,要開始一個telnet會話,必須輸入用戶名和密碼來登錄服務器,Telnet是常用的遠程控制Web服務器的方法,
需要連接的機器開啟telnet服務,
半連接攻擊(SYN_FLOOD)
半連接就是通過不斷地構造客戶端的SYN連接資料包發向服務端,等到服務端的半連接佇列滿的時候,后續的正常用戶的連接請求將會被丟棄,從而無法連接到服務端,此為半連接攻擊方式,
可通過開啟SYN Cookies來解決,即通過發送方的資訊(埠、ip)和接收方的資訊計算出一個cookie,并將其作為序列號進行回復,然后cookie對應一個時間范圍,在時間范圍內的ack都是合法的,不進入半連接佇列,直接完成三次握手,
全連接攻擊
全連接攻擊:是通過消費服務端行程數和連接數,只連接而不進行發送資料的一種攻擊方式,當客戶端連接到服務端,僅僅只是連接,此時服務端會為每一個連接創建一個行程來處理客戶端發送的資料,但是客戶端只是連接而不發送資料,此時服務端會一直阻塞在recv或者read的狀態,如此一來,多個連接,服務端的每個連接都是出于阻塞狀態從而導致服務端的崩潰,
可通過設定超時時間來解決,
為何需要三次握手而不是兩次?四次揮手而不是三次?
個人理解:三次握手分別保證客戶端發送資料的能力、服務端發送資料和接收資料的能力以及客戶端接收資料的能力,如果沒有第三次握手,就無法保證客戶端有接收資料的能力,同時三次握手保證了序列號的一致性以及雙方連接建立的完整性(兩次握手可能存在服務端的ack延遲到達客戶端,此時客戶端已經放棄連接了而服務端卻以為建立好了連接),而四次揮手則是因為被動關閉方可能還存在資料未發送完全,需要等待被動方發送完資料并主動發出FIN才能保證資料發送完畢,
http:
狀態碼:1xx資訊、2xx成功、3xx重定向、4xx客戶端錯誤、5xx服務端錯誤
100——繼續、200——成功、301永久重定向、302臨時重定向、400 Bad Request、404找不到頁面、401未授權、403拒絕請求、500服務器內部錯誤,
301 302區別:301表示舊地址A的資源已經被永久地移除了(這個資源不可訪問了),搜索引擎在抓取新內容的同時也將舊的網址交換為重定向之后的網址;302表示舊地址A的資源還在(仍然可以訪問),這個重定向只是臨時地從舊地址A跳轉到地址B,搜索引擎會抓取新的內容而保存舊的網址,
400——1、語意有誤,當前請求無法被服務器理解,除非進行修改,否則客戶端不應該重復提交這個請求,2、請求引數有誤,
403——服務器理解了請求但是拒絕執行
http頭部
通用頭部: cache-control:請求和回應遵循的快取機制 pragma:Pragma頭域用來包含實作特定的指令,最常用的是Pragma:no-cache connection:是否保持長連接 keep-alive等等 date:時間 請求頭: accept:接收什么型別 text/html等等 accept-encoding、accept-language referer:從何而來 如何跳轉過來的 user-agent:瀏覽器表明自己身份 回應頭: age:當代理服務器用自己快取的物體去回應請求時,用該頭部表明該物體從產生到現在經過多長時間了, server:表明自身是什么軟體 Server:Apache/2.0.61 (Unix) Accept-Ranges:bytes表示接受、none表示不接受 物體頭: allow:支持哪些方法 content-length:回應物件長度 content-type:回應物件的型別 last-modified:物件的最后修改時間
http各版本
1)HTTP 1.0:
- 請求與回應支持 HTTP 頭,回應含狀態行,增加了狀態碼,
- 支持 HEAD,POST 方法
- 支持傳輸 HTML 檔案以外其他型別的內容
HTTP1.0 使用的是非持久連接,主要缺點是客戶端必須為每一個待請求的物件建立并維護一個新的連接,即每請求一個檔案就要有兩倍RTT的開銷,因為同一個頁面可能存在多個物件,所以非持久連接可能使一個頁面的下載變得十分緩慢,而且這種短連接增加了網路傳輸的負擔,(RTT(Round Trip Time):一個連接的往返時間,即資料發送時刻到接收到確認的時刻的差值)
2)HTTP 1.1:
- 支持長連接,
- 在HTTP1.0的基礎上引入了更多的快取控制策略,
- 引入了請求范圍設定,優化了帶寬,
- 在錯誤通知管理中新增了錯誤狀態回應碼,
- 增加了Host頭處理,可以傳遞主機名(hostname),
**缺點:**傳輸內容是明文,不夠安全
3)HTTPS
- HTTPS運行在安全套接字協議(Secure Sockets Layer,SSL )或傳輸層安全協議(Transport Layer Security,TLS)之上,所有在TCP中傳輸的內容都需要經過加密,
- 連接方式不同,HTTP的埠是80,HTTPS的埠是443,
- HTTPS可以有效防止運營商劫持,
注:SSL運行在TCP之上
4)HTTP 1.x優化(SPDY)
SPDY 并不是新的一種協議,而是在 HTTP 之前做了一層會話層,為了達到減少頁面加載時間的目標,SPDY 引入了一個新的二進制分幀資料層,以實作優先次序、最小化及消除不必要的網路延遲,目的是更有效地利用底層 TCP 連接,
- 多路復用,為多路復用設立了請求優先級,
- 對header部分進行了壓縮,
- 引入了HTTPS加密傳輸,
- 客戶端可以在快取中取到之前請求的內容,
5)HTTP2.0(SPDY的升級版):
- HTTP2.0支持明文傳輸,而HTTP 1.X強制使用SSL/TLS加密傳輸,
- 和HTTP 1.x使用的header壓縮方法不同,
- HTTP2.0 基于二進制格式進行決議,而HTTP 1.x基于文本格式進行決議,
- 多路復用,HTTP1.1是多個請求串行化單執行緒處理,HTTP 2.0是并行執行,一個請求超時并不會影響其他請求,
HTTP2.0的多路復用提升了網頁性能:
- 在 HTTP1 中瀏覽器限制了同一個域名下的請求數量(Chrome下一般是六個),當在請求很多資源的時候,由于隊頭阻塞,當瀏覽器達到最大請求數量時,剩余的資源需等待當前的六個請求完成后才能發起請求,
- HTTP2 中引入了多路復用的技術,這個技術可以只通過一個TCP連接就可以傳輸所有的請求資料,多路復用可以繞過瀏覽器限制同一個域名下的請求數量的問題,進而提高了網頁的性能,
注意:
- 主流瀏覽器只支持基于TLS部署的HTTP 2.0協議,所以要將網站升級為HTTP 2.0,就需要先升級為HTTPS,
- HTTP 2.0完全兼容HTTP 1.x,所以對于部署了HTTP 2.0的網站可以自動向下兼容HTTP 1.X,
6) HTTP 3.0 (QUIC):
QUIC (Quick UDP Internet Connections),快速 UDP 互聯網連接,QUIC是基于UDP協議的,
兩個主要特性:
(1)線頭阻塞(HOL)問題的解決更為徹底:
基于TCP的HTTP/2,盡管從邏輯上來說,不同的流之間相互獨立,不會相互影響,但在實際傳輸方面,資料還是要一幀一幀的發送和接收,一旦某一個流的資料有丟包,則同樣會阻塞在它之后傳輸的流資料傳輸,而基于UDP的QUIC協議則可以更為徹底地解決這樣的問題,讓不同的流之間真正的實作相互獨立傳輸,互不干擾,
(2)切換網路時的連接保持:
當前移動端的應用環境,用戶的網路可能會經常切換,比如從辦公室或家里出門,WiFi斷開,網路切換為3G或4G,基于TCP的協議,由于切換網路之后,IP會改變,因而之前的連接不可能繼續保持,而基于UDP的QUIC協議,則可以內建與TCP中不同的連接標識方法,從而在網路完成切換之后,恢復之前與服務器的連接,
線頭阻塞(HOL)
TCP協議中,序號為1、3的資料包接收到后,不能直接傳遞給上層,需要等待到序號為2的資料包到達,這種等待的情況稱為線頭阻塞,
https身份認證
身份認證(CA數字證書):
https協議中身份認證的部分是由數字證書來完成的,證書由公鑰、證書主題、數字簽名等內容組成,在客戶端發起SSL請求后,服務端會將數字證書發給客戶端,客戶端對證書進行驗證,并獲取用于秘鑰交換的非對稱秘鑰
數字證書作用:
- 身份授權 確保瀏覽器訪問的網站是經過CA驗證的可信任網站
- 分發公鑰 每個數字證書都包含了注冊者生成的公鑰,在SSL握手時通過certificate訊息傳輸給客戶端
數字證書驗證:
申請者拿到CA的證書并部署在網站服務器端,瀏覽器發起握手接收到證書后,如何確認這個證書就是CA簽發的呢?怎樣避免第三方偽造這個證書?答案就是數字簽名(digital signature),數字簽名是證書的防偽標簽,目前使用最廣泛的是SHA-RSA(SHA用于哈希演算法,RSA用于非對稱加密演算法)數字簽名
瀏覽器如何驗證CA證書
首先,瀏覽器通過URL網址去請求服務端,服務端接收到請求后,就會給瀏覽器發送一個自己的CA數字證書 然后,瀏覽器接收到證書以后,開始驗證,首先從證書中得知證書的頒發機構,然后從瀏覽器系統中去尋找此頒發機構的根證書(世界上權威CA機構的根證書都是預先嵌入到瀏覽器中的),如果在瀏覽器系中沒有找到對應的根證書,就代表此機構不是受信任的,那么就會警告無法確認證書的真偽,比如以前打開12360網站就會提示, 之后,如果找到了證書頒發機構的根證書,那么就從根證書中取得那個根公鑰,用根公鑰去解密此證書的數字簽名,成功解密的話就得到證書的指紋和指紋演算法,指紋是證書內容通過指紋演算法計算得到的一個hash值,這里我們稱之為h1,h1代表證書的原始內容;然后用指紋演算法對當前接收到的證書內容再進行一次hash計算得到另一個值h2,h2則代表當前證書的內容,如果此時h1和h2是相等的,就代表證書沒有被修改過,如果證書被篡改過,h2和h1是不可能相同的, 假如證書上的指紋是不法分子偽造的,偽造是沒有用的,因為你偽造的指紋不可能用CA機構的根私鑰去加密(根私鑰是CA機構絕對保密的),偽造者只能拿自己的秘鑰去加密這個偽造的指紋,但當我們拿機構的根公鑰去解密偽造指紋的時候是不可能成功的(加密內容只能由一對公鑰私鑰解密) 在證書沒有被修改過的基礎上,再檢查證書上的使用者的URL(比如csdn.net)和我們請求的URL是否相等,如果相等,那么就可以證明當前瀏覽器鏈接的網址也是正確的,而不是一些釣魚網之類的, 但如果瀏覽器的連接被某個中間人截取了,中間人也可以發一個由權威的CA機構頒發的證書給瀏覽器,然后也可以通過證書沒有被篡改的驗證,但是在證書沒有被篡改的前提下,通過對比證書上的URL和我們請求的URL是否相同,我們還是可以判斷當前證書是不是服務器發的證書,可以這么理解,因為URL具有唯一性,所以中間人的證書的上的URL和我們的證書的URL是不可能相同的,如果中間人修改了自己證書上的URL,那么就通過不了證書沒有被篡改的驗證,所以中間人的證書也是欺騙不了我們的, 然后生成對稱密鑰即可,
https密鑰協商機制
1)非對稱加密:
客戶端發送 ClientHello(包含支持的協議版本、加密演算法和 亂數A (Client random))到服務端 服務端回傳 ServerHello、公鑰、證書、亂數B (Server random) 到客戶端 客戶端使用CA證書驗證回傳證書無誤后,生成 亂數C (Premaster secret),用公鑰對其加密,發送到服務端 服務端用 私鑰 解密得到 亂數C (Premaster secret),隨后根據已經得到的 亂數ABC生成對稱密鑰(hello的時候確定的加密演算法),并對需要發送的資料進行對稱加密發送 客戶端使用對稱密鑰(客戶端也用亂數ABC生成對稱密鑰)對資料進行解密, 雙方手持對稱密鑰 使用對稱加密演算法通訊2)DH密鑰協商:可以做到——“通訊雙方在完全沒有對方任何預先資訊的條件下通過不安全信道創建起一個密鑰”
但無法防止中間人篡改,需要和RSA配合簽名機制使用,
1. 客戶端先連上服務端 2. 服務端生成一個亂數 s 作為自己的私鑰,然后根據演算法引數計算出公鑰 S(演算法引數通常是固定的) 3. 服務端使用某種簽名演算法把“演算法引數(模數p,基數g)和服務端公鑰S”作為一個整體進行簽名 4. 服務端把“演算法引數(模數p,基數g)、服務端公鑰S、簽名”發送給客戶端 5. 客戶端收到后驗證簽名是否有效 6. 客戶端生成一個亂數 c 作為自己的私鑰,然后根據演算法引數計算出公鑰 C 7. 客戶端把 C 發送給服務端 8. 客戶端和服務端(根據上述 DH 演算法)各自計算出 k 作為會話密鑰如何防范偷窺(嗅探):
嗅探者可以通過監視網路傳輸,得到演算法引數(模數p,基數g)以及雙方的公鑰,但是【無法】推算出雙方的私鑰,也【無法】推算出會話密鑰(這是由 DH 演算法在數學上保證的)
如何防范篡改(假冒身份)
攻擊方式1:攻擊者可以第4步篡改資料(修改演算法引數或服務端公鑰),但因為這些資訊已經進行過數字簽名,篡改之后會被客戶端發現,
攻擊方式2:攻擊者可以在第7步篡改客戶端公鑰,這步沒有簽名,服務端收到資料后不會發現被篡改,但是,攻擊者篡改之后會導致“服務端與客戶端生成的會話密鑰【不一致】”,在后續的通訊步驟中會發現這點,并導致通訊終止,
TLS
整個TLS傳輸的程序如下:
(1)TCP三次握手
(2)SSL的ClientHello和ServerHello和對應的秘鑰交換KeyExchange
(3)Client和Server互相ChangeCipherSpec通知進入加密模式,此時可以進入資料傳輸狀態
(4)應用資料傳輸程序
(5)應用資料傳輸完成,TCP兩次揮手
拋開TCP連接和資料包文傳輸的部分,TLS握手部分將使用2個RTT,
前向安全性
指的是長期使用的主密鑰泄漏不會導致過去的會話密鑰泄漏
http一些引數
Content-Length:指明回應體的資料大小
content-type:資料格式,
- application/json:JSON資料格式
- text/html:HTML格式
- text/xml:XML格式
Connection: keep-alive:保持長連接
http——chunk
當客戶端向服務器請求一個靜態頁面或者一張圖片時,服務器可以很清楚的知道內容大小,然后通過Content-Length訊息首部欄位告訴客戶端需要接收多少資料,但是如果是動態頁面等時,服務器是不可能預先知道內容大小,這時就可以使用Transfer-Encoding:chunk模式來傳輸資料了,即如果要一邊產生資料,一邊發給客戶端,服務器就需要使用"Transfer-Encoding: chunked"這樣的方式來代替Content-Length,
在進行chunked編碼傳輸時,在回復訊息的頭部有Transfer-Encoding: chunked
編碼使用若干個chunk組成,由一個標明長度為0的chunk結束,每個chunk有兩部分組成,第一部分是該chunk的長度,第二部分就是指定長度的內容,每個部分用CRLF隔開,在最后一個長度為0的chunk中的內容是稱為footer的內容,是一些沒有寫的頭部內容,
chunk編碼格式如下:
[chunk size][\r\n][chunk data][\r\n][chunk size][\r\n][chunk data][\r\n][chunk size = 0][\r\n][\r\n]
chunk size是以十六進制的ASCII碼表示,比如:頭部是3134這兩個位元組,表示的是1和4這兩個ascii字符,被http協議解釋為十六進制數14,也就是十進制的20,后面緊跟[\r\n](0d 0a),再接著是連續的20個位元組的chunk正文,chunk資料以0長度的chunk塊結束,也就是(30 0d 0a 0d 0a),
原理
HTTP 1.1引入分塊傳輸編碼提供了以下幾點好處:
HTTP分塊傳輸編碼允許服務器為動態生成的內容維持HTTP持久鏈接,通常,持久鏈接需要服務器在開始發送訊息體前發送Content-Length訊息頭欄位,但是對于動態生成的內容來說,在內容創建完之前是不可知的,
分塊傳輸編碼允許服務器在最后發送訊息頭欄位,對于那些頭欄位值在內容被生成之前無法知道的情形非常重要,例如訊息的內容要使用散列進行簽名,散列的結果通過HTTP訊息頭欄位進行傳輸,沒有分塊傳輸編碼時,服務器必須緩沖內容直到完成后計算頭欄位的值并在發送內容前發送這些頭欄位的值,
DNS
瀏覽器輸入一個地址,發生了什么?
根據域名查找ip地址(瀏覽器快取——本機host快取——DNS系統呼叫——本地DNS服務器快取——遞回查詢直到獲得ip地址——可能因為負載均衡每次獲得不同的ip地址),然后向該ip發送http請求,服務器回應回復html檔案,瀏覽器決議html并根據content-type判斷如何處理(顯示、下載等等),瀏覽器獲取html檔案內嵌的圖片、音頻、js等等,最后瀏覽器還可以發送ajax異步請求,
DNS區域傳輸的時候使用TCP協議:輔域名服務器會定時(一般3小時)向主域名服務器進行查詢以便了解資料是否有變動,如有變動,會執行一次區域傳送,進行資料同步,區域傳送使用TCP而不是UDP,因為資料同步傳送的資料量比一個請求應答的資料量要多得多,
域名決議時使用UDP協議
DNS over TLS / HTTPS
加密的DNS協議,但是延時也很高,需要耗費4 RTT來保證安全,
cookie和session、token
cookie:客戶端會話技術,存盤資料在客戶端瀏覽器,默認瀏覽器關閉后清除,能存放的資料有限且安全性較低,存放sessionID,之后的請求默認攜帶,
session:服務端會話技術,存盤在服務端,用來保存狀態,依賴于cookie,存放的資料無限制且安全性高,但需要單獨存盤,耗費空間,
token:無狀態的令牌,采用簽名的方式來驗證(私鑰簽名、公鑰驗證),每次傳輸過來的資料再次進行簽名以對比,再次請求需要手動添加token,
當用戶首次與Web服務器建立連接的時候,服務器會給用戶分發一個 SessionID作為標識,SessionID是一個由24個字符組成的隨機字串,用戶每次提交頁面,瀏覽器都會把這個SessionID包含在 HTTP頭中提交給Web服務器,這樣Web服務器就能區分當前請求頁面的是哪一個客戶端,這個SessionID就是保存在客戶端的,屬于客戶端Session, 其實客戶端Session默認是以cookie的形式來存盤的,所以當用戶禁用了cookie的話,服務器端就得不到SessionID,
http和rpc區別
RPC:可以基于TCP協議,也可以基于HTTP協議 HTTP:基于HTTP協議 RPC:可以基于thrift實作高效的二進制傳輸 HTTP:大部分是通過json來實作的,位元組大小和序列化耗時都比thrift要更消耗性能 RPC:基本都自帶了負載均衡策略 HTTP:需要配置Nginx,HAProxy來實作 RPC主要用于公司內部的服務呼叫,性能消耗低,傳輸效率高,服務治理方便, HTTP主要用于對外的異構環境,瀏覽器介面呼叫,APP介面呼叫,第三方介面呼叫等,
nginx
作業流程: 1、用戶通過域名發出訪問Web服務器的請求,該域名被DNS服務器決議為反向代理服務器的IP地址; 2、反向代理服務器接受用戶的請求; 3、反向代理服務器在本地快取中查找請求的內容,找到后直接把內容發送給用戶; 4、如果本地快取里沒有用戶所請求的資訊內容,反向代理服務器會代替用戶向源服務器請求同樣的資訊內容,并把資訊內容發給用戶,如果資訊內容是非快取的還會把它保存到快取中, -------- Nginx模塊: Nginx有五大優點:模塊化、事件驅動、異步、非阻塞、多行程單執行緒,由內核和模塊組成的,其中內核完成的作業比較簡單,僅僅通過查找組態檔將客戶端請求映射到一個location block,然后又將這個location block中所配置的每個指令將會啟動不同的模塊去完成相應的作業, -------- 作用: * 保護了真實的web服務器,保證了web服務器的資源安全 * 節約了有限的IP地址資源 * 減少WEB服務器壓力,提高回應速度 - 反向代理就是通常所說的web服務器加速,它是一種通過在繁忙的web服務器和外部網路之間增加一個高速的web緩沖服務器來降低實際的web服務器的負載的一種技術,反向代理是針對web服務器提高加速功能,作為代理快取,它并不是針對瀏覽器用戶,而針對一臺或多臺特定的web服務器,它可以代理外部網路對內部網路的訪問請求, - 反向代理服務器會強制將外部網路對要代理的服務器的訪問經過它,這樣反向代理服務器負責接收客戶端的請求,然后到源服務器上獲取內容,把內容回傳給用戶,并把內容保存到本地,以便日后再收到同樣的資訊請求時,它會把本地快取里的內容直接發給用戶,以減少后端web服務器的壓力,提高回應速度,因此Nginx還具有快取功能, * 請求的統一控制,包括設定權限、過濾規則等; * 區分動態和靜態可快取內容; * 實作負載均衡,內部可以采用多臺服務器來組成服務器集群,外部還是可以采用一個地址訪問; * 解決Ajax跨域問題; * 作為真實服務器的緩沖,解決瞬間負載量大的問題;
介面冪等性
介面冪等用于表示任意多次請求執行的結果均與一次請求執行的結果相同
實作冪等性的關鍵步驟分為以下三個:
(1)每個請求操作必須有唯一的 ID,而這個 ID 就是用來表示此業務是否被執行過的關鍵憑證,例如,訂單支付業務的請求,就要使用訂單的 ID 作為冪等性驗證的 Key;
(2)每次執行業務之前必須要先判斷此業務是否已經被處理過;
(3)第一次業務處理完成之后,要把此業務處理的狀態進行保存,比如存盤到 Redis 中或者是資料庫中,這樣才能防止業務被重復處理
get請求為冪等、post則不是
理解restful(Representational State Transfer)
(1)每一個URI代表一種資源;
(2)客戶端和服務器之間,傳遞這種資源的某種表現層;
(3)客戶端通過四個HTTP動詞,對服務器端資源進行操作,實作"表現層狀態轉化",
RESTful 架構的核心規范與約束:統一介面 分為四個子約束: 1.每個資源都擁有一個資源標識,每個資源的資源標識可以用來唯一地標明該資源 2.訊息的自描述性 3.資源的自描述性, 4.HATEOAS Hypermedia As The Engine Of Application State(超媒體作為應用狀態引擎) 即客戶只可以通過服務端所回傳各結果中所包含的資訊來得到下一步操作所需要的資訊,如到底是向哪個URL發送請求等,也就是說,一個典型的REST服務不需要額外的檔案標示通過哪些URL訪問特定型別的資源,而是通過服務端回傳的回應來標示到底能在該資源上執行什么樣的操作
使用標準的狀態碼
GET 安全且冪等、獲取表示、變更時獲取表示(快取) 200(OK) - 表示已在回應中發出 204(無內容) - 資源有空表示 301(Moved Permanently) - 資源的URI已被更新 303(See Other) - 其他(如,負載均衡) 304(not modified)- 資源未更改(快取) 400 (bad request)- 指代壞請求(如,引數錯誤) 404 (not found)- 資源不存在 406 (not acceptable)- 服務端不支持所需表示 500 (internal server error)- 通用錯誤回應 503 (Service Unavailable)- 服務端當前無法處理請求 POST 不安全且不冪等 使用服務端管理的(自動產生)的實體號創建資源 創建子資源 部分更新資源 如果沒有被修改,則不過更新資源(樂觀鎖) 200(OK)- 如果現有資源已被更改 201(created)- 如果新資源被創建 202(accepted)- 已接受處理請求但尚未完成(異步處理) 301(Moved Permanently)- 資源的URI被更新 303(See Other)- 其他(如,負載均衡) 400(bad request)- 指代壞請求 404 (not found)- 資源不存在 406 (not acceptable)- 服務端不支持所需表示 409 (conflict)- 通用沖突 412 (Precondition Failed)- 前置條件失敗(如執行條件更新時的沖突) 415 (unsupported media type)- 接受到的表示不受支持 500 (internal server error)- 通用錯誤回應 503 (Service Unavailable)- 服務當前無法處理請求 PUT 不安全但冪等 用客戶端管理的實體號創建一個資源 通過替換的方式更新資源 如果未被修改,則更新資源(樂觀鎖) 200 (OK)- 如果已存在資源被更改 201 (created)- 如果新資源被創建 301(Moved Permanently)- 資源的URI已更改 303 (See Other)- 其他(如,負載均衡) 400 (bad request)- 指代壞請求 404 (not found)- 資源不存在 406 (not acceptable)- 服務端不支持所需表示 409 (conflict)- 通用沖突 412 (Precondition Failed)- 前置條件失敗(如執行條件更新時的沖突) 415 (unsupported media type)- 接受到的表示不受支持 500 (internal server error)- 通用錯誤回應 503 (Service Unavailable)- 服務當前無法處理請求 DELETE 不安全但冪等 洗掉資源 200 (OK)- 資源已被洗掉 301 (Moved Permanently)- 資源的URI已更改 303 (See Other)- 其他,如負載均衡 400 (bad request)- 指代壞請求 404 (not found)- 資源不存在 409 (conflict)- 通用沖突 500 (internal server error)- 通用錯誤回應 503 (Service Unavailable)- 服務端當前無法處理請求
路由器和交換機具體實作了什么功能,路由選擇如何實作
路由器屬于網路層,使用ip地址通信,連接局域網和外網,
交換機屬于資料鏈路層,使用mac地址通信,作業在局域網內部,
介紹下IPV6
128位,16位元組,16進制
jwt(JSON Web Token)
JSON Web Token由三部分組成,它們之間用圓點(.)連接,這三部分分別是:
- Header、Payload、Signature
header典型的由兩部分組成:token的型別(“JWT”)和演算法名稱(比如:HMAC SHA256或者RSA等等),例如:
{ 'alg': "HS256", 'typ': "JWT" }用Base64對這個JSON編碼就得到JWT的第一部分
payload,它包含宣告(要求),宣告是關于物體(通常是用戶)和其他資料的宣告,宣告有三種型別: registered, public 和 private,
Registered claims : 這里有一組預定義的宣告,它們不是強制的,但是推薦,比如:iss (issuer), exp (expiration time), sub (subject), aud (audience)等,
Public claims : 可以隨意定義,
Private claims : 用于在同意使用它們的各方之間共享資訊,并且不是注冊的或公開的宣告, 下面是一個例子:
{ "sub": '1234567890', "name": 'john', "admin":true }對payload進行Base64編碼就得到JWT的第二部分
注意,不要在JWT的payload或header中放置敏感資訊,除非它們是加密的,
Signature
為了得到簽名部分,你必須有編碼過的header、編碼過的payload、一個秘鑰,簽名演算法是header中指定的那個,然對它們簽名即可,例如:
HMACSHA256(base64UrlEncode(header) + “.” + base64UrlEncode(payload), secret)
簽名是用于驗證訊息在傳遞程序中有沒有被更改,并且,對于使用私鑰簽名的token,它還可以驗證JWT的發送方是否為它所稱的發送方,
CSRF跨站點請求偽造(Cross—Site Request Forgery)
攻擊者盜用了你的身份,以你的名義發送惡意請求,對服務器來說這個請求是完全合法的,但是卻完成了攻擊者所期望的一個操作,比如以你的名義發送郵件、發訊息,盜取你的賬號,添加系統管理員,甚至于購買商品、虛擬貨幣轉賬等,
反爬策略
- 限制IP地址單位時間的訪問次數
- 用戶登錄才能訪問網站內容, 若識別為爬蟲賬號,封禁IP
- header, User-Agent檢查用戶所用客戶端的種類和版本, 在請求頭中加入CSRF_token識別用戶請求(參考form表單驗證)
- Referer, 檢查請求由哪里來,通常可以做圖片的盜鏈判斷
- Cookies,檢測Cookie中session_id 的使?用次數,如果超過限制,就觸發反爬策略略
- 動態加載,網站使用ajax動態加載內容
- 對前端請求的API的引數進行加密
- 對網站JS進行混淆加密(適用于對API引數加密的情況,對用于加密的JS進行混淆)
- 在用戶登錄時,進行驗證碼驗證(圖片驗證碼或滑動驗證碼或短信驗證碼等)
- 對網頁資料展示的總頁數進行限制,比如用戶只能瀏覽200頁
java基礎
java和python 開發web的區別
①架構:java更像是一種行業規定的十分清晰的架構;而python則更加自由,有優勢也有缺點
②生態:java的生態十分強大,而python則相對小眾
③庫:python庫十分豐富且方便呼叫,java則是通過jar包進行發布,相對較麻煩
④穩定性:python2和python3的區別導致了穩定性方面的不足,java則更加統一,從虛擬機角度也是如此,
⑤其他:java更商業化、python的腳本等等也非常流行,
包裝類自動拆箱、自動裝箱
c == a + b c.equals(a + b)
注解
* 作用分類 * 撰寫檔案:生成檔案【doc檔案】 * 代碼分析:通過注解對代碼進行分析【使用反射】 * 編譯檢查:讓編輯器能實作基本的編譯檢查【@Override】 * JDK中預定義的一些注解 * @Override:檢測被該注解標注的方法是否是繼承自父類(介面)的 * @Deprecated:將該注解標注的內容,表示已過時 * @SuppressWarnings:壓制警告 * 一般傳遞引數all @SuppressWarnings("all") * 自定義注解 * 格式: * 元注解 * public @interface 注解名稱() { 屬性串列; } * 本質:注解本質上就是一個介面,該介面默認繼承Annotation * public interface 注解名稱 extends java.lang.annotation.Annotation {} * 屬性:介面中的抽象方法 * 要求: 1. 屬性的回傳值型別有下列取值: * 基本資料型別 * String * 列舉 * 注解 * 以上型別的陣列 2. 定義了屬性,在使用時需要給屬性賦值 1. 如果定義屬性時,使用default關鍵字給屬性默認初始值,則使用注解時可以不進行屬性的賦值 2. 如果只有一個屬性需要賦值,并且屬性的名稱為value,則value可以省略,直接定義值即可【@SuppressWarnings】 3. 陣列賦值時,值使用{}包裹,如果陣列中只有一個值,則{}可以省略 * 元注解:用于描述注解的注解 * @Target:描述注解能夠作用的位置 * ElementType取值: * TYPE:可以作用于類上 * METHOD:可以作用于方法上 * FIELD:可以作用于成員變數上 * @Retention:描述注解被保留的一個階段 * @Rentention(RententionPolicy.RUNTIME):當前被描述的注解,會保留到class位元組碼檔案中,并被JVM讀取到 * @Documented:描述注解是否被抽取到api檔案中 * @Inherited:描述注解是否被子類繼承 * 在程式中使用(決議)注解:獲取注解中定義的屬性值 1. 獲取注解定義的位置的物件 【Class,Method,Field】 2. 獲取指定的注解 * getAnnotation(Class) * public class ProImpl implements Pro{ public String className(){ return "day01.annotation.Demo1"; } public String methodName(){ return "show"; } } 3. 呼叫注解中的抽象方法,獲取配置的屬性值
- RPC(Remote Procedure Call)遠程程序呼叫,簡單的理解是一個節點請求另一個節點提供的服務
繼承和多型
繼承:它可以使用現有類的所有功能,并在無需重新撰寫原來的類的情況下對這些功能進行擴展,
多型:允許將子型別別的指標賦值給父型別別的指標,
重寫和多載
重寫:子類重寫父類方法,回傳值和形參都不能改變
多載:方法名字相同,而引數不同,回傳型別可以相同也可以不同,每個多載的方法(或者建構式)都必須有一個獨一無二的引數型別串列,
TCP UDP對應的socket編程的API
socket():創建socket
bind():系結socket到本地地址和埠,通常由服務端呼叫
listen():TCP專用,開啟監聽模式
accept():TCP專用,服務器等待客戶端連接,一般是阻塞態
connect():TCP專用,客戶端主動連接服務器
send():TCP專用,發送資料
recv():TCP專用,接收資料
sendto():UDP專用,發送資料到指定的IP地址和埠
recvfrom():UDP專用,接收資料,回傳資料遠端的IP地址和埠
closesocket():關閉socket
函式式編程
示例:
Function<Integer, Integer> f = x -> x + 1;和匿名內部類的區別:
①this指向不同,lambda指向當前類,匿名內部類指向其本身
②lambda運算式并沒有生成.class檔案,匿名內部類則生成了
RBAC模型
權限控制?
transient
不進行序列化
NIO
BytebBuffer——HeapByteBuffer:在堆中創建的緩沖區,MappedByteBuffer:直接緩沖區,
allocate方法創建的直接緩沖區是創建的DirectByteBuffer實體,
java集合
hash沖突的解決方案
鏈地址法、開放地址法
開放地址法:hash沖突時對索引進行改變再放入,如hash得到的hashcode已經沖突,則將hashcode++再判斷,直到有空位,這也就是線性探測,還可以進行平方的步長的增加,加快效率,或者再hash,
兩次hash,加鹽,等等
HashMap原始碼、實作原理、轉紅黑樹的時機(為何是7)、多執行緒優化、多執行緒失敗的場景
jdk7,陣列加鏈表,hash沖突插入首部;jdk8,陣列+鏈表+紅黑樹,hash沖突插入尾部,在鏈表長度大于7且陣列長度大于64時轉換為紅黑樹,否則先擴容,為何是7呢,因為鏈表中節點數是8的概率已經接近千分之一,而且此時鏈表的性能已經很差了,所以在這種比較罕見和極端的情況下,才會把鏈表轉變為紅黑樹,而同時樹節點的占用空間約為鏈表的兩倍,占用空間較大,且在紅黑樹節點數為6時退化為鏈表,
容量:默認容量16,可自定義容量,如果不是2的冪次系統會默認設定為2的冪次,如果容量超過initial * loadFactor(默認0.75)會進行擴容(兩倍),(hash & (len - 1) —— 10101001 00001111)
為何執行緒不安全:
jdk7中由于頭插法,會存在resize后鏈表中形成環的情況, 執行緒1阻塞前鏈表情況:a->b->null 執行緒2此時執行,會采用頭插法將該鏈表放入新的table,放入后變成b->a->null 執行緒1此時回傳,同樣采用頭插法插入該執行緒自己的新的table中,兩次回圈后變為b->a,但是由于執行緒2執行時的后果會導致b的next不是null而是a,所以回圈多執行一次,e此時變為a會導致a.next = newTable[i](也就是b)然后發生回圈, jdk8中采用尾插法會導致資料的覆寫, 假設兩個執行緒A、B都在進行put操作,并且hash函式計算出的插入下標是相同的,當執行緒A執行完第13行代碼后(hash碰撞判斷,此時判斷是沒有碰撞的但還沒有進行插入)由于時間片耗盡導致被掛起,而執行緒B得到時間片后在該下標處插入了元素,完成了正常的插入,然后執行緒A獲得時間片,由于之前已經進行了hash碰撞的判斷,所有此時不會再進行判斷,而是直接進行插入,這就導致了執行緒B插入的資料被執行緒A覆寫了,從而執行緒不安全, 除此之外還存在一個++size的執行緒安全問題, jdk8解決成環問題: 采用兩組指標loHead、loTail、hiHead、hiTail 這兩組指標將鏈表分成了兩部分,高位指標指向哪些擴容后下標變為(舊下標+擴容大小),低位指標指向哪些擴容后下標還保持不變的節點,分成兩條鏈表今次那個遷移,遷移后節點的前后順序保持不變,不會出現環的情況,(擴容后鏈表節點的情況只有兩種下標不變 or 舊下標+擴容的大小) 紅黑樹的拆分和鏈表的邏輯基本一致,不同的地方在于,重新映射后,會將紅黑樹拆分成兩條鏈表,根據鏈表的長度,判斷需不需要把鏈表重新進行樹化,多執行緒優化:和讀寫鎖配合,
ConcurrentHashMap原理
jdk7,Segment陣列 + HashEntry陣列 + ReentrantLock(對每個Segment上鎖);jdk8,Node陣列 + CAS + Synchronized(只對每個node上鎖)
具體:
jdk7:保證segment陣列為2的冪次、會再散列來獲取下標 初始化有三個引數: initialCapacity:初始容量大小 ,默認16, loadFactor, 擴容因子,默認0.75,當一個Segment存盤的元素數量大于initialCapacity* loadFactor時,該Segment會進行一次擴容, concurrencyLevel:并發度,默認16,Segment[]的陣列長度,如果并發度設定的過小,會帶來嚴重的鎖競爭問題;如果并發度設定的過大,原本位于同一個Segment內的訪問會擴散到不同的Segment中,CPU cache命中率會下降,從而引起程式性能下降, segment不擴容,擴容的是hashentry陣列 擴容時的get操作訪問的是舊鏈表,對于put以及其他更新操作會阻塞直到擴容完成,get不需要加鎖,除非讀到的是null,原理是get方法中的變數都使用volatile關鍵字修飾,且對volatile欄位的寫入操作先于讀取操作,所以即使兩個執行緒同時修改和獲取volatile變數,get操作也能保證拿到最新的值,
ArrayList和LinkedList插入效率比較
插入到最后,效率相當,插入到中間,LinkedList效率高,資料量過大,ArrayList動態擴容,LinkedList效率更高,
ArrayList
懶加載(但在jdk7及以前會直接初始化一個容量為10的陣列),需要擴容時,會首先擴容置原容量的1.5倍左右(
new = old + old >> 1),然后如果new滿足需求,則會直接用new作為新容量,否則會將當前所需容量作為新容量,
iterator
核心方法:next()、hashNext()、remove()、forEachRemaining()
remove()方法將會洗掉上次呼叫next()方法時回傳的元素
Java最頂層集合有哪些
Collection、Map
抽象類和介面的區別:
抽象類更多是對事物的抽象,如人,是一種模板的設計;而介面則是對行為的抽象,如:運動,是一種行為的規范,
抽象類可以有靜態方法、成員變數可以為任意型別,
java并發
synchronized和lock區別以及底層原理
區別:
1.首先synchronized是java內置關鍵字,在jvm層面,Lock是個java類; 2.synchronized無法判斷是否獲取鎖的狀態,Lock可以判斷是否獲取到鎖; 3.synchronized會自動釋放鎖(a 執行緒執行完同步代碼會釋放鎖 ;b 執行緒執行程序中發生例外會釋放鎖),Lock需在finally中手工釋放鎖(unlock()方法釋放鎖),否則容易造成執行緒死鎖; 4.用synchronized關鍵字的兩個執行緒1和執行緒2,如果當前執行緒1獲得鎖,執行緒2執行緒等待,如果執行緒1阻塞,執行緒2則會一直等待下去,而Lock鎖就不一定會等待下去,如果嘗試獲取不到鎖,執行緒可以不用一直等待就結束了; 5.synchronized的鎖可重入、不可中斷、非公平,而Lock鎖可重入、可中斷、可公平(兩者皆可) 6.Lock鎖適合大量同步的代碼的同步問題,synchronized鎖適合代碼少量的同步問題,底層原理:
synchronized: 原子性:保證陳述句塊內是原子的 可見性:通過在unlock前需要將變數同步回主存,其他執行緒需要重新獲取 有序性:一個變數在同一時刻只允許一條執行緒對其操作 方法級的同步是通過方法呼叫和回傳中實作的,方法常量池的ACC_SYNCHRONIZED標志是否為同步方法,如果方法是同步方法,則執行執行緒需要先持有monitor然后執行方法,回傳后釋放, 代碼塊的同步是通過monitorenter和monitorexit實作的,遇到monitorenter試圖獲取monitor物件,如果未加鎖或者已經被自己持有,則鎖計數器+1,執行,遇到monitorexit則鎖計數-1.計數器為0代表鎖釋放,如果獲取monitor物件失敗會進入阻塞,且是可重入的, 1.6前慢的原因:物件內部的監視器鎖是通過底層OS的Mutex實作的,存在用戶態到內核態的切換,成本極高 1.6后的優化:四種鎖狀態 無鎖——偏向鎖——輕量級鎖——重量級鎖,(不可降級) -- 偏向鎖:無實際競爭,且將來只有第一個申請鎖的執行緒會使用鎖,只有一次CAS 鎖物件第一次被獲取時,jvm將物件頭鎖標志位設為01偏向模式,然后通過CAS將執行緒id記錄到物件的markword中,如果成功,該執行緒在以后每次進入該同步塊時,jvm不進行任何操作,如果不是第一次獲取鎖,則判斷偏向執行緒id是否為當前執行緒,是的話就進入同步塊,否則則根據當前偏向的執行緒是否存活,未存活則取消鎖到無鎖狀態,存活則升級為輕量級鎖, -- 輕量級鎖:無實際競爭,多個執行緒交替使用鎖;允許短時間的鎖競爭,申請和釋放需要CAS 輕量級鎖是相對于重量級鎖而言的,使用輕量級鎖時,不需要申請互斥量,而是在當前執行緒堆疊幀中開辟空間Lock Record用來記錄當前物件markword的拷貝,然后將Mark Word中的部分位元組CAS更新指向執行緒堆疊中的Lock Record,如果更新成功,則輕量級鎖獲取成功,記錄鎖狀態為00輕量級鎖;否則,說明已經有執行緒獲得了輕量級鎖,如果指向的是當前執行緒的堆疊幀,則重入代碼塊,否則出現了競爭,會嘗試幾次CAS,如果不行,升級為重量級鎖,標志位11,markword中指標指向重量級鎖, -- 重量級鎖:有實際競爭,且鎖競爭時間長,monitor實作, Lock: 有三個實作類,ReentrantLock, ReentrantReadWriteLock類中的兩個靜態內部類ReadLock和WriteLock, 底層實作為AQS, AQS:CLH鎖佇列(雙向鏈表)+state狀態變數,執行緒通過CAS去改變狀態,成功則獲取鎖成功,失敗則進入等待佇列,等待被喚醒, lock的存盤結構:一個int型別狀態值(用于鎖的狀態變更),一個雙向鏈表(用于存盤等待中的執行緒) lock獲取鎖的程序:本質上是通過 CAS 來獲取狀態值修改,如果當場沒獲取到,會將該執行緒放在執行緒等待鏈表中, lock釋放鎖的程序:修改狀態值,調整等待鏈表, lock()-acquire()-tryAcquire()-未成功獲取鎖-addwaiter()-acquireQueued() acquireQueued的主要作用是把已經追加到佇列的執行緒節點進行阻塞,但阻塞前又通過tryAccquire重試是否能獲得鎖,如果重試成功能則無需阻塞,直接回傳,
monitor底層
C++實作的,通過一個管程和FIFO佇列,保證臨界區同一個時間只有一個執行緒能夠進入,
Monitor的基本結構是什么?
1.Owner欄位:初始時為NULL表示當前沒有任何執行緒擁有該monitor record,當執行緒成功擁有該鎖后保存執行緒唯一標識,當鎖被釋放時又設定為NULL
2.EntryQ欄位:關聯一個系統互斥鎖(semaphore),阻塞所有試圖鎖住monitor record失敗的執行緒
3.RcThis欄位:表示blocked或waiting在該monitor record上的所有執行緒的個數
4.Nest欄位:用來實作重入鎖的計數
5.HashCode欄位:保存從物件頭拷貝過來的HashCode值(可能還包含GC age)
6.Candidate欄位:用來避免不必要的阻塞或等待執行緒喚醒,因為每一次只有一個執行緒能夠成功擁有鎖,如果每次前一個釋放鎖的執行緒喚醒所有正在阻塞或等待的執行緒,會引起不必要的背景關系切換(從阻塞到就緒然后因為競爭鎖失敗又被阻塞)從而導致性能嚴重下降;Candidate只有兩種可能的值0表示沒有需要喚醒的執行緒1表示要喚醒一個繼任執行緒來競爭鎖
鎖升級時什么時候可能出現記憶體溢位
輕量級鎖的lock record?
ReebtrantLock
可重入的互斥鎖,可公平可非公平(公平鎖會判斷當前執行緒前是否有其他等待執行緒,有的話就進入等待佇列,沒有的話才會嘗試獲取鎖,而非公平鎖則是直接嘗試獲取鎖)
ReebtrantReadWriteLock
支持公平和非公平、可重入、鎖降級(獲得寫鎖—獲得讀鎖—釋放寫鎖),不支持鎖升級,
狀態變數:高16位標識讀,低16位表示寫,通過位運算來獲得讀寫的狀態,
BlockingQueue原理
ArrayBlockingxxx, LinkedBlockingxxx, PriorityBlockingxxx, Delayxxx(延時獲取元素), Synchronousxxx(不存盤元素), LinkedTransferxxx(無界), LinkedBlockingDeque
處理方式:拋出例外(add)、回傳特殊值(offer)、一直阻塞(put、超時退出(offer(e, time, unit))
使用Condition + LockSupport實作,
CopyOnWriteArrayList
寫時復制:就是當有多個呼叫者同時去請求一個資源時(可以是記憶體中的一個資料),當其中一個呼叫者要對資源進行修改,系統會copy一個副本給該呼叫者,讓其進行修改;而其他呼叫者所擁有資源并不會由于該呼叫者對資源的改動而發生改變,這就是寫時復制思想;
當我們往CopyOnWrite容器中添加元素時,不直接操作當前容器,而是先將容器進行Copy,然后對Copy出的新容器進行修改,修改后,再將原容器的參考指向新的容器,即完成了整個修改操作,
存盤資料的array陣列通過volatile修飾,保證執行緒間的可見性,還使用ReentrantLock鎖來實作執行緒安全
add方法為何既加鎖又復制陣列:首先加鎖保證陣列操作的安全性和原子性,其次陣列的復制,保證舊陣列所在記憶體區域的資料未發生變化,這樣在修改陣列時不會對get方法產生影響,
CountDownLatch
類似join方法,作用為使一個執行緒等待其他執行緒各自執行完畢后再執行,
構造方法傳入int型整數,表示計數器,計數器的初始值是執行緒的數量,每當一個執行緒執行完畢后,呼叫countDown方法計數器的值就-1,當計數器的值為0時,表示所有執行緒都執行完畢,然后在await()閉鎖上等待的執行緒就可以恢復作業了,
執行緒池
Executors的幾個靜態方法:
newFixedThreadPool() 阻塞佇列無限,會OOM newSingleThreadExecutor() 阻塞佇列無限,會OOM newCachedThreadPool() 最大執行緒數為Integer.MAX 也會OOM自定義執行緒池引數的設定(cpu密集 or io密集)
cpu使用率較高(也就是一些復雜運算,邏輯處理),所以執行緒數一般只需要cpu核數的執行緒就可以了,減少背景關系切換 cpu使用率較低,程式中會存在大量I/O操作占據時間,導致執行緒空余時間出來,所以通常就需要開cpu核數的兩倍的執行緒, 當執行緒進行I/O操作cpu空暇時啟用其他執行緒繼續使用cpu,提高cpu使用率核心執行緒數、最大執行緒數、存活時間、時間單位、阻塞佇列、創建執行緒的工廠、拒絕策略
拒絕策略:
AbortPolicy:默認的策略,直接拋出 RejectedExecutionException 例外,阻?系統正常運?, CallerRunsPolicy:既不會拋出例外,也不會終?任務,?是將任務回傳給調?者,從?降低新任務的流量, DiscardOldestPolicy:拋棄佇列中等待最久的任務,然后把當前任務加?佇列中嘗試再次提交任務, DiscardPolicy:該策略默默地丟棄?法處理的任務,不予任何處理也不拋出例外,如果允許任務丟失,這是最好的?種策略,
讀寫鎖的實作類
ReentrantReadWriteLock、ReadWriteLockView in StampedLock
公平鎖和非公平鎖
公平鎖保證了FIFO原則但是耗費大量資源在背景關系切換,而非公平鎖則保證了極大的吞吐量和效率,但可能造成饑餓,
CAS
compareAndSet,自旋保證原子操作,和volatile關鍵字配合使用,
ABA問題,
重排序
volatile,記憶體屏障,final
AQS
acquire方法中:tryAcquire()—失敗—addWaiter()加入等待佇列—addQueued()再嘗試一次失敗就阻塞,
只有前節點喚醒或者中斷才會繼續執行,
volatile
記憶體屏障保證有序性,總線嗅探機制保證可見性,
偽共享:volatile修飾變數需要更新時,其他和volatile修飾變數在同一快取行的變數也需要重新獲取,性能降低,通過添加一些long的變數來填充快取行,
創建執行緒的方式
①繼承自Thread重寫run方法②實作Runnable介面并實作run方法③實作Callerable介面,實作call方法,可有回傳值
TLAB
預設情況下僅占有整個Eden空間的1%
LockSupport
park()、unpark(Thread t)、parkNaos、parkUtil
Condition介面
await方法原理:將當前獲得鎖的執行緒從同步佇列移到等待佇列最后,并且釋放同步狀態,
signal:將當前condition對應的等待佇列的首節點放入到同步佇列最后,然后通過unpark喚醒執行緒,進而執行緒通過acquireQueued方法嘗試獲取同步狀態,
樂觀鎖、悲觀鎖:
容易發生沖突且沖突量大時使用悲觀鎖,否則樂觀鎖(多讀少寫),
資料庫等適合悲觀鎖,
concurrent包下有哪些:
java.util.concurrent包下包含:tools、locks、collections、executor、atomic
其中tools包含CountDownLatch、Semaphore、Executors、Exchanger等等
locks則是包含了Lock、Condition、LockSupport、ReadWriteLock
collections則是一些支持并發的集合:阻塞佇列、ConcurrentHashMap、ConcurrentSkipList等等
executors則是執行緒池,atomic為原子類,
Java執行緒的通信方式
volatile
等待/通知機制
join方式
threadLocal
ThreadLocal
首先,在每個執行緒Thread內部有一個ThreadLocal.ThreadLocalMap型別的成員變數threadLocals,這個threadLocals就是用來存盤實際的變數副本的,鍵值為當前ThreadLocal變數,value為變數副本(即T型別的變數),
初始時,在Thread里面,threadLocals為空,當通過ThreadLocal變數呼叫get()方法或者set()方法,就會對Thread類中的threadLocals進行初始化,并且以當前ThreadLocal變數為鍵值,以ThreadLocal要保存的副本變數為value,存到threadLocals,
①實際的通過ThreadLocal創建的副本是存盤在每個執行緒自己的threadLocals中的;
② 為何threadLocals的型別ThreadLocalMap的鍵值為ThreadLocal物件,因為每個執行緒中可有多個threadLocal變數,就像上面代碼中的longLocal和stringLocal;
③ 在進行get之前,必須先set,否則會報空指標例外;
如果想在get之前不需要呼叫set就能正常訪問的話,必須重寫initialValue()方法,
ThreadLoacl 類、記憶體泄漏(key是弱參考 ,value是強參考) 每次使用后remove,
由于Thread中包含變數ThreadLocalMap,因此ThreadLocalMap與Thread的生命周期是一樣長,如果都沒有手動洗掉對應key,都會導致記憶體泄漏,
但是使用弱參考可以多一層保障:弱參考ThreadLocal不會記憶體泄漏,對應的value在下一次ThreadLocalMap呼叫set(),get(),remove()【原始碼保證】的時候會被清除,
因此,ThreadLocal記憶體泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一樣長,如果沒有手動洗掉對應key就會導致記憶體泄漏,而不是因為弱參考,
解決方案:
- 每次使用完ThreadLocal都呼叫它的remove()方法清除資料
- 將ThreadLocal變數定義成private static,這樣就一直存在ThreadLocal的強參考,也就能保證任何時候都能通過ThreadLocal的弱參考訪問到Entry的value值,進而清除掉 ,
jdk原始碼
ArrayList
底層陣列,非執行緒安全
擴容:懶加載(但在jdk7及以前會直接初始化一個容量為10的陣列),擴容時先擴容1.5倍,如果無法滿足,則將需要滿足的容量作為新容量,
modCount作用:記錄了集合的操作次數,在使用迭代器迭代的程序中,每次都要查一下modCount是否變化,如果變化了,迭代器在處理時可能出現不可預知的情況,因此只要這個值變化了,迭代器就拋出例外,所以modCount的作用是迭代器在遍歷時做執行緒安全檢查的,
add:將指定位置后面的陣列進行后移一位(System.arrayCopy),然后插入新元素
remove:判斷是否為最后一位,不為的話就指定位置后的陣列前移一位,然后去除最后一個元素
RandomAccess介面,表示支持隨機訪問,無定義方法,
AQS
CLH佇列 + volatile修飾的state變數
LockSupport + CMS完成
可重入
兩種資源共享方式:獨占(ReentrantLock)和共享(Semaphore/CountDownLatch)
accquire —— tryAccquire(嘗試獲取鎖) —— addWaiter(cms添加到等待佇列最后) —— accquireQueued(還會嘗試一次獲取鎖)
共享模式下:doAcquireShared —— setHeadAndPropagate(將head指向自己,還有剩余資源可以再喚醒之后的執行緒)
由前序節點 或者 中斷 喚醒等待中的執行緒
這篇博客講的很好
PriorityQueue
堆排序,底層陣列
見資料結構
TreeMap
紅黑樹
HashMap
JVM
JVM記憶體結構
class檔案——類加載子系統——運行時資料區(方法區、堆、虛擬機堆疊、本地方法堆疊、PC暫存器)——執行引擎——本地方法庫
垃圾回收演算法
標記:參考計數和可達性分析
標記-清除、標記-整理、復制演算法
垃圾回收器:CMS、G1、ParNew、Serial Old、Parellel Old
類加載程序
加載(已經存在Class物件)——鏈接(驗證、準備、決議,準備階段對類變數賦默認值)——初始化(clinit方法)
類加載機制
- 隱式加載 new 創建類的實體,
- 顯式加載:loaderClass,forName等
- 訪問類的靜態變數,或者為靜態變數賦值
- 呼叫類的靜態方法
- 使用反射方式創建某個類或者介面物件的Class物件,
- 初始化某個類的子類
- 直接使用
java.exe命令來運行某個主類
雙親委派模型
- 如果一個類加載器收到了類加載請求,它并不會自己先去加載,而是把這個請求委托給父類豳加載器去執行;
- 如果父類加載器還存在其父類加載器,則進一步向上委托,依次遞回,請求最終將到達頂層的啟動類加載器
- 如果父類加載器可以完成類加載任務,就成功回傳,倘若父類加載器無法完成此加載任務,子加載器才會嘗試自己去加載,這就是雙親委派模式,
破壞雙親委派機制
在Java應用中存在著很多服務提供者介面(Service Provider Interface,SPI),這些介面允許第三方為它們提供實作,如常見的 SPI 有 JDBC、JNDI等,這些 SPI 的介面屬于 Java 核心庫,一般存在rt.jar包中,由Bootstrap類加載器加載,而 SPI 的第三方實作代碼則是作為Java應用所依賴的 jar 包被存放在classpath路徑下,由于SPI介面中的代碼經常需要加載具體的第三方實作類并呼叫其相關方法,但SPI的核心介面類是由引導類加載器來加載的,而Bootstrap類加載器無法直接加載SPI的實作類,同時由于雙親委派模式的存在,Bootstrap類加載器也無法反向委托AppClassLoader加載器SPI的實作類,在這種情況下,我們就需要一種特殊的類加載器來加載第三方的類別庫,而執行緒背景關系類加載器就是很好的選擇,
執行緒背景關系類加載器(contextClassLoader)是從 JDK 1.2 開始引入的,我們可以通過java.lang.Thread類中的getContextClassLoader()和 setContextClassLoader(ClassLoader cl)方法來獲取和設定執行緒的背景關系類加載器,如果沒有手動設定背景關系類加載器,執行緒將繼承其父執行緒的背景關系類加載器,初始執行緒的背景關系類加載器是系統類加載器(AppClassLoader),在執行緒中運行的代碼可以通過此類加載器來加載類和資源
沙箱安全機制
假如自定義java.lang.String類,但是在加載自定義String類的時候會率先使用引導類加載器加載,而引導類加載器在加載的程序中會先加載jdk自帶的檔案(rt.jar包中java\lang\String.class),報錯資訊說沒有main方法,就是因為加載的是rt.jar包中的String類,這樣可以保證對java核心源代碼的保護,這就是沙箱安全機制,
ClassLoader的方法
getParent()獲取父加載器、loadClass(String name)加載指定name的類并回傳Class物件、findClass(String name)查找指定name的類并回傳Class物件、 findLoadedClass(String name)查找已經加載的類并回傳、defineClass(String name, byte[] b, int off, int len)將位元組陣列中的內容轉換為一個類、resolveClass(Class<?> c)鏈接指定的一個類,
遵循雙親委派——重寫findClass方法即可、打破雙親委派——重寫loadClass方法
OOM如何分析,一些分析工具,常用命令
jvisualVM、jprofiler
jmap、jflag、jinfo、jstate
堆和堆疊的區別
堆中存在OOM和GC,堆疊中只存在OOM,堆是存盤的單位,堆疊是運行時的單位,
物件頭
MarkWord(hashcode、鎖標志位、分代年齡、是否偏向鎖、偏向執行緒id等等)
元資料指標(指向方法區的型別資料資訊)
陣列長度(如果是陣列的話)
物件實體化程序
①查看對應的類資訊是否加載②計算所需記憶體并分配空間③并發問題(CAS)分配TLAB④默認初始化⑤設定物件頭⑥顯示初始化
NIO
直接記憶體,DirectByteBuffer操作本地記憶體,沒有中間狀態,IO多路復用
String不可變原理:
final修飾char陣列(jdk9后采用byte陣列),
原因:①字串常量池②String快取了自身的hashcode,如果可變但hash沒變,散列會存在問題③String會作為引數
StringBuffer執行緒安全,synchronized修飾,StringBuilder性能更高,
String str1=“a”;String str2=“a”+“bc”;
都在字串常量池中創建一個物件,
String a = new String(“11”)+new String(“22”);創建了幾個物件?
new String(“xx”)都會創建兩個物件,然后如果+兩邊存在變數,那么都會存在一個StringBuilder物件,StirngBuilder還會通過toString方法再創建一個物件,
ClassNotFoundException場景
1、呼叫class的forName方法時,找不到指定的類
2、ClassLoader 中的 findSystemClass() 方法時,找不到指定的類
3、ClassLoader 中的 loadClass() 方法時,找不到指定的類
GC ROOTS
在Java語言中,GC Roots包括以下幾類元素:
①虛擬機堆疊中參考的物件,比如:各個執行緒被呼叫的方法中使用到的引數、區域變數等,
②本地方法堆疊內JNI(通常說的本地方法)參考的物件
③方法區中類靜態屬性參考的物件,比如: Java類的參考型別靜態變數
④方法區中常量參考的物件,比如:字串常量池(string Table)里的參考
⑤所有被同步鎖synchronized持有的物件
⑥Java虛擬機內部的參考,基本資料型別對應的class物件,一些常駐的例外物件(如:NullPointerException、OutOfMemoryError)、系統類加載器
⑦反映java虛擬機內部情況的JMXBean、JVMTI中注冊的回呼、本地代碼快取等,
⑧在某些特殊情況下,還可能存在一些物件臨時加入到Root中的情況,(如:在分代垃圾收集時,如果只回收新生代的物件,那么一些老年代的物件也可以作為Root)
跨代參考問題
常用jvm引數
-Xms, -Xmx
-XX:MetaspaceSize; -XX:MaxMetaspaceSize / -XX:PermSize
-XX:+PrintGCDetails
-XX:NewRatio: 配置新生代與老年代在堆結構的占比
-XX:SurvivorRatio: 設定新生代中Eden和s0/s1空間的比例
OOM排查
1、先查看應用行程號pid: ps -ef | grep 應用名 2、查看pid垃圾回收情況: jstat -gc pid 5000(時間間隔) 3、開啟OOM快照: -XX:+HeapDumpOnOutOfMemoryError(開啟堆快照) -XX:HeapDumpPath=C:/m.hprof(保存檔案到哪個目錄) 4、dump 查看方法堆疊資訊: jstack -l pid > /home/test/jstack.txt 5、dump 查看JVM記憶體分配以及使用情況 jmap -heap pid > /home/test/jmapHeap.txt 6、dump jvm二進制的記憶體詳細使用情況 jmap -dump:format=b,file=/home/test/oom.hprof pid
Spring
java連接資料庫 jdbc原生
public static void conn() { String URL = "jdbc:mysql://127.0.0.1:3306/Supermarket?characterEncoding=utf-8"; String USER = "root"; String PASSWORD = "123"; // 1.加載驅動程式 try { Class.forName("com.mysql.jdbc.Driver"); // 2.獲得資料庫鏈接 Connection conn = DriverManager.getConnection(URL, USER, PASSWORD); // 3.通過資料庫的連接操作資料庫,實作增刪改查(使用Statement類) String name="張三"; //預編譯 String sql="select * from userinfo where UserName=?"; PreparedStatement statement = conn.prepareStatement(sql); statement.setString(1, name); ResultSet rs = statement.executeQuery(); // String sql="select * from userinfo where UserName='"+name+"'"; // Statement statement = conn.createStatement(); // ResultSet rs = statement.executeQuery(sql); // 4.處理資料庫的回傳結果(使用ResultSet類) while (rs.next()) { System.out.println(rs.getString("UserName") + " " + rs.getString("Password")); } // 關閉資源 conn.close(); rs.close(); statement.close(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (SQLException e) { e.printStackTrace(); } }
IOC AOP
IOC: Inversion of Control,控制反轉 DI: Dependency Injection,依賴注入 關系:IOC是一種面向編程設計思想,DI是IOC思想的實作方式,即:DI實作IOC這一思想 A需要B,A不直接控制B,A給IOC容器需要的資訊,然后IOC容器為A創建B, 主動獲取反轉為被動獲取,解耦, AOP: jdk or cglib JDK動態代理主要涉及java.lang.reflect包下邊的兩個類:Proxy和InvocationHandler,其中,InvocationHandler是一個介面,可以通過實作該介面定義橫切邏輯,并通過反射機制呼叫目標類的代碼,動態地將橫切邏輯和業務邏輯貶值在一起, 所以使用JDK動態代理的話,他有一個限制,就是它只能為介面創建代理實體,而對于沒有通過介面定義業務方法的類,如何創建動態代理實體呢?答案就是CGLib, CGLib采用底層的位元組碼技術,全稱是:Code Generation Library,CGLib可以為一個類創建一個子類,在子類中采用方法攔截的技術攔截所有父類方法的呼叫并順勢織入橫切邏輯, 在spring中,框架會根據目標類是否實作了介面來決定采用哪種動態代理的方式, ----------------------------------------------------------------------- JDK的動態代理 final Advice advice = new Advice(); // 獲得增強物件 final Target target = new Target(); // 回傳值就是生成的動態代理物件 TargetInterface proxy = (TargetInterface) Proxy.newProxyInstance( target.getClass().getClassLoader(), // 目標物件的類加載器 target.getClass().getInterfaces(), // 目標物件相同的介面位元組碼物件陣列 new InvocationHandler() { // 呼叫代理物件的任何方法,實質執行的為invoke方法 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { advice.before();// 前置增強 method.invoke(target, args);// 執行目標方法 advice.after(); return null; // 該回傳值對于方法本身而言 無意義 } } ); proxy.save(); // 呼叫代理物件的方法 cglib的動態代理 final Advice advice = new Advice(); // 獲得增強物件 final Target target = new Target(); // 回傳值就是生成的動態代理物件 基于cglib // 1.創建增強器 Enhancer enhancer = new Enhancer(); // 2.設定父類 (目標) enhancer.setSuperclass(Target.class); // 3.設定回呼 enhancer.setCallback(new MethodInterceptor() { public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { advice.before(); // 執行前置 Object invoke = method.invoke(target, args); advice.after(); // 執行后置 return invoke; } }); // 4.生成代理物件 Target proxy = (Target) enhancer.create(); proxy.save();
Spring加載bean的流程
class物件——實體化得到原始bean物件——屬性注入——初始化——放入單例池——銷毀
三級快取與回圈依賴
class物件——實體化——放入三級快取——屬性注入——需要的屬性先查找一級快取、然后查找二級快取、然后查找三級快取,放入二級快取、洗掉三級快取,
三級快取記憶體存的是函式式介面,
構造器的回圈依賴和prototype的回圈依賴無法解決,(?)
Bean生命周期
實體化Bean --> 屬性注入 --> [Aware(xxxXxxAware介面) -->] 初始化 --> 銷毀
大的流程大概為四步,如上,后續補充:
1、InstantiationAwareBeanPostProcessor介面(postProcessBeforeInstantiation、postProcessAfterInstantiation)
- 該兩介面作用于實體化Bean的前后
2、BeanPostProcessor介面(postProcessBeforeInitialization、postProcessAfterInitialization)
- 該兩介面作用于初始化的前后
- 所有的Aware介面注入也就是在postProcessBeforeInitialization中完成的
3、InitializingBean和DisposableBean對應初始化和銷毀階段
- InitializingBean方法中可以使用在Aware階段所獲得的資源,
4、需要AOP時:
實體化Bean --> 屬性注入 --> [Aware(xxxXxxAware介面) -->] 初始化 --> AOP --> 單例池(ConcurrentHashMap<BeanName, 物件>)
5、廣義:
class --> BeanDefinition --> 實體化Bean --> 屬性注入 --> [Aware(xxxXxxAware介面) -->] 初始化 --> AOP --> 單例池(ConcurrentHashMap<BeanName, 物件>)
6、BeanDefinition
而BeanDefinition實際上是通過決議最初提供的class得到的,其中包含了bean的scope等等資訊,同時我們可以通過實作BeanFactoryPostProcessor介面并實作postProcessBeanFactory方法對其中的資訊進行修改,(例如我們可以將BeanDefinition中的beanClass改變,這樣創建bean實體時創建的也就是改變后的class對應的物件實體了)
而BeanFactoryPostProcessor是在BeanFactory組建完成后才會呼叫的,而BeanFactory組建完成是指所有類對應的BeanDefinition都創建完成并且存入對應的ConcurrentMap后,
而BeanFactory創建bean的程序也就是從BeanDefinitionMap中拿到對應的BeanDefinition然后進行實體化bean,并放入單例池,
注:BeanDefinition可參考下圖,
7、FactoryBean
FactoryBean是一個介面,當在IOC容器中的Bean實作了FactoryBean后,通過getBean(String BeanName)獲取到的Bean物件并不是FactoryBean的實作類物件,而是這個實作類中的實作的getObject()方法回傳的物件,如下圖的MapperFactoryBean,而如果要想獲取FactoryBean的實作類,就要getBean(&BeanName),在BeanName之前加上&,
8、ImportBeanDefinitionRegistrar
ImportBeanDefinitionRegistrar是一個介面,實作該介面的類需要實作registerBeanDefinitions方法,該方法可以將Spring中沒有通過@Component修飾的介面通過@Import注解的方式插入到registerBeanDefinitions方法的引數中,然后通過該方法生成BeanDefinition,并通過方法的registry引數注冊到BeanDefinitionMap中也就是BeanFactory中,然后進行后面Bean的實體化,即不是通過ComponentScan掃描并自動生成BD的,

解決回圈依賴(三級快取)
Spring創建bean的流程如下:
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); // 一級快取 單例池
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16); // 二級快取 存放代理物件
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16); // 三級快取 存放lambda
大致流程:
- 先從一級快取singletonObjects中去獲取,(如果獲取到就直接return)
- 如果獲取不到或者物件正在創建中(isSingletonCurrentlyInCreation()),那就再從二級快取earlySingletonObjects中獲取,(如果獲取到就直接return)
- 如果還是獲取不到,且允許singletonFactories(allowEarlyReference=true)通過getObject()獲取,就從三級快取singletonFactory.getObject()獲取,(如果獲取到了就將其從三級快取中移除,并且放進二級快取earlySingletonObjects,)
Spring、MyBatis整合
通過注解對mapper介面進行掃描并獲得對應包名和介面,然后進行BD的注冊,注冊完后根據介面型別等資訊對BD進行處理,即改變BD生成bean的方式(通過實作BeanFactory的方式來生成),通過MapperBeanFactory進行后續的bean的生成,
BeanFactory和FactoryBean
FactoryBean是一個介面,當在IOC容器中的Bean實作了FactoryBean后,通過getBean(String BeanName)獲取到的Bean物件并不是FactoryBean的實作類物件,而是這個實作類中的實作的getObject()方法回傳的物件,如下圖的MapperFactoryBean,而如果要想獲取FactoryBean的實作類,就要getBean(&BeanName),在BeanName之前加上&,
BeanFactory是一個介面,Spring內部實作了很多類,存放很多東西,如definitionmap、singletonmap等等,BeanFactory是個Factory,也就是IOC容器或物件工廠,在Spring中,所有的Bean都是由BeanFactory(也就是IOC容器)來進行管理的
@Autowired和@Resource區別
@Autowired//默認按type注入
@Qualifier(“cusInfoService”)//一般作為@Autowired()的修飾用
@Resource(name=“cusInfoService”)//默認按name注入,可以通過name和type屬性進行選擇性注入
@Inject和@Autowired
@Inject是Java EE 6(JSR-299)中引入的Java CDI(背景關系和依賴項注入)標準的一部分
@Autowired則是Spring框架的一部分,
前者自動裝配的bean范圍是singleton,后者則是原型
前者和@Named一起使用,后者和@Qualifier一起使用,
全域例外處理
繼承HandlerExceptionResolver介面,并將實作類作為bean進行注冊到Spring,在resolveException中實作處理邏輯,
SpringBoot怎么在服務端接收到HTTP請求后,再轉發到控制層?
filter->dispatchServlet(doService->doDispath)->intercepter(prehandle攔截器)->RequestMappingHandler(查找映射)->ServletInvocableHandlerMethod: invokeAndHandle(引數處理)->通過java 反射機制動態呼叫目標API方法進入相對應具體的controller->執行 handleReturnValue 方法, 首先會判斷是否需要對response entity 進行二次處理(ResponseBodyAdvice: beforeBodyWrite), 處理完成后, 呼叫注冊進來的MessageConvertor 對回傳資訊進行轉換處理, 比如使用faster Jackson 把物件型別轉成 json 字串,并以application/json 的形式回傳 http response -> 攔截器 postHandle 方法, 最后執行攔截器的afterCompletion 方法,
按照 Servlet 規范,所有請求都會被tomcat容器交到 dispatchServlet 的 doService 方法中去處理,跟到這個方法中去,我們發現其中設定了變數進 request 物件,然后執行了 doDispatch 方法,這個方法才是真正實作請求處理的核心,
該doDispatch方法中呼叫 getHandler 找到 url 匹配的 handler 方法(示例代碼中的 hello 方法),然后呼叫 ha.handle() 來獲得處理結果,
對于getHandler 方法,是通過 HandlerMapping 介面物件的集合物件來操作的,HandlerMapping 介面要求實作類實作從請求到處理物件的映射的方法,以 RequestMappingHandlerMapping 實作為例,它底層注冊了一個 url -> handler方法的 map,每當請求過來,就會根據請求的url 去 map 中匹配,匹配到對應的handler 方法,
Spring中的設計模式
簡單工廠模式——BeanFactory
工廠方法模式——FactoryBean
單例模式——單例池,bean(singleton)
配接器模式——AOP
包裝器模式——Wrapper、Decorator
代理模式——AOP動態代理
觀察者模式——
策略模式——
SpringMVC
什么是MVC
Controller(控制器)、Model(業務模型)、View(用戶視圖)實作代碼分離,Controller用于同步Model和View
SpringMVC流程
(1)用戶發送請求至前端控制器DispatcherServlet;
(2) DispatcherServlet收到請求后,呼叫HandlerMapping處理器映射器,請求獲取Handle;
(3)處理器映射器根據請求url找到具體的處理器,生成處理器物件及處理器攔截器(如果有則生成)一并回傳給DispatcherServlet;
(4)DispatcherServlet 呼叫 HandlerAdapter處理器配接器;
(5)HandlerAdapter 經過適配呼叫 具體處理器(Handler,也叫后端控制器);
(6)Handler執行完成回傳ModelAndView;
(7)HandlerAdapter將Handler執行結果ModelAndView回傳給DispatcherServlet;
(8)DispatcherServlet將ModelAndView傳給ViewResolver視圖決議器進行決議;
(9)ViewResolver決議后回傳具體View;
(10)DispatcherServlet對View進行渲染視圖(即將模型資料填充至視圖中)
(11)DispatcherServlet回應用戶,
SpringMVC實作回傳json
通過一些json框架如(Jackson),并在方法前加上@ResponseBody即可
解決post、get亂碼問題
post:在web.xml中配置一個CharacterEncodingFilter過濾器,設定成utf-8;
get:①修改tomcat組態檔添加編碼與工程編碼一致;②對傳過來的引數進行重新編碼
SpringMVC例外處理
可以將例外拋給Spring框架,由Spring框架來處理;我們只需要配置簡單的例外處理器,在例外處理器中添視圖頁面即可,
SpringMVC控制器
是單例的,多執行緒存在執行緒安全問題,不使用同步,會影響性能,在控制器中不寫欄位來保證執行緒安全,
SpringMVC常用注解
@RequestMapping:用于處理請求 url 映射的注解,可用于類或方法上,用于類上,則表示類中的所有回應請求的方法都是以該地址作為父路徑,
@RequestBody:注解實作接收http請求的json資料,將json轉換為java物件,
@ResponseBody:注解實作將conreoller方法回傳物件轉化為json物件回應給客戶,@Controller、@RestController(@Controller + @ResponseBody)
如何在方法中得到session、request物件
直接在方法中宣告這個物件,SpringMvc就自動會把屬性賦值到這個物件里面,
SpringMvc用什么物件從后臺向前臺傳遞資料的?
通過ModelMap物件,可以在這個物件里面呼叫put方法,把物件加到里面,前臺就可以通過el運算式拿到,
怎么樣把ModelMap里面的資料放入Session里面?
可以在類上面加上@SessionAttributes注解,里面包含的字串就是要放入session里面的key,
SpringMvc里面攔截器是怎么寫的?
有兩種寫法,一種是實作HandlerInterceptor介面,另外一種是繼承配接器類,接著在介面方法當中,實作處理邏輯;然后在SpringMVC的組態檔中配置攔截器即可
注解原理
注解本質是一個繼承了
Annotation的特殊介面,其具體實作類是Java運行時生成的動態代理類,我們通過反射獲取注解時,回傳的是Java運行時生成的動態代理物件,通過代理物件呼叫自定義注解的方法,會最終呼叫AnnotationInvocationHandler的invoke方法,該方法會從memberValues這個Map中索引出對應的值,而memberValues的來源是Java常量池,
Controller區域例外處理
①在某個方法上方使用@ExceptionHandler()注解,并給出想要處理的例外型別,然后該方法就會作為該Controller的例外處理方法
②定義一個例外處理類,并使用@ControllerAdvice()注解修飾,并給出想要處理的Controller,可以傳入一個介面class物件,表示實作了該介面的Controller的例外都由該例外類處理,內部的exceptionHandler和方法①一致,
SpringBoot
組態檔裝載順序
①application.properties優先級大于application.yml
②先去專案根目錄找config檔案夾下找組態檔件;再去根目錄下找組態檔;去resources下找cofnig檔案夾下找組態檔;去resources下找組態檔
③如果高優先級的組態檔和低優先級的組態檔中屬性不沖突,則可以實作互補配置,
④外部配置:如cmd命令,或者系統屬性System.getProperties();同樣可以形成互補配置
Redis
單執行緒原因:
- 單執行緒編程容易并且更容易維護;
- Redis 的性能瓶頸不再 CPU ,主要在記憶體和網路;
- 多執行緒就會存在死鎖、執行緒背景關系切換等問題,甚至會影響性能,
快取淘汰策略
lru(最近最少未使用)、ttl(時間)、lfu(使用頻率)、不驅逐
布隆過濾器原理
bitmap + 多次hash
對于hash結果不正確的,一定不存在,反之則不是,可能存在也可能不存在,
一致性hash
hash環(對2^32而不是一個固定值進行hash),保證即使hash結果數要求發生變化,只用改變hash環上的分割即可,可使用虛擬節點進行hash環分配不均的改良,
虛擬節點即將單個節點虛擬為多個節點,使得資料能夠平均分布在各個節點上,
記憶體模型
k-v結構,本質記憶體模型就是為底層的字典結構,參考下方
redis和java字典的區別
(1)Redis中有縮容,Java中沒有
(2)Redis中的擴容不是一次完成的,可以分多次,是漸進式地,而Java的是一次完成的
持久化機制
AOF、RDB
RDB其實就是把資料以快照的形式保存在磁盤上,什么是快照呢,你可以理解成把當前時刻的資料拍成一張照片保存下來,Redis調forks,同時擁有父行程和子行程,子行程將資料集寫入到一個臨時RDB檔案中,當子行程完成對新RDB檔案的寫入時,Redis用新RDB檔案替換原來的RDB檔案,并洗掉舊的RDB檔案, RDB持久化是指在指定的時間間隔內將記憶體中的資料集快照寫入磁盤,也是默認的持久化方式,這種方式是就是將記憶體中資料以快照的方式寫入到二進制檔案中,默認的檔案名為dump.rdb, 三種觸發機制save、bgsave、自動化, save: 阻塞redis,用新的RDB替換舊的并直到程序完成才繼續 bgsave: Redis會在后臺異步進行快照操作,快照同時還可以回應客戶端請求,具體操作是Redis行程執行fork操作創建子行程,RDB持久化程序由子行程負責,完成后自動結束,阻塞只發生在fork階段,一般時間很短,基本上 Redis 內部所有的RDB操作都是采用bgsave命令, 自動化:更改組態檔,它在“N 秒內資料集至少有 M 個改動”這一條件被滿足時, 自動進行資料集保存操作, RDB是一個非常緊湊的檔案,方便傳輸 快照持久化期間修改的資料不會被保存,可能丟失資料, ---------------------------------------------------------------------------------------------------- AOF:redis會將每一個收到的寫命令都通過write函式追加到檔案中,通俗的理解就是日志記錄,每當Redis執行一個改變資料集的命令時(比如 SET), 這個命令就會被追加到AOF檔案的末尾,這樣的話, 當Redis重新啟時, 程式就可以通過重新執行AOF檔案中的命令來達到重建資料集的目的 為了壓縮aof的持久化檔案,redis提供了bgrewriteaof命令,將記憶體中的資料以命令的方式保存到臨時檔案中,同時會fork出一條新行程來將檔案重寫, 每次修改同步always:同步持久化,每次發生資料變更會被立即記錄到磁盤,性能較差但資料完整性比較好 每秒同步everysec:異步操作,每秒記錄 如果一秒內宕機,有資料丟失 不同no:從不 fsync :將資料交給作業系統來處理,由作業系統來決定什么時候同步資料,更快,也更不安全的選擇, AOF重寫:因為 AOF 的運作方式是不斷地將命令追加到檔案的末尾, 所以隨著寫入命令的不斷增加, AOF 檔案的體積也會變得越來越大,舉個例子, 如果你對一個計數器呼叫了 100 次 INCR , 那么僅僅是為了保存這個計數器的當前值, AOF 檔案就需要使用 100 條記錄(entry),然而在實際上, 只使用一條 SET 命令已經足以保存計數器的當前值了, 其余 99 條記錄實際上都是多余的, 為了處理這種情況, Redis 支持一種有趣的特性: 可以在不打斷服務客戶端的情況下, 對 AOF 檔案進行重建(rebuild),執行 bgrewriteaof 命令, Redis 將生成一個新的 AOF 檔案, 這個檔案包含重建當前資料集所需的最少命令,Redis 持久化 之 AOF 和 RDB 同時開啟,Redis聽誰的?
AOF
快取雪崩、快取擊穿、快取穿透
快取雪崩是指快取同一時間大面積的失效(也可能為redis重啟),所以,后面的請求都會落到資料庫上,造成資料庫短時間內承受大量請求而崩掉,
快取穿透是指快取和資料庫中都沒有的資料,導致所有的請求都落到資料庫上,造成資料庫短時間內承受大量請求而崩掉,(一般出現于被攻擊或者電商中高并發的場景)
快取擊穿是指快取中沒有但資料庫中有的資料(一般是快取時間到期),這時由于并發用戶特別多,同時讀快取沒讀到資料,又同時去資料庫去取資料,引起資料庫壓力瞬間增大,造成過大壓力,和快取雪崩不同的是,快取擊穿指并發查同一條資料,快取雪崩是不同資料都過期了,很多資料都查不到從而查資料庫,
底層資料結構
字串:SDS(char陣列buf + len + 未使用的空間大小free),可以動態擴容(預分配),惰性空間釋放(改變free的值而非回收記憶體),
鏈表:雙向鏈表,表頭前置為null,表尾后置為null
字典:新增時,先根據鍵值對的鍵計算出哈希值,然后根據 sizemask 屬性和哈希值,計算索引值——即落入陣列中的哪個位置,之后如果有一個位置多個鍵值對要存入時,組成單向鏈表即可,
這里和 HashMap 的不同之處在于,鏈表添加時總是添加在表頭位置,因為 dictEntry 節點組成的鏈表沒有指向鏈表表尾的指標,為了速度考慮,總是將新節點加在鏈表的表頭位置,(為什么要這樣,而不是遍歷完整個鏈表后加在鏈表尾部,不遍歷出現重復鍵怎么辦?)
rehash: rehash 也可以參考 Java 中 HashMap 的原理, 負載因子 = 哈希表中已保存的節點數量 / 哈希表陣列大小, 當哈希表中存放的鍵值對不斷增多或減少,為了讓負載因子在一個合理的范圍內,需要對大小進行擴展或者收縮,(這里類似 HashMap 中的重新散列方法) 1. 字典的 ht[1] 分配空間,空間的大小由 ht[0] 已經使用的鍵值對數量以及執行的擴張和收縮來決定, - 擴展操作,那么 ht[1] 分配的空間大小應是比當前 ht[0].used 值的二倍大的第一個 2 的整數冪,(比如當前使用空間 14,那么找 28 的下一個 2 的整數冪,為 32) - 收縮操作,取 ht[0].used 的第一個大于等于的 2 的整數冪,(比如 14,那么就是 16) 2. 將 ht[0] 中的所有鍵值對,rehash 到 ht[1] 上面:根據新的大小來重新計算所有鍵的哈希和索引,映射到新陣列的指定位置上, 3. ht[0] 的所有鍵值對都遷移到 ht[1] 之后,釋放 ht[0] ,然后將 ht[1] 設定為 ht[0] ,然后在 ht[1] 處新創建空白哈希表,為下一次 rehash 做準備, 擴展的條件 服務器沒有執行 BGSAVE 或者 BGREWRITEAOF 命令,并且哈希表的負載因子大于等于 1 , 服務器正在執行 BGSAVE 或者 BGREWRITEAOP 命令,并且哈希表的負載因子大于等于 5 , 這兩種情況根據是否有后臺命令執行來區分,是因為在執行 BGSAVE 或者 BGREWRITEAOF 的程序中,Redis 需要創建當前服務器行程的子行程,而大多數作業系統都采用寫時復制(copy-on-write)技術來優化子行程的使用效率,所以在子行程存在期間,服務器會提高執行擴展操作所需的負載因子,盡可能避免在子行程存在期間進行哈希表的擴展操作,來避免不必要的記憶體寫入操作,最大限度的節省記憶體, 收縮的條件 當哈希表的負載因子小于 0.1 時,自動開始對哈希表進行收縮操作, 漸進式rehash: 如果鍵值對量巨大時,一次性全部 rehash 必然造成一段時間的停止服務,所以要分多次、漸進式的將鍵值對從 ht[0] 慢慢的 rehash 到 ht[1] 中, 具體程序: 1. 為 ht[1] 分配空間,同時有 ht[0] 和 ht[1] 兩個哈希表, 2. 在字典中維持一個索引計數器變數 rehashindex ,并將其置為 0 ,表示 rehash 正式開始, 3. 在 rehash 期間,每次對字典執行添加、洗掉、查找或者更新操作時,程式除了執行指定的操作之外,還會順便將 ht[0] 哈希表在 rehashindex 索引上的所有鍵值對 rehash 到 ht[1] 上,當 rehash 作業完成之后,程式將 rehashindex 的值加一, 4. 隨著字典操作的不斷進行,最終在某個時間點,ht[0] 的所有鍵值對都被 rehash 到 ht[1] ,這時程式將 rehashindex 的值置為 -1 ,表示 rehash 作業完成, 漸進式 rehash 的程序中,更新洗掉查找等都會在兩個哈希表上進行,比如查找,先在 ht[0] 中查找,如果沒找到,就去 ht[1] 中查找,而新增操作,直接新增在 ht[1] 中,ht[0] 不會進行任何的新增操作,保證 ht[0] 的數量只減不增,最終變為空表,
跳表:跳躍表是一種有序資料結構,通過在每個節點中維持多個指向其他節點的指標,從而達到快速訪問節點的目的,Redis 使用跳躍表作為有序集合鍵的底層實作之一,
跳躍表在 Redis 中,只有兩個地方用到:一個是實作有序集合物件,另一個是在集群節點中用作內部資料結構,head:指向跳躍表的表頭節點, tail:指向跳躍表的表尾節點, level:記錄當前跳躍表中,層數最高的節點的層數(表頭節點的層數不計算), length:記錄跳躍表的長度,即包含節點的數量, level:每一層都有前進指標和跨度,從頭到尾遍歷時,訪問會沿著層的前進指標進行, BW:后退指標,指向前一個節點,從尾到頭遍歷時使用, score:分值,跳躍表中的分值按從小到大排列, obj:成員物件,各個節點保存有各個成員物件,
整數集合:整數集合是集合鍵的底層實作之一,當一個集合只包含整數值元素,并且這個集合的元素數量不多時,Redis 就會使用整數集合作為集合鍵的底層實作,
整數集合是 Redis 保存整數值的集合的抽象資料結構,可以保存 int16_t ,int32_t ,int64_t 的整數值,并且集合中不會出現重復元素,
底層由陣列實作,整數集合的每個元素都是陣列的一個陣列項,各個項在陣列中按從小到大排列,length 屬性記錄了包含的元素數量,即陣列的長度,升級: 當一個新元素添加到整數集合中時,如果新元素型別比整數集合現有的所有元素的型別都要長時,整數集合要先進行升級,然后才能將新元素添加到整數集合中, 1. 根據新元素型別,擴展整數集合底層陣列的大小,并為新元素分配空間, 2. 將底層陣列現有的所有元素都轉換成新元素相同的型別,并將型別轉換后的元素放置到正確位置上,而且放置程序中需要維持底層陣列的有序, 3. 將新元素添加到底層陣列中, 因為引發升級的新元素的長度肯定比現有所有元素都大,才會出現升級的情況,所以這個值要么大于所有元素,放置的位置就對應新陣列的末尾;要么小于所有元素,放置的位置在陣列的開頭, 升級可以提高靈活性,不用擔心型別錯誤,可以隨意添加不同型別的元素,另外,可以節約記憶體,只在有需要的時候進行升級, 另外,整數集合不支持降級操作,壓縮串列
壓縮串列(ziplist)是串列鍵和哈希鍵的底層實作之一,當一個串列鍵只包含少量串列項并且每個都是小整數值或者長度比較短的字串時,Redis 就采用壓縮串列做底層實作,當一個哈希鍵只包含少量鍵值對,并且每個鍵值對的鍵和值也是小整數值或者長度比較短的字串時,Redis 就采用壓縮串列做底層實作,
壓縮串列是 Redis 為了節約記憶體而實作的,是一系列特殊編碼的連續記憶體塊組成的順序型資料結構,
zlbytes :4 位元組,記錄整個壓縮串列占用的記憶體位元組數,在記憶體重分配或者計算 zlend 的位置時使用, zltail :4 位元組,記錄壓縮串列表尾節點記錄壓縮串列的起始地址有多少個位元組,可以通過該屬性直接確定表尾節點的地址,無需遍歷, zllen :2 位元組,記錄了壓縮串列包含的節點數量,由于只有 2 位元組大小,那么小于 65535 時,表示節點數量,等于 65535 時,需要遍歷得到總數, entry :串列節點,長度不定,由內容決定, zlend :1 位元組,特殊值 0xFF ,用于標記壓縮串列的結束,壓縮串列節點保存一個位元組陣列或者一個整數值,
位元組陣列可以是下列值:
- 長度小于等于 2^6-1 位元組的位元組陣列
- 長度小于等于 2^14-1 位元組的位元組陣列
- 長度小于等于 2^32-1 位元組的位元組陣列
整數可以是六種長度:
- 4 位長,介于 0 到 12 之間的無符號整數
- 1 位元組長的有符號整數
- 3 位元組長的有符號整數
- int16_t 型別整數
- int32_t 型別整數
- int64_t 型別整數
每個壓縮串列節點的結構如圖:
previous_entry_length 屬性以位元組為單位,記錄了壓縮串列中前一個節點的長度,該屬性的長度可以是 1 位元組或者 5 位元組,如果前一個節點的長度小于 254 位元組,那么該屬性長度為 1 位元組,保存小于 254 的值,如果前一節點的長度大于等于 254 位元組,那么長度需要為 5 位元組,屬性的第一位元組會被設定為 0xFE (254) 之后的 4 個位元組保存其長度, 壓縮串列的從表尾到表頭遍歷: 1. 首先,有指向壓縮串列表尾節點起始地址的指標 p1 (指向表尾節點的指標可以通過指向壓縮串列起始地址的指標加上 zltail 屬性的值得出); 2. 通過用 p1 減去節點的 previous_entry_length 屬性,得到前一個節點的起始地址的指標, 3. 如此回圈,最終從表尾遍歷到表頭節點, encoding 屬性記錄了節點的 content 屬性所保存的資料的型別和長度: - 一位元組、兩位元組或五位元組長,值的最高位為 00、01 或者 10 的是位元組陣列編碼,位元組陣列的長度由編碼除去最高兩位之后的其他位記錄; - 一位元組長,值的最高位以 11 開頭的是整數編碼,這種編碼表示保存是整數值,整數值的型別和長度由其他位記錄, 出現新增或洗掉節點導致 previous_entry_length 1 位元組或者 5 位元組的長度變化,是連鎖更新的問題,但出現幾率比較小,而且數量不多的情況下不會對性能造成影響,
資料結構底層實作
字串:①整數——int;②長字串(大于44【64 - 19(頭部) - 1(’/0’)】位元組)——raw;③短字串(小于44位元組)——embstr
串列:①滿足串列物件所有字串元素長度都小于64個位元組且元素數量小于512——ziplist;②其他使用雙向鏈表
hash:①滿足元素數量小于512且所有元素長度小于64位元組——ziplist;②哈希表
set:①所有元素都是整數,元素數量小于512——整數串列;②哈希表
zset:①所有元素都是整數,元素數量小于512——整數串列;②跳表
Docker
Docker是一個容器化平臺,它以容器的形式將您的應用程式及其所有依賴項打包在一起,以確保您的應用程式在任何環境中無縫運行,
Docker不是虛擬化方法,docker四種狀態:運行、已暫停、重新啟動、已退出,
DockerFile:FROM—指定基礎鏡像;LABEL—功能是為鏡像指定標簽;RUN—運行指定的命令;CMD—容器啟動時要運行的命令,
雖然ADD并且COPY在功能上類似,但是首選COPY,
那是因為它比ADD更易懂,COPY僅支持將本地檔案復制到容器中,而ADD具有一些功能(如僅限本地的tar提取和遠程URL支持),這些功能并不是很明顯,因此,ADD的最佳用途是將本地tar檔案自動提取到鏡像中
Docker鏡像是Docker容器的源代碼,類和物件實體的關系,
docker鏡像本質:
分層檔案系統,
centos的鏡像很小,復用了os的bootfs,只有其他層,tomcat鏡像很大,因為依賴于其他的鏡像,
RocketMQ
選型
MQ 描述 RabbitMQ erlang開發,對訊息堆積的支持并不好,當大量訊息積壓的時候,會導致 RabbitMQ 的性能急劇下降,每秒鐘可以處理幾萬到十幾萬條訊息, RocketMQ java開發,面向互聯網集群化功能豐富,對在線業務的回應時延做了很多的優化,大多數情況下可以做到毫秒級的回應,每秒鐘大概能處理幾十萬條訊息, Kafka Scala開發,面向日志功能豐富,性能最高,當你的業務場景中,每秒鐘訊息數量沒有那么多的時候,Kafka 的時延反而會比較高,所以,Kafka 不太適合在線業務場景, ActiveMQ java開發,簡單,穩定,性能不如前面三個,小型系統用也ok,但是不推薦,推薦用互聯網主流的,
底層實作
netty
檔案上傳
斷點傳輸、檔案秒傳(已經上傳過的不再上傳):hash
檔案傳輸粘包問題
為何粘包:
A. TCP協議為了提高傳輸效率,發送方往往需要收集定量的資料才會封裝給底層并發送,若出現連續send(data),TCP會把該資料進行整合(直到裝滿資料緩沖區),這樣就造成了粘包資料;
B. 接收方接收方的粘包是由于接收用戶相關行程不及時接收資料,從而導致粘包問題,這是因為接收方先把接收到的資料放在系統接受緩沖區,用戶行程從該緩沖區取定量的資料,但若下一包資料到達前,緩沖區的資料沒有及時的被用戶行程取走,則下一包資料與前一包部分資料在系統緩沖區,就可能導致用戶設定的行程緩沖區從系統緩沖區取走兩個包的部分資料,從而導致粘包解決方案:
A 發送方在send()之前,先向接收方發送資料總量大小,并通過雙端確認,server端發送資料包,然后接收方通過按資料量大小回圈設立緩沖區接收資料;;
B: TCP提供了PUSH(強制資料立即傳送)操作,但影響性能;
傳輸檔案的方式
ftp、sftp
事務訊息:
問題: 1、賬號服務扣款成功了,通知積分系統也成功了,但是積分增加的時候失敗了,資料不一致了, 2、賬號服務扣款成功了,但是通知積分系統失敗了,所以積分不會增加,資料不一致了, rocketmq的解決方案: 針對問題1:如果消費失敗了,是會自動重試的,如果重試幾次后還是消費失敗,那么這種情況就需要人工解決了,比如放到死信佇列里然后手動查原因進行處理等, 針對問題2:RocketMQ針對第二個問題解決方案是:如果你扣款成功了,但是往mq寫訊息的時候失敗了,那么RocketMQ會進行回滾訊息的操作,這時候我們也能回滾我們扣款的操作,(通過半訊息實作)
順序訊息:
方案1:發送訊息到一個queue中來保證順序消費(MessageQueueSelector),
方案2:執行緒數設定為1,且通過訊息體判斷到哪個queue進行消費,
訊息持久化
Broker端拿到訊息后先將訊息、topic、queue等內容存到ByteBuffer里,然后去持久化到commitlog檔案中,commitlog檔案大小為1G,超出大小會新創建commitlog檔案來存盤,采取的nio方式,
如何保證訊息不丟失
訊息消費的流程: 生產階段:Producer通過網路將訊息發送給Broker,這個發送可能會發生丟失,比如網路延遲不可達等, 存盤階段:Broker肯定是先把訊息放到記憶體的,然后根據刷盤策略持久化到硬碟中,剛收到Producer的訊息,在記憶體中了,但是例外宕機了,導致訊息丟失,(持久化位置為commitlog) 消費階段:消費失敗了其實也是訊息丟失的一種變體吧,解決方案: 生產階段: 1、通過同步發送來保證訊息的成功送達, 2、如果發送失敗會重試,默認為三次,可通過api調整,producer.setRetryTimesWhenSendFailed(10); 3、如果broker宕機,producer會重試發送到另一臺broker 同步發送+自動重試機制+多個Master節點 存盤階段: 1、同步刷盤來保證訊息的不丟失,同樣會損失一部分性能,(默認為異步) 2、集群部署保證高可用,等Master和Slave都刷完盤后才去通知Producer說訊息ok了,brokerRole=SYNC_MASTER 消費階段: 1、手動ack 2、自動重試15次,進入死信佇列
發訊息的時候選擇queue的演算法
random、hash、自定義
為什么同一個消費組設定不同tag會出現奇怪現象
兩個相同組的消費者c1,c2相同topic,訂閱tag1,tag2,此時往這個topic的兩個tag分別發送10條訊息,會發現c1沒有收到訊息,c2只收到了不到10條訊息,訊息能夠正常發送,
原因:broker的問題,
Consumer端發心跳給Broker,Broker收到后存到consumerTable里(就是個Map),key是GroupName,value是ConsumerGroupInfo,
ConsumerGroupInfo里面是包含topic等資訊的,但是問題就出在上一步驟,key是groupName,你同GroupName的話Broker心跳最后收到的Consumer會覆寫前者的,所以c1的tag1被覆寫,無法接收到訊息,為何c2沒有收到10條訊息呢,因為是集群模式消費,所以會有負載均衡,有一部分訊息到達了c1但tag為tag2,無法消費,所以c2只收到了幾條訊息而非10條,如果換為廣播模式,則c2能接收到10條訊息,
注意:一個consumer可以訂閱多個topic(存在一個map以topic為鍵)
消費者負載均衡策略
- queue個數大于Consumer個數,且queue個數能整除Consumer個數的話, 那么Consumer會平均分配queue,(比如上面表格的Consumer有2個 可以整除部分)
- queue個數大于Consumer個數,且queue個數不能整除Consumer個數的話, 那么會有一個Consumer多消費1個queue,其余Consumer平均分配,(比如上面表格的Consumer有3個 不可整除部分)
- queue個數小于Consumer個數,那么會有Consumer閑置,就是浪費掉了,其余Consumer平均分配到queue上,(比如上面表格的Consumer有5個 無法都分配部分)
nginx
負載均衡有哪些演算法
輪詢法、隨機法、源地址hash法、加權輪詢、加權隨機、最小連接數法(動態地選取其中當前積壓連接數最少的一臺服務器來處理當前的請求)
紅黑樹管理timer:
單服務器抗壓
1.對socket方面的優化 1)作業系統(linux)的設定: 增大socket的最大連接數 echo 50000 > /proc/sys/net/core/somaxconn (系統默認的值是128,現在改成50000) 加快系統的tcp回識訓制 (系統默認tcp在斷開后還會存活一段時間) 方法如下 echo 1 > /proc/sys/net/ipv4/tcp_tw_recycle (系統默認是0,修改為1) 允許空的tcp回收利用 方法如下 echo 1 >/proc/sys/net/ipv4/tcp_tw_reuse (系統默認為0,修改為1) 讓系統不做洪水抵御保護,(當系統檢測到80埠在大量的請求時,會自動給回傳資訊中增加 cookie ,還驗證客戶端身份,從而避免受到攻擊,但這時只是高并發,并不是攻擊,所以要把這個抵御機制給關閉) 方法如下 echo 0 >/proc/sys/net/ipv4/tcp_syncookie (系統默認為1,修改為0) 2)nginx的設定: 增大子行程打開的連接 ,在event段中 worker_connections 1024; nginx默認能打開1024個連接 修改worker_connections 10000; 修改為可以打開10000個socket連接 2.對檔案系統方面的優化 1)作業系統方面: 讓作業系統允許打開更多的檔案 ulimit -n(設定一個比較大的值) ulimit -n 10240; (把作業系統允許打開檔案的最大值設為10240,原本的默認值是1024) 2)nginx 配置子行程可以打開的檔案個數 在nginx全域的配置中 worker_processes 1;下面加上worker_limit_nofile 10240; work_limit_nofile 10240 ; (nginx的子行程可以打開10240個檔案)
行程模型
nginx模型有兩種行程,master行程和worker行程,master行程主要用來管理worker行程,管理包含:接收來自外界的信號,向各worker行程發送信號,監控worker行程的運行狀態,當worker行程退出后(例外情況下),會自動重新啟動新的worker行程,
而基本的網路事件,則是放在worker行程中來處理了,多個worker行程之間是對等的,他們同等競爭來自客戶端的請求,各行程互相之間是獨立的,一個請求,只可能在一個worker行程中處理,一個worker行程,不可能處理其它行程的請求,worker行程的個數是可以設定的,一般我們會設定與機器cpu核數一致.
啟動方式
Nginx的啟動方式有兩種:
單行程啟動:此時系統中只有一個行程,這個行程既是master行程,也是worker行程,
多行程啟動:此時系統中有且僅有一個master行程,有多個worker行程,master行程主要是用來管理worker行程的,

如何處理請求
worker行程之間是平等的,每個行程,處理請求的機會也是一樣的,
當我們提供80埠的http服務時,一個連接請求過來,每個行程都有可能處理這個連接,首先,每個worker行程都是從master行程fork過來,在master行程里面,先建立好需要listen的socket之后,然后再fork出多個worker行程,這樣每個worker行程都可以去accept這個socket,
一般來說,當一個連接進來后,所有在accept在這個socket上面的行程,都會收到通知,而只有一個行程可以accept這個連接,其它的則accept失敗,這是所謂的驚群現象(驚群現象(thundering herd)就是當多個行程和執行緒在同時阻塞等待同一個事件時,如果這個事件發生,會喚醒所有的行程,但最終只可能有一個行程/執行緒對該事件進行處理,其他行程/執行緒會在失敗后重新休眠,這種性能浪費就是驚群,)
當然,nginx也不會視而不見,所以nginx提供了一個accept_mutex這個東西,從名字上,我們可以看這是一個加在accept上的一把共享鎖,有了這把鎖之后,同一時刻,就只會有一個行程在accpet連接,這樣就不會有驚群問題了,accept_mutex是一個可控選項,我們可以顯示地關掉,默認是打開的,
當一個worker行程在accept這個連接之后,就開始讀取請求,決議請求,處理請求,產生資料后,再回傳給客戶端,最后才斷開連接,這樣一個完整的請求就是這樣的了,我們可以看到,一個請求,完全由worker行程來處理,而且只在一個worker行程中處理,
通信
linux與nginx之間通過信號進行通信,
master行程與worker行程通過sockpair(全雙工通信)進行通信(channel)
worker行程間則是通過比較快速的共享記憶體進行通信,(mmap記憶體映射、通過檔案、通過system v)
資料庫MySQL
臟讀、不可重復讀、幻讀
臟讀:臟讀就是指當一個事務正在訪問資料,并且對資料進行了修改,而這種修改還沒有提交到資料庫中,這時,另外一個事務也訪問這個被修改的資料,然后使用了這個資料,事務讀取到了其他事務修改并且未提交的資料,
不可重復讀:一個事務多次讀取同一資料,該資料記錄會改變,(其他事務進行了資料的修改)
幻讀:多次讀取,后面讀取時資料記錄數量發生改變,(其他事務進行了資料的插入、洗掉操作)
四種隔離級別:
讀未提交、讀已提交、可重復讀、串行化,
讀已提交解決臟讀;可重復讀解決臟讀和不可重復讀;串行化解決所有問題
delete、truncate、drop區別
1) DELETE陳述句執行洗掉的程序是每次從表中洗掉一行,并且同時將該行的洗掉操作作為事務記錄在日志中保存以便進行進行回滾操作,會觸發觸發器,TRUNCATE TABLE 則一次性地從表中洗掉所有的資料并不把單獨的洗掉操作記錄記入日志保存,洗掉行是不能恢復的,并且在洗掉的程序中不會激活與表有關的洗掉觸發器,執行速度快, 2) 釋放空間問題 當表被TRUNCATE 后,這個表和索引所占用的空間會恢復到初始大小, DELETE操作不會減少表或索引所占用的空間, drop陳述句將表所占用的空間全釋放掉, 一般而言,drop > truncate > delete 3) TRUNCATE 只能對TABLE; DELETE可以是table和view TRUNCATE 和 DELETE 只洗掉資料, DROP則洗掉整個表(結構和資料), TRUNCATE 無法使用 WHERE 子句 eg. delete from table_test where ... / truncate table table_test
索引、索引型別(Hash、B+、全文索引)
innodb、MyIsam——資料節點存盤資料 or 存盤資料地址(指標)
MyISAM索引檔案和資料檔案分開——非聚集索引、InnoDB則是索引檔案和資料檔案在一起——聚集索引,
主鍵索引、輔助索引,
最左前綴匹配,
索引型別:
1.普通索引;2.唯一索引;3.主鍵索引;4.組合索引;5.全文索引(innodb不支持)
創建聯合索引:
create index xxx on 表名(欄位,欄位,xxx)增加索引:
ALTER TABLE 'table_name' ADD INDEX index_name('col1','col2','col3')
hash索引
對單行記錄查詢非常快,但不支持范圍查詢,要想支持范圍查詢,需要和其他資料結構進行結合,
如:hash + 跳表
聚簇索引和非聚簇索引
聚簇索引:索引和資料一起存放(innodb),非聚簇索引:索引的葉節點是指標指向對應的資料,分開存放(myisam)
插入資料時,聚簇索引需要排序,非聚簇索引則需要維護索引到資料的指標,非聚集索引會存在索引和資料的兩次io,更耗時,
innodb四大特性
插入快取、兩次寫、自適應hash、提前讀click here or there
innodb底層詳解
InnoDB的記憶體架構主要分為三大塊,緩沖池(Buffer Pool)、重做緩沖池(Redo Log Buffer)和額外記憶體池
緩沖池采用了LRU演算法,但可能導致緩沖池污染,
mysql在寫入記錄前,會先記錄到redo log,用于刷盤,
插入快取:等資料達到某個閾值(例如50條)才批量的寫入磁盤,降低io,
兩次寫:插入緩沖提高了MySQL的性能,而兩次寫則在此基礎上提高了資料的可靠性,我們知道,當資料還在緩沖池中的時候,當機器宕機了,發生了寫失效,有Redo Log來進行恢復,但是如果是在從緩沖池中將資料刷回磁盤的時候宕機了呢?這種情況叫做部分寫失效,此時重做日志就無法解決問題,
在刷臟頁時,并不是直接刷入磁盤,而是copy到記憶體中的Doublewrite Buffer中,然后再拷貝至磁盤共享表空間(你可以就理解為磁盤)中,每次寫入1M,等copy完成后,再將Doublewrite Buffer中的頁寫入磁盤檔案,有了兩次寫機制,即使在刷臟頁時宕機了,在實體恢復的時候也可以從共享表空間中找到Doublewrite Buffer的頁副本,直接將其覆寫原來的資料頁即可,
自適應哈希索引:參考jit熱點代碼,對熱點索引進行hash,
提前讀:innodb中將64個頁劃分為一個extent,當一個extent中的頁,被順序讀超過了多少個,比如50個,這個值是可以通過nnodb_read_ahead_threshold設定的,那么就會認為順序讀到下一個extent的可能性很大,會提前將下一個extent中的所有頁都加載到buffer pool中,這叫線性預讀
間隙鎖
Innodb在可重復讀提交下為了解決幻讀問題時引入的鎖機制,
針對范圍查詢,例如查詢id為1-9之間的所有資料(前提,資料庫中沒有id為2 4 6的資料),但是范圍鎖就會將1-9的所有都鎖上,如果此時想要插入一條id為2的資料,是無法插入的,會導致阻塞,
間隙鎖可能導致死鎖,間隙鎖之間并不是互斥的,
解決mysql讀寫效率
sql優化、索引、快取、主從復制+讀寫分離、垂直拆分(分布式)、水平拆分(解決主鍵問題)、磁區
MVCC原理
多版本并發控制技術,保存資料的歷史版本,可以通過比較版本號決定資料是否顯示出來,讀取資料的時候不需要加鎖可以保證事務的隔離效果,
innodb:更新前建立undo log,根據各種策略讀取時非阻塞就是MVCC,undo log中的行就是MVCC中的多版本,即:事務更新某記錄時,先用排他鎖鎖定,然后copy一份記錄到undo log然后讓roll_ptr指向undo log,然后進行更新并填寫事務編號,
解決讀寫之間阻塞的問題,通過 MVCC 可以讓讀寫互相不阻塞,讀不相互阻塞,寫不阻塞讀,這樣可以提升資料并發處理能力, 降低了死鎖的概率,這個是因為 MVCC 采用了樂觀鎖的方式,讀取資料時,不需要加鎖,寫操作,只需要鎖定必要的行, 解決了一致性讀的問題,當我們朝向某個資料庫在時間點的快照是,只能看到這個時間點之前事務提交更新的結果,不能看到時間點之后事務提交的更新結果,InnoDB 的 MVCC 是如何實作的?
InnoDB 是如何存盤記錄多個版本的?這些資料是 事務版本號,行記錄中的隱藏列(row_id, tx_id, roll_ptr)和Undo Log,
事務查詢行記錄: 當前事務的 creator_trx_id 想要讀取某個行記錄,這個行記錄ID的trx_id ,這樣會有以下的情況: 如果 trx_id < 活躍的最小事務ID(up_limit_id),也就是說這個行記錄在這些活躍的事務創建前就已經提交了,那么這個行記錄對當前事務是可見的, 如果trx_id > 活躍的最大事務ID(low_limit_id),這個說明行記錄在這些活躍的事務之后才創建,說明這個行記錄對當前事務是不可見的, 如果 up_limit_id < trx_id <low_limit_id,說明該記錄需要在 trx_ids 集合中,可能還處于活躍狀態,因此我們需要在 trx_ids 集合中遍歷,如果trx_id 存在于 trx_ids 集合中,證明這個事務 trx_id 還處于活躍狀態,不可見,否則 ,trx_id 不存在于 trx_ids 集合中,說明事務trx_id 已經提交了,這行記錄是可見的, 如何查詢一條記錄 1、獲取事務自己的版本號,即事務ID 2、獲取 Read View 3、查詢得到的資料,然后 Read View 中的事務版本號進行比較, 4、如果不符合 ReadView 規則, 那么就需要 UndoLog 中歷史快照; 5、最后回傳符合規則的資料 InnoDB 實作多版本控制 (MVCC)是通過 ReadView + UndoLog 實作的,UndoLog 保存了歷史快照,ReadView 規則幫助判斷當前版本的資料是否可見,innodb如何保證崩潰恢復能力
兩階段日志提交
MVCC下的一些操作
增刪查改 在InnoDB中,給每行增加兩個隱藏欄位來實作MVCC,一個用來記錄資料行的創建時間,另一個用來記錄行的過期時間(洗掉時間),在實際操作中,存盤的并不是時間,而是事務的版本號,每開啟一個新事務,事務的版本號就會遞增, 于是乎,默認的隔離級別(REPEATABLE READ)下,增刪查改變成了這樣: SELECT:讀取創建版本小于或等于當前事務版本號,并且洗掉版本為慷訓大于當前事務版本號的記錄,這樣可以保證在讀取之前記錄是存在的, INSERT:將當前事務的版本號保存至行的創建版本號 UPDATE:新插入一行,并以當前事務的版本號作為新行的創建版本號,同時將原記錄行的洗掉版本號設定為當前事務版本號 DELETE:將當前事務的版本號保存至行的洗掉版本號 快照讀和當前讀 快照讀:讀取的是快照版本,也就是歷史版本 當前讀:讀取的是最新版本 普通的SELECT就是快照讀,而UPDATE、DELETE、INSERT、SELECT ... LOCK IN SHARE MODE、SELECT ... FOR UPDATE是當前讀,
read-view(一致性視圖)
未提交的事務id陣列以及當前已經創建(不論是否提交)的最大事務id,(【未提交事務id】,max_id)
當執行查詢sql時會生成一致性視圖read-view,它由執行查詢時所有未提交事務id陣列(陣列里最小的id為min_id)和已創建的最大事務id (max_id)組成,查詢的資料結果需要跟read-view做比對從而得到快照結果 版本鏈比對規則: 1.如果落在綠色部分( trx_id < min_id ),表示這個版本是已提交的事務生成的,這個資料是可見的; 2.如果落在紅色部分( trx id > max_id ),表示這個版本是由將來啟動的事務生成的,是肯定不可見的; 3.如果落在黃色部分(min_ id <= trx_id <= max_id),那就包括兩種情況 a.若row的trx_id在陣列中,表示這個版本是由還沒提交的事務生成的,不可見,當前自己的事務是可見的; b.若row的trx_id不在陣列中,表示這個版本是已經提交了的事務生成的,可見, 對于洗掉的情況可以認為是update的特殊情況,會將版本鏈上最新的資料復制一份,然后將trx_id修改成洗掉操作的trx_id,同時在該條記錄的頭信(record header)里的(deleted_flag)標記位寫上true,來表示當前記錄已經被洗掉,在查間時按照上面的規則查到對應的記錄如果delete flag標記位為true,意味著記錄已被洗掉,則不回傳資料,如果是可重復讀的隔離級別,則MVCC中每個select記錄使用的是之前select的read-view,即進行延用而非重新創建,(當一個session發起第一個查詢時,此刻對應的全表的read-view已經確定了,之后的查詢都會使用該read-view)
而如果是讀已提交的隔離界別,則MVCC中每個select記錄都會重新生成read-view,
mvcc不同實作方式
第一種實作方式是將資料記錄的多個版本保存在資料庫中,當這些不同版本資料不再需要時,垃圾收集器回收這些記錄,
這個方式被PostgreSQL和Firebird/Interbase采用,SQL Server使用的類似機制,所不同的是舊版本資料不是保存在資料庫中,而保存在不同于主資料庫的另外一個資料庫tempdb中/
第二種實作方式只在資料庫保存最新版本的資料,但是會在使用undo時動態重構舊版本資料,
這種方式被Oracle和MySQL/InnoDB使用,
mysql的mvcc的缺陷
The snapshot of the database state applies to SELECT statements within a transaction, not necessarily to DML statements. If you insert or modify some rows and then commit that transaction, a DELETE or UPDATE statement issued from another concurrent REPEATABLE READ transaction could affect those just-committed rows, even though the session could not query them. If a transaction does update or delete rows committed by a different transaction, those changes do become visible to the current transaction.
一個不同的事務通過update或者delete陳述句進行修改時,會導致被修改的記錄對于之前的事務的select陳述句可見,而本身是不應該的,根本原因在于 MySQL 在 update/delete/insert/select for update/select lock in share mode 時進行的是 current read(selectlocktype != LOCK_NONE) 而非 consistent read,
select所見非update所用啊,
行鎖和表鎖(具體)
行鎖:鎖定一行或者多行記錄,行鎖是基于索引加載的,可能死鎖,
表鎖:沒有觸發索引,則會鎖表,表鎖回應的是非索引欄位,即全表掃描,不會死鎖,
如何實作的隔離級別
讀寫鎖 or MVCC,
事務看到的是自己查詢時候的快照ReadView,
回表查詢
第一遍定位主鍵值,再定位行記錄,它的性能較掃一遍索引樹更低,
索引覆寫
查詢時不需要回表查詢,直接通過索引就可以獲取查詢的結果資料,extra:using index代表索引覆寫
mysql,資料查詢慢怎么辦
慢查詢抓取sql陳述句,然后用explain執行計劃判斷,建索引,B+樹,為什么可以增快速度,
show profiles查看sql陳述句性能,
explain
id、select_type、table、partitions、type、possible_keys、key、key_len、ref、row、filtered、Extra
* id: SELECT查詢的識別符號.每個SELECT都會自動分配一個唯一的識別符號. * select_type: SELECT查詢的型別. * table: 查詢的是哪個表 * partitions: 匹配的磁區 * type: join型別 - 從好到壞 最少應使用range - system、const、eq_ref、ref、fulltext、ref_or_null、unique_subquery、index_subquery、[range]、index_merge、index、ALL * possible_keys: 此次查詢中可能選用的索引 * key: 此次查詢中確切使用到的索引. * ref: 哪個欄位或常數與key一起被使用 * rows: 顯示此查詢一共掃描了多少行.這個是一個估計值. * filtered: 表示此查詢條件所過濾的資料的百分比 * extra: 額外的資訊
分庫、分表、磁區
就搞一個主庫,掛多個從庫,然后我們就單單只是寫主庫,然后主庫會自動把資料給同步到從庫上去,讀從庫,主從復制,binlog,
分庫:垂直按功能模塊切分、水平減少資料庫壓力、資料隔離、功能切分,
分表:資料冗余問題、熱點資料等等,
磁區:磁區并不是生成新的資料表,而是將表的資料均衡分攤到不同的硬碟,系統或是不同服務器存盤介子中,實際上還是一張表,另外,磁區可以做到將表的資料均衡到不同的地方,提高資料檢索的效率,降低資料庫的頻繁IO壓力值,
磁區就是把一張表的資料分成N個區塊,在邏輯上看最終只是一張表,但底層是由N個物理區塊組成的
垂直拆分是把不同的表拆到不同的資料庫中,而水平拆分是把同一個表拆到不同的資料庫中,
垂直拆分和水平拆分
首選垂直拆分
垂直切分是指按照業務將表進行分類,分布到不同的資料庫上面,這樣也就將資料或者說壓力分擔到不同的庫上面 ,
水平拆分的典型場景就是大家熟知的分庫分表,
垂直拆分后遇到單機瓶頸,可以使用水平拆分,相對于垂直拆分的區別是:垂直拆分是把不同的表拆到不同的資料庫中,而水平拆分是把同一個表拆到不同的資料庫中,
mysql主從復制原理
①為何需要主從復制?
并發量大時,需要讀寫分離,主庫負責寫,從庫負責讀,即使需要鎖表,對讀的性能也沒有影響,
資料的預熱,
降低單機IO壓力,②主從復制原理(bin log)
原理: 1)、在master機器上的操作: 當master上的資料發生變化時,該事件變化會按照順序寫入bin-log中,當slave鏈接到master的時候,master機器會為slave開啟binlog dump執行緒,當master的binlog發生變化的時候,bin-log dump執行緒會通知slave,并將相應的binlog內容發送給slave, 2)、在slave機器上操作: 當主從同步開啟的時候,slave上會創建兩個執行緒:I\O執行緒,該執行緒連接到master機器,master機器上的binlog dump 執行緒會將binlog的內容發送給該I\O執行緒,該I/O執行緒接收到binlog內容后,再將內容寫入到本地的relay log;sql執行緒,該執行緒讀取到I/O執行緒寫入的ralay log,并且根據relay log,并且根據relay log 的內容對slave資料庫做相應的操作, 也就是說: - 從庫會生成兩個執行緒,一個I/O執行緒,一個SQL執行緒; - I/O執行緒會去請求主庫的binlog,并將得到的binlog寫到本地的relay-log(中繼日志)檔案中; - 主庫會生成一個log dump執行緒,用來給從庫I/O執行緒傳binlog; - SQL執行緒,會讀取relay log檔案中的日志,并決議成sql陳述句逐一執行;③三種主要實作粒度
詳細的主從同步主要有三種形式:statement、row、mixed
1)、statement: 會將對資料庫操作的sql陳述句寫道binlog中
2)、row: 會將每一條資料的變化寫道binlog中,
3)、mixed: statement與row的混合,Mysql決定何時寫statement格式的binlog, 何時寫row格式的binlog,④主從復制延遲問題
原因:
1)、MySQL資料庫主從同步延遲原理mysql主從同步原理:主庫針對寫操作,順序寫binlog,從庫單執行緒去主庫順序讀”寫操作的binlog”,從庫取到binlog在本地原樣執行(隨機寫),來保證主從資料邏輯上一致,mysql的主從復制都是單執行緒的操作,主庫對所有DDL和DML產生binlog,binlog是順序寫,所以效率很高,slave的Slave_IO_Running執行緒到主庫取日志,效率比較高,下一步,問題來了,slave的Slave_SQL_Running執行緒將主庫的DDL和DML操作在slave實施,DML和DDL的IO操作是隨即的,不是順序的,成本高很多,還可能可slave上的其他查詢產生lock爭用,由于Slave_SQL_Running也是單執行緒的,所以一個DDL卡主了,需要執行10分鐘,那么所有之后的DDL會等待這個DDL執行完才會繼續執行,這就導致了延時,有朋友會問:“主庫上那個相同的DDL也需要執行10分,為什么slave會延時?”,答案是master可以并發,Slave_SQL_Running執行緒卻不可以, 2)、MySQL資料庫主從同步延遲是怎么產生的?當主庫的TPS并發較高時,產生的DDL數量超過slave一個sql執行緒所能承受的范圍,那么延時就產生了,當然還有就是可能與slave的大型query陳述句產生了鎖等待,首要原因:資料庫在業務上讀寫壓力太大,CPU計算負荷大,網卡負荷大,硬碟隨機IO太高次要原因:讀寫binlog帶來的性能影響,網路傳輸延遲,解決方案:
①硬體方面;②禁用從庫binlog;③分擔壓力等等
事務如何實作隔離性
讀寫鎖 or MVCC
三大范式
1NF:欄位不可分
2NF:屬性完全依賴于主鍵
3NF:屬性不依賴于其它非主屬性 屬性直接依賴于主鍵
BCNF:無傳遞依賴
事務特性
ACID,原子性、一致性、隔離性、持久性
一致性:資料前后的完整性,
sql很慢優化:
索引、拆分表、大欄位、減少函式運算、記憶體、cpu
自增id用完
id邊界值使用后,越過此邊界值插入資料會失敗,主鍵沖突、無法插入,使用big int or 自己的主鍵
mysql和redis資料一致性保證
索引下推
即如果下圖中
name也有索引,則會將其過濾也放在底層進行過濾,進而增大效率,
在不使用ICP的情況下,在使用非主鍵索引(又叫普通索引或者二級索引)進行查詢時,存盤引擎通過索引檢索到資料,然后回傳給MySQL服務器,服務器然后判斷資料是否符合條件 , 在使用ICP的情況下,如果存在某些被索引的列的判斷條件時,MySQL服務器將這一部分判斷條件傳遞給存盤引擎,然后由存盤引擎通過判斷索引是否符合MySQL服務器傳遞的條件,只有當索引符合條件時才會將資料檢索出來回傳給MySQL服務器 , 索引條件下推優化可以減少存盤引擎查詢基礎表的次數,也可以減少MySQL服務器從存盤引擎接收資料的次數,
join和union區別
join是通過on上面的條件進行的
union則是將兩個結果集進行合并
視窗函式(用于組內排名)
# 基本語法 <視窗函式> over (partition by <用于分組的列名> order by <用于排序的列名>) # rank, dense_rank, row_number # rank排名會間隔 如 1 2 2 4 5... # 而dense_rank則不會間隔 如 1 2 2 3 4 5 # row_number則不考慮并列的情況<視窗函式>的位置,可以放以下兩種函式:
1) 專用視窗函式,包括后面要講到的rank, dense_rank, row_number等專用視窗函式,
2) 聚合函式,如sum. avg, count, max, min等
因為視窗函式是對where或者group by子句處理后的結果進行操作,所以視窗函式原則上只能寫在select子句中,
partition by用來對表分組,order by子句的功能是對分組后的結果進行排序(默認asc),
partition by不會改變表的行數,而group by則會改變,
對于聚合函式,則會在分組內,按順序進行,例如:
select *, sum(成績) over (order by 學號) as current_sum, avg(成績) over (order by 學號) as current_avg, count(成績) over (order by 學號) as current_count, max(成績) over (order by 學號) as current_max, min(成績) over (order by 學號) as current_min from 班級表
資料庫分庫分表主鍵處理
①使用自增主鍵,適用于資料量大并發較小需要分表的情況,
②設定資料庫 sequence 或者表自增欄位步長,比如說,現在有 8 個服務節點,每個服務節點使用一個 sequence 功能來產生 ID,每個 sequence 的起始 ID 不同,并且依次遞增,步長都是 8,方案簡單,但是如果需要增加服務節點時,就較為麻煩,
③UUID,好處就是本地生成,不用基于資料庫來了;不好之處就是,UUID 太長了、占用空間大,作為主鍵性能太差了,會導致索引效率低下
④snowflake 演算法,把一個 64 位的 long 型的 id,1 個 bit 是不用的,用其中的 41 bit 作為毫秒數,用 10 bit 作為作業機器 id,12 bit 作為序列號,
設計模式
工廠模式、單例模式、原型模式、裝飾器模式、代理模式,
工廠:定義一個創建物件的介面,讓其子類自己決定實體化哪一個工廠類,工廠模式使其創建程序延遲到子類進行,1、一個呼叫者想創建一個物件,只要知道其名稱就可以了, 2、擴展性高,如果想增加一個產品,只要擴展一個工廠類就可以, 3、屏蔽產品的具體實作,呼叫者只關心產品的介面,
單例:懶漢、餓漢、雙重檢驗鎖懶漢
volatile
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
原型模式:創建物件代價太大 采用實作Cloneable介面和重寫clone方法
裝飾器模式:裝飾器模式(Decorator Pattern)允許向一個現有的物件添加新的功能,同時又不改變其結構,這種型別的設計模式屬于結構型模式,它是作為現有的類的一個包裝,
代理模式:jdk代理和cglib代理
分布式系統
rpc框架
dubbo、springcloud、thrift
分布式與集群的區別
集群是指將多臺服務器集中在一起提供同一種服務,在邏輯上可以看做是一臺服務器對外進行服務,這些服務器組合就是集群,
從概念上就可以看出兩者最主要的區別就是分布式是將一種業務拆分成多個子業務部署在多臺服務器上,進而對外提供服務;而集群就是將多臺服務器組合在一起提供同一種服務;
集群強調在多臺服務器位置集中,并且容易統一管理;而分布式沒有具體要求,不論放置在哪個位置,只要通過網路連接起來就行;
集群是一種物理形態,即多臺服務器在一起提供一種服務;而分布式是一種作業方式,即一個程式或業務分解到多臺服務器分別完成;
從單機結構到集群結構,你的代碼基本無需要作任何修改,你要做的僅僅是多部署幾臺服務器,每臺服務器上運行相同的代碼就行了,但是,當你要從集群結構演進到微服務結構的時候,之前的那套代碼就需要發生較大的改動了,所以對于新系統我們建議,系統設計之初就采用微服務架構,這樣后期運維的成本更低,但如果一套老系統需要升級成微服務結構的話,那就得對代碼大動干戈了,所以,對于老系統而言,究竟是繼續保持集群模式,還是升級成微服務架構,這需要你們的架構師深思熟慮、權衡投入產出比, 分布式結構就是將一個完整的系統,按照業務功能,拆分成一個個獨立的子系統,在分布式結構中,每個子系統就被稱為“服務”,這些子系統能夠獨立運行在web容器中,它們之間通過RPC方式通信,舉個例子,假設需要開發一個在線商城,按照微服務的思想,我們需要按照功能模塊拆分成多個獨立的服務,如:用戶服務、產品服務、訂單服務、后臺管理服務、資料分析服務等等,這一個個服務都是一個個獨立的專案,可以獨立運行,如果服務之間有依賴關系,那么通過RPC方式呼叫,
微服務如何拆分服務
①服務拆分最多三層,兩次呼叫 ②僅僅單向呼叫,嚴禁回圈呼叫 ③將串行呼叫改為并行呼叫,或者異步化 ④介面應該實作冪等
為何需要冪等性
- 前端重復提交:提交訂單,用戶快速重復點擊多次,造成后端生成多個內容重復的訂單,
- 介面超時重試:對于給第三方呼叫的介面,為了防止網路抖動或其他原因造成請求丟失,這樣的介面一般都會設計成超時重試多次,
- 訊息重復消費:MQ訊息中間件,訊息重復消費,
如何實作介面冪等性
資料庫上的冪等性相關:select和delete一定是冪等性,而insert如果帶唯一索引則是冪等性,不帶唯一索引則不是;update對于計算式的更新不是冪等性的,非計算式則是冪等性的,
只有不帶唯一索引插入(insert)和計算式的更新(update)會引起冪等性問題的
單機冪等性:代碼邏輯判斷、資料庫約束、token(在記憶體中存盤正在處理的用戶id來實作冪等性)
分布式系統冪等性:
①token+redis保證多次請求被攔截,
②資料庫通過唯一索引特性保證唯一性
③redis將唯一序列號作為key進行校驗,需要設定過期時間,
④狀態機,每個狀態存在前置狀態,無法回退,
前后端分離?
前后端通過http請求通信,后端只負責提高api介面和api檔案,頁面跳轉交給前端完成,
微服務鏈式呼叫例外
避免呼叫鏈過長,通過訊息佇列解耦
其他
CAP
- 一致性(Consistency):資料在多個副本之間能否保持一致的特性
- 可用性(Availability):提供的服務必須一直處于可用的狀態,對于用戶的每一個操作請求總是能夠在有限的時間內回傳結果,這里的重點是"有限時間內"和**“回傳結果”**,
- 磁區容忍性(Partition tolerance):分布式系統在遇到任何網路磁區故障的時候,仍然需要能夠保證對外提供滿足一致性和可用性的服務,除非是整個網路環境都發生了故障,(基本需求
BASE
- Basically Availble --基本可用:基本可用是指分布式系統在出現不可預知故障的時候,允許損失部分可用性,
- Soft-state --軟狀態/柔性事務:即允許系統在不同節點的資料副本之間進行資料同步的程序存在延時,
“Soft state” 可以理解為"無連接"的, 而 “Hard state” 是"面向連接"的
- Eventual Consistency --最終一致性:所有的資料副本,在經過一段時間的同步之后,最終都能夠達到一個一致的狀態,
對CAP理論中一致性和可用性的權衡,
最終一致性(分布式事務)
2PL,兩階段提交,
訊息重試、補償交易、事務訊息、介面支持重入
什么是可重入
重入函式也可以這樣理解,重入即表示重復進入,首先它意味著這個函式可以被中斷,其次意味著它除了使用自己堆疊上的變數以外不依賴于任何環境(包括static),這樣的函式就是purecode(純代碼)可重入,可以允許有該函式的多個副本在運行,由于它們使用的是分離的堆疊,所以不會互相干擾,如果確實需要訪問全域變數(包括static),一定要注意實施互斥手段,可重入函式在并行運行環境中非常重要,但是一般要為訪問全域變數付出一些性能代價,
專案
基于區塊鏈技術的資料共享平臺
去中心化的資料共享 + 計算
客戶 主要為高校科研團隊、交通委
資料提供方則為 路網中心、高德這類
共享資料 給激勵 用其他人的資料就需要花錢
類似于kaggle的模式
rocketmq收發訊息 廣播模式直接無法系結
訊息組相同 tag不同的情況
金剛石檔案專案
markdown檔案編輯器
團隊協作
冪等性問題
Linux
wc
wc [-clw] 檔案*
計算檔案的行數 單詞數 位元組數
-c或–bytes或–chars 只顯示Bytes數,-l或–lines 顯示行數,-w或–words 只顯示字數,
可以跟多個檔案,則會都顯示并進行匯總
Linux查看記憶體, cpu的占有率的命令
top
netstat、find、cat、cp、mv、su、ftp(ftp+ip)
netstat 命令用于顯示網路狀態,-a顯示所有套接字和埠,例子:TCP 192.168.9.52:1299 60.210.8.160:https CLOSE_WAIT
find命令格式:find path -xxx expression,將當前目錄及其子目錄下所有檔案后綴為 .c 的檔案列出來:find . -name “*.c”
把 textfile1 的檔案內容加上行號后輸入 textfile2 這個檔案里:cat -n textfile1 > textfile2 (-b和-n相似只是-b不會把空白行輸出)
ps
-a/-e 所有
-f 輸出完整的
根據時間查找檔案
---(+n)----------|----------(n)----------|----------(-n)--- (n+1)*24H前| (n+1)*24H~n*24H間 |n*24H內 mtime 檔案內容上次修改時間 atime 檔案被讀取或訪問的時間 ctime 檔案狀態變化時間 eg. 查找./本目錄下十天前修改過的目錄(層級為1,即當前目錄下),并洗掉 find ./ -mtime +10 -type d -maxdepth 1 | xargs rm -rf eg. 當前時間24小時 - 當前時間(昨天-今天) find . -mtime 0
chmod
chmod [owner group others]
RWX——777chmod 777 file
telnet
telnet + ip登陸遠程主機
Linux 中主要有哪幾種內核鎖
mutex互斥鎖、semaphore信號量、Spanlock自旋鎖、seqlock順序鎖、rwlock讀寫鎖(讀不阻塞,寫阻塞)
linux申請記憶體
kmalloc() 申請的記憶體位于物理記憶體映射區域,而且在物理上也是連續的,它們與真實的物理地址只有一個固定的偏移,因為存在較簡單的轉換關系,所以對申請的記憶體大小有限制,不能超過128KB,kzalloc()將申請到的記憶體清0,
vmalloc() 函式則會在虛擬記憶體空間給出一塊連續的記憶體區,但這片連續的虛擬記憶體在物理記憶體中并不一定連續,由于 vmalloc() 沒有保證申請到的是連續的物理記憶體,因此對申請的記憶體大小沒有限制,如果需要申請較大的記憶體空間就需要用此函式了,
只有開機時,申請到大塊記憶體的概率較大,
演算法
反轉鏈表:遞回、迭代
洗掉字串中的空格,時間O(n),空間O(1)
寫一個程式判斷機器是大端法還是小端法(什么是大端、什么是小端)
小端法:低地址存放低位,高地址存放高位
int i = 0x11223344; char *c = (char*)(&i); printf("%x", *c); // 0x44-小端 0x11-大端
很長的英文論文,統計出現最高頻的k個單詞,(hashmap + 堆)
一組無序的數,找出中位數,(快速選擇,O(n))
熟悉的排序演算法(歸并、快排、冒泡、插入、選擇) 從小到大
插入排序:從第二個數開始,和前面的數進行比較,如果比前面的數小,則交換,直到沒有比該數大的數,繼續遍歷下一個數,
選擇排序:從當前遍歷到的數開始遍歷到結尾,找到最小的數,和當前數交換,直到遍歷完陣列,
冒泡排序:每一趟排序,將相鄰的兩個數進行比較,小的放在前面,重復len - 1趟,且每一趟都能確定len - 1 - i個數,
歸并排序:分治 + 雙指標,
快速排序:分治 + 選取樞紐,
穩定:插入、冒泡、歸并
不穩定:選擇、快排、堆排序
10億個數排序后列印,記憶體有限(拆分成小檔案并排好序分別放入小檔案,然后每個檔案進行讀取首資料然后關閉然后讀取下一個檔案,可以利用堆排序實作大檔案的有序)
從100萬個數中找出最大的前100個數(快速選擇 or 維護最大堆)
java操作有500萬資料的大表如何操作(多執行緒?分表?limit分頁?)
兩個執行緒交替列印1-100
分布鎖?
下層層序序列的完全二叉樹 列印前序遍歷結果
找出一個字串有多少回文字串,輸出最大的
Leetcode 1438
最長上升子序列(需要輸出子序列)
64匹馬 8條賽道 找出跑的最快的4匹 最少需要幾場比賽(11)
lru
高并發設計:
拆分為服務、快取、mq、分庫分表、讀寫分離、分布式
如果不用紅黑樹,怎么把hash后桶上的鏈表存入到磁盤空間內,要怎么設計 磁盤內的存盤方式?
桶上的鏈表再進行一次hash,
——————————
——————
————
———
—
👆這樣劃分磁盤的存盤空間,使記錄hash后的值時先在最大的那塊記錄,有沖突就往第二大的快記錄,使最大的磁盤塊能直接回傳值,防止hash沖突,
位運算實作加法
0 + 0 = 0 不進位 1 + 0 = 1 不進位 0 + 1 = 1 不進位 1 + 1 = 0 進位1 -------------- 加法本身:a ^ b 進位:(a & b) << 1 -------------- 結論:設a,b為兩個二進制數,則a+b = a^b + (a&b)<<1, 證明:a^b是不考慮進位時加法結果,當二進制位同時為1時,才有進位,因此 (a&b)<<1是進位產生的值,稱為進位補償,將兩者相加便是完整加法結果, 后續的加法通過遞回即可 public int add(int a, int b) { if (b == 0) return a; // 進位為0時退出遞回 return add(a ^ b, (a & b) << 1); }
4億個unsigned int型的數讓你保存,然后給你一個數,判斷它是否存在已經保存的數中,
bitmap
高考成績查詢高并發
省份、成績區間分庫分表
redis預熱資料
前端頁面提前快取CDN
nginx負載均衡
中間件
三數之和
lc 678
lc 316(單調堆疊 + 貪心)
場景題:兩堆大數,100億個數和10億個數,找交集
場景題:直播房間,一個大V發了一條訊息,如何讓上千萬的粉絲收到這條訊息,如果只是純粹的廣播會很耗資源
自己拉取 ?
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/275473.html
標籤:其他
下一篇:行程間通信(管道與共享記憶體)











