總結Java開發面試常問的問題,持續更新中~

在雲端發表於2018-06-04

GitHub地址:github.com/zaiyunduan1…,如果對你有幫助歡迎Star

資料庫

mysql

為什麼用自增列作為主鍵

  1. 如果我們定義了主鍵(PRIMARY KEY),那麼InnoDB會選擇主鍵作為聚集索引、如果沒有顯式定義主鍵,則InnoDB會選擇第一個不包含有NULL值的唯一索引作為主鍵索引、如果也沒有這樣的唯一索引,則InnoDB會選擇內建6位元組長的ROWID作為隱含的聚集索引(ROWID隨著行記錄的寫入而主鍵遞增,這個ROWID不像ORACLE的ROWID那樣可引用,是隱含的)。

  2. 資料記錄本身被存於主索引(一顆B+Tree)的葉子節點上。這就要求同一個葉子節點內(大小為一個記憶體頁或磁碟頁)的各條資料記錄按主鍵順序存放,因此每當有一條新的記錄插入時,MySQL會根據其主鍵將其插入適當的節點和位置,如果頁面達到裝載因子(InnoDB預設為15/16),則開闢一個新的頁(節點)

  3. 如果表使用自增主鍵,那麼每次插入新的記錄,記錄就會順序新增到當前索引節點的後續位置,當一頁寫滿,就會自動開闢一個新的頁

  4. 如果使用非自增主鍵(如果身份證號或學號等),由於每次插入主鍵的值近似於隨機,因此每次新紀錄都要被插到現有索引頁得中間某個位置,此時MySQL不得不為了將新記錄插到合適位置而移動資料,甚至目標頁面可能已經被回寫到磁碟上而從快取中清掉,此時又要從磁碟上讀回來,這增加了很多開銷,同時頻繁的移動、分頁操作造成了大量的碎片,得到了不夠緊湊的索引結構,後續不得不通過OPTIMIZE TABLE來重建表並優化填充頁面。

為什麼使用資料索引能提高效率

  1. 資料索引的儲存是有序的
  2. 在有序的情況下,通過索引查詢一個資料是無需遍歷索引記錄的
  3. 極端情況下,資料索引的查詢效率為二分法查詢效率,趨近於 log2(N)

B+樹索引和雜湊索引的區別

B+樹是一個平衡的多叉樹,從根節點到每個葉子節點的高度差值不超過1,而且同層級的節點間有指標相互連結,是有序的

總結Java開發面試常問的問題,持續更新中~
雜湊索引就是採用一定的雜湊演算法,把鍵值換算成新的雜湊值,檢索時不需要類似B+樹那樣從根節點到葉子節點逐級查詢,只需一次雜湊演算法即可,是無序的
總結Java開發面試常問的問題,持續更新中~

雜湊索引的優勢:

  1. 等值查詢。雜湊索引具有絕對優勢(前提是:沒有大量重複鍵值,如果大量重複鍵值時,雜湊索引的效率很低,因為存在所謂的雜湊碰撞問題。)

雜湊索引不適用的場景:

  1. 不支援範圍查詢
  2. 不支援索引完成排序
  3. 不支援聯合索引的最左字首匹配規則

通常,B+樹索引結構適用於絕大多數場景,像下面這種場景用雜湊索引才更有優勢:

在HEAP表中,如果儲存的資料重複度很低(也就是說基數很大),對該列資料以等值查詢為主,沒有範圍查詢、沒有排序的時候,特別適合採用雜湊索引,例如這種SQL:

select id,name from table where name='李明'; — 僅等值查詢
複製程式碼

而常用的InnoDB引擎中預設使用的是B+樹索引,它會實時監控表上索引的使用情況,如果認為建立雜湊索引可以提高查詢效率,則自動在記憶體中的“自適應雜湊索引緩衝區”建立雜湊索引(在InnoDB中預設開啟自適應雜湊索引),通過觀察搜尋模式,MySQL會利用index key的字首建立雜湊索引,如果一個表幾乎大部分都在緩衝池中,那麼建立一個雜湊索引能夠加快等值查詢。

注意:在某些工作負載下,通過雜湊索引查詢帶來的效能提升遠大於額外的監控索引搜尋情況和保持這個雜湊表結構所帶來的開銷。但某些時候,在負載高的情況下,自適應雜湊索引中新增的read/write鎖也會帶來競爭,比如高併發的join操作。like操作和%的萬用字元操作也不適用於自適應雜湊索引,可能要關閉自適應雜湊索引。

B樹和B+樹的區別

  1. B樹,每個節點都儲存key和data,所有節點組成這棵樹,並且葉子節點指標為nul,葉子結點不包含任何關鍵字資訊。
    這裡寫圖片描述
  2. B+樹,所有的葉子結點中包含了全部關鍵字的資訊,及指向含有這些關鍵字記錄的指標,且葉子結點本身依關鍵字的大小自小而大的順序連結,所有的非終端結點可以看成是索引部分,結點中僅含有其子樹根結點中最大(或最小)關鍵字。 (而B 樹的非終節點也包含需要查詢的有效資訊)

這裡寫圖片描述

為什麼說B+比B樹更適合實際應用中作業系統的檔案索引和資料庫索引?

  1. B+的磁碟讀寫代價更低 B+的內部結點並沒有指向關鍵字具體資訊的指標。因此其內部結點相對B樹更小。如果把所有同一內部結點的關鍵字存放在同一盤塊中,那麼盤塊所能容納的關鍵字數量也越多。一次性讀入記憶體中的需要查詢的關鍵字也就越多。相對來說IO讀寫次數也就降低了。

  2. B+-tree的查詢效率更加穩定 由於非終結點並不是最終指向檔案內容的結點,而只是葉子結點中關鍵字的索引。所以任何關鍵字的查詢必須走一條從根結點到葉子結點的路。所有關鍵字查詢的路徑長度相同,導致每一個資料的查詢效率相當。

mysql聯合索引

  1. 聯合索引是兩個或更多個列上的索引。對於聯合索引:Mysql從左到右的使用索引中的欄位,一個查詢可以只使用索引中的一部份,但只能是最左側部分。例如索引是key index (a,b,c). 可以支援a 、 a,b 、 a,b,c 3種組合進行查詢,但不支援 b,c進行查詢 .當最左側欄位是常量引用時,索引就十分有效。
  2. 利用索引中的附加列,您可以縮小搜尋的範圍,但使用一個具有兩列的索引 不同於使用兩個單獨的索引。複合索引的結構與電話簿類似,人名由姓和名構成,電話簿首先按姓氏對進行排序,然後按名字對有相同姓氏的人進行排序。如果您知 道姓,電話簿將非常有用;如果您知道姓和名,電話簿則更為有用,但如果您只知道名不姓,電話簿將沒有用處。

什麼情況下應不建或少建索引

  1. 表記錄太少
  2. 經常插入、刪除、修改的表
  3. 資料重複且分佈平均的表欄位,假如一個表有10萬行記錄,有一個欄位A只有T和F兩種值,且每個值的分佈概率大約為50%,那麼對這種表A欄位建索引一般不會提高資料庫的查詢速度。
  4. 經常和主欄位一塊查詢但主欄位索引值比較多的表欄位

MySQL分割槽

什麼是表分割槽?

表分割槽,是指根據一定規則,將資料庫中的一張表分解成多個更小的,容易管理的部分。從邏輯上看,只有一張表,但是底層卻是由多個物理分割槽組成。

表分割槽與分表的區別

分表:指的是通過一定規則,將一張表分解成多張不同的表。比如將使用者訂單記錄根據時間成多個表。

分表與分割槽的區別在於:分割槽從邏輯上來講只有一張表,而分表則是將一張表分解成多張表。

表分割槽有什麼好處?

  1. 分割槽表的資料可以分佈在不同的物理裝置上,從而高效地利用多個硬體裝置。 2. 和單個磁碟或者檔案系統相比,可以儲存更多資料
  2. 優化查詢。在where語句中包含分割槽條件時,可以只掃描一個或多個分割槽表來提高查詢效率;涉及sum和count語句時,也可以在多個分割槽上並行處理,最後彙總結果。
  3. 分割槽表更容易維護。例如:想批量刪除大量資料可以清除整個分割槽。
  4. 可以使用分割槽表來避免某些特殊的瓶頸,例如InnoDB的單個索引的互斥訪問,ext3問價你係統的inode鎖競爭等。

分割槽表的限制因素

  1. 一個表最多隻能有1024個分割槽
  2. MySQL5.1中,分割槽表示式必須是整數,或者返回整數的表示式。在MySQL5.5中提供了非整數表示式分割槽的支援。
  3. 如果分割槽欄位中有主鍵或者唯一索引的列,那麼多有主鍵列和唯一索引列都必須包含進來。即:分割槽欄位要麼不包含主鍵或者索引列,要麼包含全部主鍵和索引列。
  4. 分割槽表中無法使用外來鍵約束
  5. MySQL的分割槽適用於一個表的所有資料和索引,不能只對表資料分割槽而不對索引分割槽,也不能只對索引分割槽而不對錶分割槽,也不能只對表的一部分資料分割槽。

如何判斷當前MySQL是否支援分割槽?

命令:show variables like '%partition%' 執行結果:

mysql> show variables like '%partition%'; +-------------------+-------+ | Variable_name | Value | +-------------------+-------+ | have_partitioning | YES | +-------------------+-------+ 1 row in set (0.00 sec) have_partintioning 的值為YES,表示支援分割槽。

MySQL支援的分割槽型別有哪些?

  1. RANGE分割槽: 這種模式允許將資料劃分不同範圍。例如可以將一個表通過年份劃分成若干個分割槽
  2. LIST分割槽: 這種模式允許系統通過預定義的列表的值來對資料進行分割。按照List中的值分割槽,與RANGE的區別是,range分割槽的區間範圍值是連續的。
  3. HASH分割槽 :這中模式允許通過對錶的一個或多個列的Hash Key進行計算,最後通過這個Hash碼不同數值對應的資料區域進行分割槽。例如可以建立一個對錶主鍵進行分割槽的表。
  4. KEY分割槽 :上面Hash模式的一種延伸,這裡的Hash Key是MySQL系統產生的。

四種隔離級別

  1. Serializable (序列化):可避免髒讀、不可重複讀、幻讀的發生。
  2. Repeatable read (可重複讀):可避免髒讀、不可重複讀的發生。
  3. Read committed (讀已提交):可避免髒讀的發生。
  4. Read uncommitted (讀未提交):最低階別,任何情況都無法保證。

關於MVVC

MySQL InnoDB儲存引擎,實現的是基於多版本的併發控制協議——MVCC (Multi-Version Concurrency Control) (注:與MVCC相對的,是基於鎖的併發控制,Lock-Based Concurrency Control)。MVCC最大的好處:讀不加鎖,讀寫不衝突。在讀多寫少的OLTP應用中,讀寫不衝突是非常重要的,極大的增加了系統的併發效能,現階段幾乎所有的RDBMS,都支援了MVCC。

  1. LBCC:Lock-Based Concurrency Control,基於鎖的併發控制。
  2. MVCC:Multi-Version Concurrency Control,基於多版本的併發控制協議。純粹基於鎖的併發機制併發量低,MVCC是在基於鎖的併發控制上的改進,主要是在讀操作上提高了併發量。

在MVCC併發控制中,讀操作可以分成兩類:

  1. 快照讀 (snapshot read):讀取的是記錄的可見版本 (有可能是歷史版本),不用加鎖(共享讀鎖s鎖也不加,所以不會阻塞其他事務的寫)。
  2. 當前讀 (current read):讀取的是記錄的最新版本,並且,當前讀返回的記錄,都會加上鎖,保證其他事務不會再併發修改這條記錄。

行級鎖定的優點:

  1. 當在許多執行緒中訪問不同的行時只存在少量鎖定衝突。
  2. 回滾時只有少量的更改
  3. 可以長時間鎖定單一的行。

行級鎖定的缺點:

  1. 比頁級或表級鎖定佔用更多的記憶體。
  2. 當在表的大部分中使用時,比頁級或表級鎖定速度慢,因為你必須獲取更多的鎖。
  3. 如果你在大部分資料上經常進行GROUP BY操作或者必須經常掃描整個表,比其它鎖定明顯慢很多。
  4. 用高階別鎖定,通過支援不同的型別鎖定,你也可以很容易地調節應用程式,因為其鎖成本小於行級鎖定。

MySQL 觸發器簡單例項

  1. CREATE TRIGGER <觸發器名稱> --觸發器必須有名字,最多64個字元,可能後面會附有分隔符.它和MySQL中其他物件的命名方式基本相象.
  2. { BEFORE | AFTER } --觸發器有執行的時間設定:可以設定為事件發生前或後。
  3. { INSERT | UPDATE | DELETE } --同樣也能設定觸發的事件:它們可以在執行insert、update或delete的過程中觸發。
  4. ON <表名稱> --觸發器是屬於某一個表的:當在這個表上執行插入、 更新或刪除操作的時候就導致觸發器的啟用. 我們不能給同一張表的同一個事件安排兩個觸發器。
  5. FOR EACH ROW --觸發器的執行間隔:FOR EACH ROW子句通知觸發器 每隔一行執行一次動作,而不是對整個表執行一次。
  6. <觸發器SQL語句> --觸發器包含所要觸發的SQL語句:這裡的語句可以是任何合法的語句, 包括複合語句,但是這裡的語句受的限制和函式的一樣。

什麼是儲存過程

簡單的說,就是一組SQL語句集,功能強大,可以實現一些比較複雜的邏輯功能,類似於JAVA語言中的方法;

ps:儲存過程跟觸發器有點類似,都是一組SQL集,但是儲存過程是主動呼叫的,且功能比觸發器更加強大,觸發器是某件事觸發後自動呼叫;

有哪些特性

  1. 有輸入輸出引數,可以宣告變數,有if/else, case,while等控制語句,通過編寫儲存過程,可以實現複雜的邏輯功能;
  2. 函式的普遍特性:模組化,封裝,程式碼複用;
  3. 速度快,只有首次執行需經過編譯和優化步驟,後續被呼叫可以直接執行,省去以上步驟;
DROP PROCEDURE IF EXISTS `proc_adder`;
DELIMITER ;;
CREATE DEFINER=`root`@`localhost` PROCEDURE `proc_adder`(IN a int, IN b int, OUT sum int)
BEGIN
    #Routine body goes here...

    DECLARE c int;
    if a is null then set a = 0; 
    end if;
  
    if b is null then set b = 0;
    end if;

    set sum  = a + b;
END
;;
DELIMITER ;

set @b=5;
call proc_adder(0,@b,@s);
SELECT @s as sum;



create table tab2(
   tab2_id varchar(11)
);

DROP TRIGGER if EXISTS t_ai_on_tab1;
create TRAILING t_ai_on_tab1
AFTER INSERT ON tab1
for EACH ROW
BEGIN
   INSERT INTO tab2(tab2_id) values(new.tab1_id);
end;

INSERT INTO tab1(tab1_id) values('0001');

SELECT * FROM tab2;
複製程式碼

MySQL優化

  1. 開啟查詢快取,優化查詢
  2. explain你的select查詢,這可以幫你分析你的查詢語句或是表結構的效能瓶頸。EXPLAIN 的查詢結果還會告訴你你的索引主鍵被如何利用的,你的資料表是如何被搜尋和排序的
  3. 當只要一行資料時使用limit 1,MySQL資料庫引擎會在找到一條資料後停止搜尋,而不是繼續往後查少下一條符合記錄的資料
  4. 為搜尋欄位建索引
  5. 使用 ENUM 而不是 VARCHAR,如果你有一個欄位,比如“性別”,“國家”,“民族”,“狀態”或“部門”,你知道這些欄位的取值是有限而且固定的,那麼,你應該使用 ENUM 而不是VARCHAR。
  6. Prepared Statements Prepared Statements很像儲存過程,是一種執行在後臺的SQL語句集合,我們可以從使用 prepared statements 獲得很多好處,無論是效能問題還是安全問題。Prepared Statements 可以檢查一些你繫結好的變數,這樣可以保護你的程式不會受到“SQL隱碼攻擊式”攻擊
  7. 垂直分表
  8. 選擇正確的儲存引擎

key和index的區別

  1. key 是資料庫的物理結構,它包含兩層意義和作用,一是約束(偏重於約束和規範資料庫的結構完整性),二是索引(輔助查詢用的)。包括primary key, unique key, foreign key 等
  2. index是資料庫的物理結構,它只是輔助查詢的,它建立時會在另外的表空間(mysql中的innodb表空間)以一個類似目錄的結構儲存。索引要分類的話,分為字首索引、全文字索引等;

Mysql 中 MyISAM 和 InnoDB 的區別有哪些?

區別:

  1. InnoDB支援事務,MyISAM不支援,對於InnoDB每一條SQL語言都預設封裝成事務,自動提交,這樣會影響速度,所以最好把多條SQL語言放在begin和commit之間,組成一個事務;

  2. InnoDB支援外來鍵,而MyISAM不支援。對一個包含外來鍵的InnoDB錶轉為MYISAM會失敗;

  3. InnoDB是聚集索引,資料檔案是和索引綁在一起的,必須要有主鍵,通過主鍵索引效率很高。但是輔助索引需要兩次查詢,先查詢到主鍵,然後再通過主鍵查詢到資料。因此,主鍵不應該過大,因為主鍵太大,其他索引也都會很大。而MyISAM是非聚集索引,資料檔案是分離的,索引儲存的是資料檔案的指標。主鍵索引和輔助索引是獨立的。

  4. InnoDB不儲存表的具體行數,執行select count(*) from table時需要全表掃描。而MyISAM用一個變數儲存了整個表的行數,執行上述語句時只需要讀出該變數即可,速度很快;

  5. Innodb不支援全文索引,而MyISAM支援全文索引,查詢效率上MyISAM要高;

如何選擇:

  1. 是否要支援事務,如果要請選擇innodb,如果不需要可以考慮MyISAM;

  2. 如果表中絕大多數都只是讀查詢,可以考慮MyISAM,如果既有讀寫也挺頻繁,請使用InnoDB。

  3. 系統奔潰後,MyISAM恢復起來更困難,能否接受;

  4. MySQL5.5版本開始Innodb已經成為Mysql的預設引擎(之前是MyISAM),說明其優勢是有目共睹的,如果你不知道用什麼,那就用InnoDB,至少不會差。

資料庫表建立注意事項

一、欄位名及欄位配製合理性

  1. 剔除關係不密切的欄位

  2. 欄位命名要有規則及相對應的含義(不要一部分英文,一部分拼音,還有類似a.b.c這樣不明含義的欄位)

  3. 欄位命名儘量不要使用縮寫(大多數縮寫都不能明確欄位含義)

  4. 欄位不要大小寫混用(想要具有可讀性,多個英文單詞可使用下劃線形式連線)

  5. 欄位名不要使用保留字或者關鍵字

  6. 保持欄位名和型別的一致性

  7. 慎重選擇數字型別

  8. 給文字欄位留足餘量

二、系統特殊欄位處理及建成後建議

  1. 新增刪除標記(例如操作人、刪除時間)

  2. 建立版本機制

三、表結構合理性配置

  1. 多型欄位的處理,就是表中是否存在欄位能夠分解成更小獨立的幾部分(例如:人可以分為男人和女人)

  2. 多值欄位的處理,可以將表分為三張表,這樣使得檢索和排序更加有調理,且保證資料的完整性!

四、其它建議

  1. 對於大資料欄位,獨立表進行儲存,以便影響效能(例如:簡介欄位)

  2. 使用varchar型別代替char,因為varchar會動態分配長度,char指定長度是固定的。

  3. 給表建立主鍵,對於沒有主鍵的表,在查詢和索引定義上有一定的影響。

  4. 避免表欄位執行為null,建議設定預設值(例如:int型別設定預設值為0)在索引查詢上,效率立顯!

  5. 建立索引,最好建立在唯一和非空的欄位上,建立太多的索引對後期插入、更新都存在一定的影響(考慮實際情況來建立)。

redis

redis單執行緒問題

單執行緒指的是網路請求模組使用了一個執行緒(所以不需考慮併發安全性),即一個執行緒處理所有網路請求,其他模組仍用了多個執行緒。

為什麼說redis能夠快速執行

  1. 絕大部分請求是純粹的記憶體操作(非常快速)
  2. 採用單執行緒,避免了不必要的上下文切換和競爭條件
  3. 非阻塞IO - IO多路複用

redis的內部實現

內部實現採用epoll,採用了epoll+自己實現的簡單的事件框架。epoll中的讀、寫、關閉、連線都轉化成了事件,然後利用epoll的多路複用特性,不在io上浪費一點時間 這3個條件不是相互獨立的,特別是第一條,如果請求都是耗時的,採用單執行緒吞吐量及效能很差。redis為特殊的場景選擇了合適的技術方案。

Redis關於執行緒安全問題

redis實際上是採用了執行緒封閉的觀念,把任務封閉在一個執行緒,自然避免了執行緒安全問題,不過對於需要依賴多個redis操作的複合操作來說,依然需要鎖,而且有可能是分散式鎖。

使用redis有哪些好處?

  1. 速度快,因為資料存在記憶體中,類似於HashMap,HashMap的優勢就是查詢和操作的時間複雜度都是O(1)
  2. 支援豐富資料型別,支援string,list,set,sorted set,hash
  3. 支援事務,操作都是原子性,所謂的原子性就是對資料的更改要麼全部執行,要麼全部不執行
  4. 豐富的特性:可用於快取,訊息,按key設定過期時間,過期後將會自動刪除

redis相比memcached有哪些優勢?

  1. memcached所有的值均是簡單的字串,redis作為其替代者,支援更為豐富的資料型別
  2. redis的速度比memcached快很多
  3. redis可以持久化其資料
  4. Redis支援資料的備份,即master-slave模式的資料備份。
  5. 使用底層模型不同,它們之間底層實現方式 以及與客戶端之間通訊的應用協議不一樣。Redis直接自己構建了VM 機制 ,因為一般的系統呼叫系統函式的話,會浪費一定的時間去移動和請求。
  6. value大小:redis最大可以達到1GB,而memcache只有1MB

Redis主從複製

過程原理:

  1. 當從庫和主庫建立MS關係後,會向主資料庫傳送SYNC命令
  2. 主庫接收到SYNC命令後會開始在後臺儲存快照(RDB持久化過程),並將期間接收到的寫命令快取起來
  3. 當快照完成後,主Redis會將快照檔案和所有快取的寫命令傳送給從Redis
  4. 從Redis接收到後,會載入快照檔案並且執行收到的快取的命令
  5. 之後,主Redis每當接收到寫命令時就會將命令傳送從Redis,從而保證資料的一致

缺點:所有的slave節點資料的複製和同步都由master節點來處理,會照成master節點壓力太大,使用主從從結構來解決

redis兩種持久化方式的優缺點

  1. RDB 持久化可以在指定的時間間隔內生成資料集的時間點快照(point-in-time snapshot)
  2. AOF 持久化記錄伺服器執行的所有寫操作命令,並在伺服器啟動時,通過重新執行這些命令來還原資料集。
  3. Redis 還可以同時使用 AOF 持久化和 RDB 持久化。當redis重啟時,它會有限使用AOF檔案來還原資料集,因為AOF檔案儲存的資料集通常比RDB檔案所儲存的資料集更加完整

RDB的優點:

  1. RDB 是一個非常緊湊(compact)的檔案,它儲存了 Redis 在某個時間點上的資料集。 這種檔案非常適合用於進行備份: 比如說,你可以在最近的 24 小時內,每小時備份一次 RDB 檔案,並且在每個月的每一天,也備份一個 RDB 檔案。 這樣的話,即使遇上問題,也可以隨時將資料集還原到不同的版本。

  2. RDB 非常適用於災難恢復(disaster recovery):它只有一個檔案,並且內容都非常緊湊,可以(在加密後)將它傳送到別的資料中心,或者亞馬遜 S3 中。

  3. RDB 可以最大化 Redis 的效能:父程式在儲存 RDB 檔案時唯一要做的就是 fork 出一個子程式,然後這個子程式就會處理接下來的所有儲存工作,父程式無須執行任何磁碟 I/O 操作。

  4. RDB 在恢復大資料集時的速度比 AOF 的恢復速度要快

Redis 常見的效能問題都有哪些?如何解決?

  1. Master寫記憶體快照,save命令排程rdbSave函式,會阻塞主執行緒的工作,當快照比較大時對效能影響是非常大的,會間斷性暫停服務,所以Master最好不要寫記憶體快照。
  2. Master AOF持久化,如果不重寫AOF檔案,這個持久化方式對效能的影響是最小的,但是AOF檔案會不斷增大,AOF檔案過大會影響Master重啟的恢復速度。Master最好不要做任何持久化工作,包括記憶體快照和AOF日誌檔案,特別是不要啟用記憶體快照做持久化,如果資料比較關鍵,某個Slave開啟AOF備份資料,策略為每秒同步一次。
  3. Master呼叫BGREWRITEAOF重寫AOF檔案,AOF在重寫的時候會佔大量的CPU和記憶體資源,導致服務load過高,出現短暫服務暫停現象。
  4. Redis主從複製的效能問題,為了主從複製的速度和連線的穩定性,Slave和Master最好在同一個區域網內

redis 提供 6種資料淘汰策略

  1. volatile-lru:從已設定過期時間的資料集(server.db[i].expires)中挑選最近最少使用的資料淘汰
  2. volatile-ttl:從已設定過期時間的資料集(server.db[i].expires)中挑選將要過期的資料淘汰
  3. volatile-random:從已設定過期時間的資料集(server.db[i].expires)中任意選擇資料淘汰
  4. allkeys-lru:從資料集(server.db[i].dict)中挑選最近最少使用的資料淘汰
  5. allkeys-random:從資料集(server.db[i].dict)中任意選擇資料淘汰
  6. no-enviction(驅逐):禁止驅逐資料

java

java虛擬機器

這裡寫圖片描述
圖片來源(Hollis微信公眾號)

什麼時候會觸發full gc

  1. System.gc()方法的呼叫
  2. 老年代空間不足
  3. 永生區空間不足(JVM規範中執行時資料區域中的方法區,在HotSpot虛擬機器中又被習慣稱為永生代或者永生區,Permanet Generation中存放的為一些class的資訊、常量、靜態變數等資料)
  4. GC時出現promotion failed和concurrent mode failure
  5. 統計得到的Minor GC晉升到舊生代平均大小大於老年代剩餘空間
  6. 堆中分配很大的物件

可以作為root的物件:

  1. 類中的靜態變數,當它持有一個指向一個物件的引用時,它就作為root
  2. 活動著的執行緒,可以作為root
  3. 一個Java方法的引數或者該方法中的區域性變數,這兩種物件可以作為root
  4. JNI方法中的區域性變數或者引數,這兩種物件可以作為root

例子:下述的Something和Apple都可以作為root物件。

public AClass{
 
  public static Something;
  public static final Apple;
   ''''''
}
複製程式碼

Java方法的引數和方法中的區域性變數,可以作為root.

public Aclass{

public void doSomething(Object A){
    ObjectB b = new ObjectB; 
    }
 }
複製程式碼

新生代轉移到老年代的觸發條件

  1. 長期存活的物件
  2. 大物件直接進入老年代
  3. minor gc後,survivor仍然放不下
  4. 動態年齡判斷 ,大於等於某個年齡的物件超過了survivor空間一半 ,大於等於某個年齡的物件直接進入老年代

G1和CMS的區別

  1. G1同時回收老年代和年輕代,而CMS只能回收老年代,需要配合一個年輕代收集器。另外G1的分代更多是邏輯上的概念,G1將記憶體分成多個等大小的region,Eden/ Survivor/Old分別是一部分region的邏輯集合,物理上記憶體地址並不連續。
    這裡寫圖片描述
  2. CMS在old gc的時候會回收整個Old區,對G1來說沒有old gc的概念,而是區分Fully young gc和Mixed gc,前者對應年輕代的垃圾回收,後者混合了年輕代和部分老年代的收集,因此每次收集肯定會回收年輕代,老年代根據記憶體情況可以不回收或者回收部分或者全部(這種情況應該是可能出現)。

雙親委派模型中有哪些方法。使用者如何自定義類載入器 。怎麼打破雙親委託機制

  1. 雙親委派模型中用到的方法:
  • findLoadedClass(),
  • loadClass()
  • findBootstrapClassOrNull()
  • findClass()
  • defineClass():把二進位制資料轉換成位元組碼。
  • resolveClass()

自定義類載入器的方法:繼承 ClassLoader 類,重寫 findClass()方法 。

  1. 繼承ClassLoader覆蓋loadClass方法 原順序
  2. findLoadedClass
  3. 委託parent載入器載入(這裡注意bootstrap載入器的parent為null)
  4. 自行載入 打破委派機制要做的就是打亂2和3的順序,通過類名篩選自己要載入的類,其他的委託給parent載入器。

即時編譯器的優化方法

位元組碼可以通過以下兩種方式轉換成合適的語言:

  1. 直譯器
  2. 即時編譯器 即時編譯器把整段位元組碼編譯成原生程式碼,執行原生程式碼比一條一條進行解釋執行的速度快很多,因為原生程式碼是儲存在快取裡的

編譯過程的五個階段

  1. 第一階段:詞法分析
  2. 第二階段:語法分析
  3. 第三階段:詞義分析與中間程式碼產生
  4. 第四階段:優化
  5. 第五階段:目的碼生成

java應用系統執行速度慢的解決方法

問題解決思路:

  1. 檢視部署應用系統的系統資源使用情況,CPU,記憶體,IO這幾個方面去看。找到對就的程式。
  2. 使用jstack,jmap等命令檢視是JVM是在在什麼型別的記憶體空間中做GC(記憶體回收),和檢視GC日誌檢視是那段程式碼在佔用記憶體。 首先,調節記憶體的引數設定,如果還是一樣的問題,就要定位到相應的程式碼。
  3. 定位程式碼,修改程式碼(一般是程式碼的邏輯問題,或者程式碼獲取的資料量過大。)

記憶體溢位是什麼,什麼原因導致的

記憶體溢位是指應用系統中存在無法回收的記憶體或使用的記憶體過多,最終使得程式執行要用到的記憶體大於虛擬機器能提供的最大記憶體。為了解決Java中記憶體溢位問題,我們首先必須瞭解Java是如何管理記憶體的。Java的記憶體管理就是物件的分配和釋放問題。在Java中,記憶體的分配是由程式完成的,而記憶體的釋放是由垃圾收集器(Garbage Collection,GC)完成的,程式設計師不需要通過呼叫GC函式來釋放記憶體,因為不同的JVM實現者可能使用不同的演算法管理GC,有的是記憶體使用到達一定程度時,GC才開始工作,也有定時執行的,有的是中斷式執行GC。但GC只能回收無用並且不再被其它物件引用的那些物件所佔用的空間。Java的記憶體垃圾回收機制是從程式的主要執行物件開始檢查引用鏈,當遍歷一遍後發現沒有被引用的孤立物件就作為垃圾回收。

引起記憶體溢位的原因有很多種,常見的有以下幾種:

  1. 記憶體中載入的資料量過於龐大,如一次從資料庫取出過多資料;

  2. 集合類中有對物件的引用,使用完後未清空,使得JVM不能回收;

  3. 程式碼中存在死迴圈或迴圈產生過多重複的物件實體;

  4. 使用的第三方軟體中的BUG;

  5. 啟動引數記憶體值設定的過小;

記憶體溢位的解決

記憶體溢位雖然很棘手,但也有相應的解決辦法,可以按照從易到難,一步步的解決。

第一步,就是修改JVM啟動引數,直接增加記憶體。這一點看上去似乎很簡單,但很容易被忽略。JVM預設可以使用的記憶體為64M,Tomcat預設可以使用的記憶體為128MB,對於稍複雜一點的系統就會不夠用。在某專案中,就因為啟動引數使用的預設值,經常報“OutOfMemory”錯誤。因此,-Xms,-Xmx引數一定不要忘記加。

第二步,檢查錯誤日誌,檢視“OutOfMemory”錯誤前是否有其它異常或錯誤。在一個專案中,使用兩個資料庫連線,其中專用於傳送簡訊的資料庫連線使用DBCP連線池管理,使用者為不將簡訊發出,有意將資料庫連線使用者名稱改錯,使得日誌中有許多資料庫連線異常的日誌,一段時間後,就出現“OutOfMemory”錯誤。經分析,這是由於DBCP連線池BUG引起的,資料庫連線不上後,沒有將連線釋放,最終使得DBCP報“OutOfMemory”錯誤。經過修改正確資料庫連線引數後,就沒有再出現記憶體溢位的錯誤。

檢視日誌對於分析記憶體溢位是非常重要的,通過仔細檢視日誌,分析記憶體溢位前做過哪些操作,可以大致定位有問題的模組。

第三步,找出可能發生記憶體溢位的位置。重點排查以下幾點:

  1. 檢查程式碼中是否有死迴圈或遞迴呼叫。

  2. 檢查是否有大迴圈重複產生新物件實體。

  3. 檢查對資料庫查詢中,是否有一次獲得全部資料的查詢。一般來說,如果一次取十萬條記錄到記憶體,就可能引起記憶體溢位。這個問題比較隱蔽,在上線前,資料庫中資料較少,不容易出問題,上線後,資料庫中資料多了,一次查詢就有可能引起記憶體溢位。因此對於資料庫查詢儘量採用分頁的方式查詢。

  4. 檢查List、MAP等集合物件是否有使用完後,未清除的問題。List、MAP等集合物件會始終存有對物件的引用,使得這些物件不能被GC回收。

第四步,使用記憶體檢視工具動態檢視記憶體使用情況。某個專案上線後,每次系統啟動兩天後,就會出現記憶體溢位的錯誤。這種情況一般是程式碼中出現了緩慢的記憶體洩漏,用上面三個步驟解決不了,這就需要使用記憶體檢視工具了。

記憶體檢視工具有許多,比較有名的有:Optimizeit Profiler、JProbe Profiler、JinSight和Java1.5的Jconsole等。它們的基本工作原理大同小異,都是監測Java程式執行時所有物件的申請、釋放等動作,將記憶體管理的所有資訊進行統計、分析、視覺化。開發人員可以根據這些資訊判斷程式是否有記憶體洩漏問題。一般來說,一個正常的系統在其啟動完成後其記憶體的佔用量是基本穩定的,而不應該是無限制的增長的。持續地觀察系統執行時使用的記憶體的大小,可以看到在記憶體使用監控視窗中是基本規則的鋸齒形的圖線,如果記憶體的大小持續地增長,則說明系統存在記憶體洩漏問題。通過間隔一段時間取一次記憶體快照,然後對記憶體快照中物件的使用與引用等資訊進行比對與分析,可以找出是哪個類的物件在洩漏。

通過以上四個步驟的分析與處理,基本能處理記憶體溢位的問題。當然,在這些過程中也需要相當的經驗與敏感度,需要在實際的開發與除錯過程中不斷積累。

總體上來說,產生記憶體溢位是由於程式碼寫的不好造成的,因此提高程式碼的質量是最根本的解決辦法。有的人認為先把功能實現,有BUG時再在測試階段進行修正,這種想法是錯誤的。正如一件產品的質量是在生產製造的過程中決定的,而不是質量檢測時決定的,軟體的質量在設計與編碼階段就已經決定了,測試只是對軟體質量的一個驗證,因為測試不可能找出軟體中所有的BUG。

java併發

JAVA 執行緒狀態轉換圖示

這裡寫圖片描述

synchronized 的底層怎麼實現

  1. 同步程式碼塊(Synchronization)基於進入和退出管程(Monitor)物件實現。每個物件有一個監視器鎖(monitor)。當monitor被佔用時就會處於鎖定狀態,執行緒執行monitorenter指令時嘗試獲取monitor的所有權,過程如下:
  • 如果monitor的進入數為0,則該執行緒進入monitor,然後將進入數設定為1,該執行緒即為monitor的所有者。

  • 如果執行緒已經佔有該monitor,只是重新進入,則進入monitor的進入數加1.

  • 如果其他執行緒已經佔用了monitor,則該執行緒進入阻塞狀態,直到monitor的進入數為0,再重新嘗試獲取monitor的所有權。

  1. 被 synchronized 修飾的同步方法並沒有通過指令monitorenter和monitorexit來完成(理論上其實也可以通過這兩條指令來實現),不過相對於普通方法,其常量池中多了ACC_SYNCHRONIZED標示符。JVM就是根據該標示符來實現方法的同步的:當方法呼叫時,呼叫指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設定,如果設定了,執行執行緒將先獲取monitor,獲取成功之後才能執行方法體,方法執行完後再釋放monitor。在方法執行期間,其他任何執行緒都無法再獲得同一個monitor物件。 其實本質上沒有區別,只是方法的同步是一種隱式的方式來實現,無需通過位元組碼來完成

講一下CAS

CAS,compare and swap的縮寫,中文翻譯成比較並交換。樂觀鎖用到的機制就是CAS,每次不加鎖而是假設沒有衝突而去完成某項操作,如果因為衝突失敗就重試。

原理:

  1. CAS有3個運算元,記憶體值V,舊的預期值A,要修改的新值B。當且僅當預期值A和記憶體值V相同時,將記憶體值V修改為B,否則什麼都不做。

JDK文件說cas同時具有volatile讀和volatile寫的記憶體語義。

缺點:

  1. ABA問題。 因為CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化

  2. 迴圈時間長開銷大。 自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷。

  3. 只能保證一個共享變數的原子操作。 對多個共享變數操作時,迴圈CAS就無法保證操作的原子性,這個時候就可以用鎖,或者有一個取巧的辦法,就是把多個共享變數合併成一個共享變數來操作。比如有兩個共享變數i=2,j=a,合併一下ij=2a,然後用CAS來操作ij。從Java1.5開始JDK提供了AtomicReference類來保證引用物件之間的原子性,你可以把多個變數放在一個物件裡來進行CAS操作。

執行緒池

Executor執行緒池框架是一個根據一組執行策略呼叫,排程,執行和控制的非同步任務的框架。

ThreadPoolExecutor執行的策略

  1. 執行緒數量未達到corePoolSize,則新建一個執行緒(核心執行緒)執行任務
  2. 執行緒數量達到了corePools,則將任務移入佇列等待
  3. 佇列已滿,新建執行緒(非核心執行緒)執行任務
  4. 佇列已滿,匯流排程數又達到了maximumPoolSize,就會由(RejectedExecutionHandler)丟擲異常

新建執行緒 -> 達到核心數 -> 加入佇列 -> 新建執行緒(非核心) -> 達到最大數 -> 觸發拒絕策略

常見四種執行緒池

  1. CachedThreadPool():可快取執行緒池。
  • 執行緒數無限制
  • 有空閒執行緒則複用空閒執行緒,若無空閒執行緒則新建執行緒
  • 一定程式減少頻繁建立/銷燬執行緒,減少系統開銷
  1. FixedThreadPool():定長執行緒池。
  • 可控制執行緒最大併發數(同時執行的執行緒數)
  • 超出的執行緒會在佇列中等待
  1. ScheduledThreadPool():定時執行緒池。
  • 支援定時及週期性任務執行。
  1. SingleThreadExecutor():單執行緒化的執行緒池。
  • 有且僅有一個工作執行緒執行任務
  • 所有任務按照指定順序執行,即遵循佇列的入隊出隊規則

四種拒絕策略

  1. AbortPolicy:拒絕任務,且還丟擲RejectedExecutionException異常,執行緒池預設策略
  2. CallerRunPolicy:拒絕新任務進入,如果該執行緒池還沒有被關閉,那麼這個新的任務在執行執行緒中被呼叫
  3. DiscardOldestPolicy: 如果執行程式尚未關閉,則位於頭部的任務將會被移除,然後重試執行任務(再次失敗,則重複該過程),這樣將會導致新的任務將會被執行,而先前的任務將會被移除。
  4. DiscardPolicy:沒有新增進去的任務將會被拋棄,也不丟擲異常。基本上為靜默模式。

為什麼要用執行緒池

  1. 減少了建立和銷燬執行緒的次數,每個工作執行緒都可以被重複利用,可執行多個任務。
  2. 運用執行緒池能有效的控制執行緒最大併發數,可以根據系統的承受能力,調整執行緒池中工作線執行緒的數目,防止因為消耗過多的記憶體,而把伺服器累趴下(每個執行緒需要大約1MB記憶體,執行緒開的越多,消耗的記憶體也就越大,最後當機)。
  3. 對執行緒進行一些簡單的管理,比如:延時執行、定時迴圈執行的策略等,運用執行緒池都能進行很好的實現

物件鎖和靜態鎖之間的區別

  1. 物件鎖用於物件例項方法,
  2. 類鎖用於類的靜態方法或一個類的class物件。
  3. 類的物件例項可以有很多,不同物件例項的物件鎖互不干擾,而每個類只有一個類鎖

簡述volatile字

兩個特性

  1. 保證了不同執行緒對這個變數進行 讀取 時的可見性,即一個執行緒修改 了某個變數的值 , 這新值對其他執行緒來說是立即可見的 。(volatile 解決了 執行緒間 共享變數
  2. 禁止進行指令重排序 ,阻止編譯器對程式碼的優化

要想併發程式正確地執行,必須要保證原子性、可見性以及有序性,鎖保證了原子性,而volatile保證可見性和有序性

happens-before 原則(先行發生原則):

  1. 程式次序規則:一個執行緒內,按照程式碼順序,書寫在前面的操作先行發生於書寫在 後面的操作
  2. 鎖定規則:一個 unLock 操作先行發生於後面對同一個鎖的 lock 操作
  3. volatile 變數規則:對一個變數的寫操作先行發生於後面對這個變數的讀操作
  4. 傳遞規則:如果操作 A 先行發生於操作 B,而操作 B 又先行發生於操作 C,則可以 得出操作 A 先行發生於操作 C
  5. 執行緒啟動規則:Thread 物件的 start()方法先行發生於此執行緒的每個一個動作
  6. 執行緒中斷規則:對執行緒 interrupt()方法的呼叫先行發生於被中斷執行緒的程式碼檢測 到中斷事件的發生
  7. 執行緒終結規則:執行緒中所有的操作都先行發生於執行緒的終止檢測,我們可以通過 T hread.join()方法結束、Thread.isAlive()的返回值手段檢測到執行緒已經終止執行
  8. 物件終結規則:一個物件的初始化完成先行發生於他的 finalize()方法的開始

Lock 和synchronized 的區別

  1. Lock 是一個 介面,而 synchronized 是 Java 中的 關鍵字, synchronized 是 內建的語言實現;

  2. synchronized 在 發生異常時,會 自動釋放執行緒佔有的鎖,因此 不會導 致死鎖現象發生;而 Lock 在發生異常時,如果沒有主動通過 unLock()去釋放 鎖,則很 可能造成死鎖現象,因此用 使用 Lock 時需要在 finally 塊中釋放鎖;

  3. Lock 可以讓 等待鎖的執行緒響應中斷 (可中斷鎖),而 synchronized 卻不行,使用 synchronized 時,等待的執行緒會一直等待下去, 不能夠響應中 斷 (不可中斷鎖);

  4. 通過 Lock 可以知道 有沒有成功獲取鎖 (tryLock ( ) 方法 : 如果獲取 了鎖 ,回 則返回 true ;回 否則返回 false e, , 也就說這個方法無論如何都會立即返回 。 在拿不到鎖時不會一直在那等待。 ),而 synchronized 卻無法辦到。

  5. Lock 可以提高 多個執行緒進行讀操作的效率( 讀寫鎖)。

  6. Lock 可以實現 公平鎖,synchronized 不保證公平性。 在效能上來說,如果執行緒競爭資源不激烈時,兩者的效能是差不多的,而 當競爭資源非常激烈時(即有大量執行緒同時競爭),此時 Lock 的效能要遠遠優 於 synchronized。所以說,在具體使用時要根據適當情況選擇。

ThreadLocal(執行緒變數副本)

Synchronized實現記憶體共享,ThreadLocal為每個執行緒維護一個本地變數。 採用空間換時間,它用於執行緒間的資料隔離,為每一個使用該變數的執行緒提供一個副本,每個執行緒都可以獨立地改變自己的副本,而不會和其他執行緒的副本衝突。 ThreadLocal類中維護一個Map,用於儲存每一個執行緒的變數副本,Map中元素的鍵為執行緒物件,而值為對應執行緒的變數副本。 ThreadLocal在Spring中發揮著巨大的作用,在管理Request作用域中的Bean、事務管理、任務排程、AOP等模組都出現了它的身影。 Spring中絕大部分Bean都可以宣告成Singleton作用域,採用ThreadLocal進行封裝,因此有狀態的Bean就能夠以singleton的方式在多執行緒中正常工作了。

通過Callable和Future建立執行緒

Java 5在concurrency包中引入了java.util.concurrent.Callable 介面,它和Runnable介面很相似,但它可以返回一個物件或者丟擲一個異常。

Callable介面使用泛型去定義它的返回型別。Executors類提供了一些有用的方法去線上程池中執行Callable內的任務。由於Callable任務是並行的,我們必須等待它返回的結果。java.util.concurrent.Future物件為我們解決了這個問題。線上程池提交Callable任務返回了一個Future物件,使用它我們可以知道Callable任務的狀態和得到Callable返回的執行結果。Future提供了get()方法讓我們可以等待Callable結束並獲取它的執行結果

  1. 建立Callable介面的實現類,並實現call()方法,該call()方法將作為執行緒執行體,並且有返回值。
  2. 建立Callable實現類的例項,使用FutureTask類來包裝Callable物件,該FutureTask物件封裝了該Callable物件的call()方法的返回值。
  3. 使用FutureTask物件作為Thread物件的target建立並啟動新執行緒
  4. 呼叫FutureTask物件的get()方法來獲得子執行緒執行結束後的返回值

什麼叫守護執行緒,用什麼方法實現守護執行緒(Thread.setDeamon()的含義)

在Java中有兩類執行緒:User Thread(使用者執行緒)、Daemon Thread(守護執行緒) 用個比較通俗的比如,任何一個守護執行緒都是整個JVM中所有非守護執行緒的保姆: 只要當前JVM例項中尚存在任何一個非守護執行緒沒有結束,守護執行緒就;只有當最後一個非守護執行緒結束時,守護執行緒隨著JVM一同結束工作。 JVM內部的實現是如果執行的程式只剩下守護執行緒的話,程式將終止執行,直接結束。所以守護執行緒是作為輔助執行緒存在的,主要的作用是提供計數等等輔助的功能。

如何停止一個執行緒?

終止執行緒的三種方法:

  1. 使用退出標誌,使執行緒正常退出,也就是當run方法完成後執行緒終止。 在定義退出標誌exit時,使用了一個Java關鍵字volatile,這個關鍵字的目的是使exit同步,也就是說在同一時刻只能由一個執行緒來修改exit的值,
  thread.exit = true;  // 終止執行緒thread 
複製程式碼
  1. 使用stop方法強行終止執行緒(這個方法不推薦使用,因為stop和suspend、resume一樣,也可能發生不可預料的結果)。 使用stop方法可以強行終止正在執行或掛起的執行緒。我們可以使用如下的程式碼來終止執行緒: thread.stop(); 雖然使用上面的程式碼可以終止執行緒,但使用stop方法是很危險的,就象突然關閉計算機電源,而不是按正常程式關機一樣,可能會產生不可預料的結果,因此,並不推薦使用stop方法來終止執行緒。

  2. 使用interrupt方法中斷執行緒,使用interrupt方法來終端執行緒可分為兩種情況:

  • 執行緒處於阻塞狀態,如使用了sleep方法。
  • 使用while(!isInterrupted()){……}來判斷執行緒是否被中斷。 在第一種情況下使用interrupt方法,sleep方法將丟擲一個InterruptedException例外,而在第二種情況下執行緒將直接退出。

注意:在Thread類中有兩個方法可以判斷執行緒是否通過interrupt方法被終止。一個是靜態的方法interrupted(),一個是非靜態的方法isInterrupted(),這兩個方法的區別是interrupted用來判斷當前線是否被中斷,而isInterrupted可以用來判斷其他執行緒是否被中斷。因此,while (!isInterrupted())也可以換成while (!Thread.interrupted())。

什麼是執行緒安全?什麼是執行緒不安全?

  1. 執行緒安全就是多執行緒訪問時,採用了加鎖機制,當一個執行緒訪問該類的某個資料時,進行保護,其他執行緒不能進行訪問直到該執行緒讀取完,其他執行緒才可使用。不會出現資料不一致或者資料汙染。
  2. 執行緒不安全就是不提供資料訪問保護,有可能出現多個執行緒先後更改資料造成所得到的資料是髒資料 在多執行緒的情況下,由於同一程式的多個執行緒共享同一片儲存空間,在帶來方便的同時,也帶來了訪問衝突這個嚴重的問題。Java語言提供了專門機制以解決這種衝突,有效避免了同一個資料物件被多個執行緒同時訪問。

java容器

HashSet和TreeSet區別

HashSet

  1. 不能保證元素的排列順序,順序有可能發生變化
  2. 不是同步的
  3. 集合元素可以是null,但只能放入一個null 當向HashSet結合中存入一個元素時,HashSet會呼叫該物件的hashCode()方法來得到該物件的hashCode值,然後根據 hashCode值來決定該物件在HashSet中儲存位置。

TreeSet

  1. TreeSet是SortedSet介面的唯一實現類
  2. TreeSet可以確保集合元素處於排序狀態。TreeSet支援兩種排序方式,自然排序 和定製排序,其中自然排序為預設的排序方式。向TreeSet中加入的應該是同一個類的物件

講一下LinkedHashMap

LinkedHashMap的實現就是HashMap+LinkedList的實現方式,以HashMap維護資料結構,以LinkList的方式維護資料插入順序

LinkedHashMap儲存了記錄的插入順序,在用Iterator遍歷LinkedHashMap時,先得到的記錄肯定是先插入的。 在遍歷的時候會比HashMap慢TreeMap能夠把它儲存的記錄根據鍵排序,預設是按升序排序,也可以指定排序的比較器

利用LinkedHashMap實現LRU演算法快取(

  1. LinkedList首先它是一個Map,Map是基於K-V的,和快取一致
  2. LinkedList提供了一個boolean值可以讓使用者指定是否實現LRU)

Java8 中HashMap的優化(引入紅黑樹的資料結構和擴容的優化)

  1. if (binCount >= TREEIFY_THRESHOLD - 1) 當符合這個條件的時候,把連結串列變成treemap紅黑樹,這樣查詢效率從o(n)變成了o(log n) ,在JDK1.8的實現中,優化了高位運算的演算法,通過hashCode()的高16位異或低16位實現的:
  2. 我們使用的是2次冪的擴充套件(指長度擴為原來2倍),所以,元素的位置要麼是在原位置,要麼是在原位置再移動2次冪的位置

這裡的Hash演算法本質上就是三步:取key的hashCode值、高位運算、取模運算。

元素在重新計算hash之後,因為n變為2倍,那麼n-1的mask範圍在高位多1bit(紅色),因此新的index就會發生這樣的變化: hashMap 1.8 雜湊演算法例圖2

這裡寫圖片描述
因此,我們在擴充HashMap的時候,不需要像JDK1.7的實現那樣重新計算hash,只需要看看原來的hash值新增的那個bit是1還是0就好了,是0的話索引沒變,是1的話索引變成“原索引+oldCap”

Map遍歷的keySet()和entrySet()效能差異原因

Set<Entry<String, String>> entrySet = map.entrySet();
Set<String> set = map.keySet();` 
複製程式碼
  1. keySet()迴圈中通過key獲取對應的value的時候又會呼叫getEntry()進行迴圈。迴圈兩次
  2. entrySet()直接使用getEntry()方法獲取結果,迴圈一次
  3. 所以 keySet()的效能會比entrySet()差點。所以遍歷map的話還是用entrySet()來遍歷
 public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }    
複製程式碼
final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        int hash = (key == null) ? 0 : hash(key);
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
}
複製程式碼

java基礎

抽象類和介面的對比

引數 抽象類 介面
預設的方法實現 它可以有預設的方法實現 介面完全是抽象的。它根本不存在方法的實現
實現 子類使用extends關鍵字來繼承抽象類。如果子類不是抽象類的話,它需要提供抽象類中所有宣告的方法的實現。 子類使用關鍵字implements來實現介面。它需要提供介面中所有宣告的方法的實現
構造器 抽象類可以有構造器 介面不能有構造器
與正常Java類的區別 除了你不能例項化抽象類之外,它和普通Java類沒有任何區別 介面是完全不同的型別
訪問修飾符 抽象方法可以有public、protected和default這些修飾符 介面方法預設修飾符是public。你不可以使用其它修飾符。
main方法 抽象方法可以有main方法並且我們可以執行它 介面沒有main方法,因此我們不能執行它。
多繼承 抽象方法可以繼承一個類和實現多個介面 介面只可以繼承一個或多個其它介面
速度 它比介面速度要快 介面是稍微有點慢的,因為它需要時間去尋找在類中實現的方法。
新增新方法 如果你往抽象類中新增新的方法,你可以給它提供預設的實現。因此你不需要改變你現在的程式碼。 如果你往介面中新增方法,那麼你必須改變實現該介面的類。

建立一個類的幾種方法?

  1. 使用new關鍵字 → 呼叫了建構函式
  2. 使用Class類的newInstance方法 → 呼叫了建構函式
Employee emp2 = (Employee)Class.forName("org.programming.mitra.exercises.Employee").newInstance();
複製程式碼
  1. 使用Constructor類的newInstance方法 → 呼叫了建構函式
Constructor<Employee> constructor = Employee.class.getConstructor();
Employee emp3 = constructor.newInstance();
複製程式碼
  1. 使用clone方法 → 沒有呼叫建構函式
  2. 使用反序列化 }→ 沒有呼叫建構函式
ObjectInputStream in = new ObjectInputStream(new FileInputStream("data.obj"));
Employee emp5 = (Employee) in.readObject();
複製程式碼

Redirect和forward

  1. 上圖所示的間接轉發請求的過程如下: 瀏覽器向Servlet1發出訪問請求; Servlet1呼叫sendRedirect()方法,將瀏覽器重定向到Servlet2; 瀏覽器向servlet2發出請求; 最終由Servlet2做出響應。

  2. 上圖所示的直接轉發請求的過程如下: 瀏覽器向Servlet1發出訪問請求; Servlet1呼叫forward()方法,在伺服器端將請求轉發給Servlet2; 最終由Servlet2做出響應。

什麼是泛型,為什麼要使用以及型別擦除。

  1. 泛型的本質就是“引數化型別”,也就是說所操作的資料型別被指定為一個引數。 建立集合時就指定集合元素的資料型別,該集合只能儲存其指定型別的元素, 避免使用強制型別轉換。
  2. Java 編譯器生成的位元組碼是不包含泛型資訊的,泛型型別資訊將在 編譯處理 時 被擦除,這個過程即 型別擦除。型別擦除可以簡單的理解為將泛型 java 程式碼轉 換為普通 java 程式碼,只不過編譯器更直接點,將泛型 java 程式碼直接轉換成普通 java 位元組碼。

型別擦除的主要過程如下:

  1. 將所有的泛型引數用其最左邊界(最頂級的父型別)型別替換。
  2. 移除所有的型別引數。

Object跟這些標記符代表的java型別有啥區別呢?

Object是所有類的根類,任何類的物件都可以設定給該Object引用變數,使用的時候可能需要型別強制轉換,但是用使用了泛型T、E等這些識別符號後,在實際用之前型別就已經確定了,不需要再進行型別強制轉換。

Error類和Exception類區別

  1. Error類和Exception類的父類都是throwable類,他們的區別是: Error類一般是指與虛擬機器相關的問題,如系統崩潰,虛擬機器錯誤,記憶體空間不足,方法呼叫棧溢等。對於這類錯誤的導致的應用程式中斷,僅靠程式本身無法恢復和和預防,遇到這樣的錯誤,建議讓程式終止。 Exception類表示程式可以處理的異常,可以捕獲且可能恢復。遇到這類異常,應該儘可能處理異常,使程式恢復執行, 而不應該隨意終止異常。
  2. Exception類又分為執行時異常(Runtime Exception)和受檢查的異常(Checked Exception ),執行時異常;ArithmaticException,IllegalArgumentException,編譯能通過,但是一執行就終止了,程式不會處理執行時異常,出現這類異常,程式會終止。而受檢查的異常,要麼用try。。。catch捕獲,要麼用throws字句宣告丟擲,交給它的父類處理,否則編譯不會通過。

throw和throws區別

throw:(針對物件的做法) 丟擲一個異常,可以是系統定義的,也可以是自己定義的

public void yichang(){
    NumberFormatException e = new NumberFormatException();
    throw e;
}
複製程式碼

throws:(針對一個方法丟擲的異常) 丟擲一個異常,可以是系統定義的,也可以是自己定義的。

public void yichang() throws NumberFormatException{
    int a = Integer.parseInt("10L");
}
複製程式碼
  1. throws出現在方法函式頭;而throw出現在函式體。
  2. throws表示出現異常的一種可能性,並不一定會發生這些異常;throw則是丟擲了異常,執行throw則一定丟擲了某種異常。
  3. 兩者都是消極處理異常的方式(這裡的消極並不是說這種方式不好),只是丟擲或者可能丟擲異常,但是不會由函式去處理異常,真正的處理異常由函式的上層呼叫處理。

.class 檔案是什麼型別檔案

class檔案是一種8位位元組的二進位制流檔案

java中序列化之子類繼承父類序列化

父類實現了Serializable,子類不需要實現Serializable

相關注意事項 1. 序列化時,只對物件的狀態進行儲存,而不管物件的方法; 2. 當一個父類實現序列化,子類自動實現序列化,不需要顯式實現Serializable介面; c)當一個物件的例項變數引用其他物件,序列化該物件時也把引用物件進行序列化; 3. 並非所有的物件都可以序列化,至於為什麼不可以,有很多原因了,比如: 1.安全方面的原因,比如一個物件擁有private,public等field,對於一個要傳輸的物件,比如寫到檔案,或者進行rmi傳輸等等,在序列化進行傳輸的過程中,這個物件的private等域是不受保護的。 2. 資源分配方面的原因,比如socket,thread類,如果可以序列化,進行傳輸或者儲存,也無法對他們進行重新的資源分配,而且,也是沒有必要這樣實現。

2,反過來父類未實現Serializable,子類實現了,序列化子類例項的時候,父類的屬性是直接被跳過不儲存,還是能儲存但不能還原?(答案:值不儲存)

解:父類實現介面後,所有派生類的屬性都會被序列化。子類實現介面的話,父類的屬性值丟失。

java中序列化之子類繼承父類序列化

識別符號

識別符號可以包括這4種字元:字母、下劃線、$、數字;開頭不能是數字;不能是關鍵字

Integer i=new Integer(127);和Integer i=127;的區別

Integer i = 127的時候,使用Java常量池技術,是為了方便快捷地建立某些物件,當你需要一個物件時候,就去這個池子裡面找,找不到就在池子裡面建立一個。但是必須注意 如果物件是用new 建立的。那麼不管是什麼對像,它是不會放到池子裡的,而是向堆申請新的空間儲存。Byte,Short,Integer,Long,Character這5種整型的包裝類也只是在對應值在-128到127之間的數時才可使用物件池。超過了就要申請空間建立物件了

    int i1=128;
    Integer i2=128;
    Integer i3=new Integer(128);//自動拆箱
    
    System.out.println(i1==i2);//true
    System.out.println(i1==i3);//true
    
    Integer i5=127;
    Integer i6=127;
    System.out.println(i5==i6);//true
    
    
    Integer i5=127;
    Integer ii5=new Integer(127);
    System.out.println(i5==ii5);//false
    
    Integer i7=new Integer(127);
    Integer i8=new Integer(127);
    System.out.println(i7==i8);//false
複製程式碼

手寫單例模式

最好的單例模式是靜態內部類,不要寫雙重檢驗

private static class LazySomethingHolder {
  public static Something something = new Something();
}

public static Something getInstance() {
  return LazySomethingHolder.something;
}
複製程式碼

為什麼執行緒通訊的方法wait(), notify()和notifyAll()被定義在Object類裡?

Java的每個物件中都有一個鎖(monitor,也可以成為監視器) 並且wait(),notify()等方法用於等待物件的鎖或者通知其他執行緒物件的監視器可用。在Java的執行緒中並沒有可供任何物件使用的鎖和同步器。這就是為什麼這些方法是Object類的一部分,這樣Java的每一個類都有用於執行緒間通訊的基本方法

Java中wait 和sleep 方法比較

  1. 這兩個方法來自不同的類分別是Thread和Object

  2. 最主要是sleep方法沒有釋放鎖,而wait方法釋放了鎖,使得其他執行緒可以使用同步控制塊或者方法。

  3. wait,notify和notifyAll只能在同步控制方法或者同步控制塊裡面使用,而sleep可以在任何地方使用(使用範圍)

  4. sleep必須捕獲異常,而wait,notify和notifyAll不需要捕獲異常

  5. sleep方法屬於Thread類中方法,表示讓一個執行緒進入睡眠狀態,等待一定的時間之後,自動醒來進入到可執行狀態,不會馬上進入執行狀態,因為執行緒排程機制恢復執行緒的執行也需要時間,一個執行緒物件呼叫了sleep方法之後,並不會釋放他所持有的所有物件鎖,所以也就不會影響其他程式物件的執行。但在sleep的過程中過程中有可能被其他物件呼叫它的interrupt(),產生InterruptedException異常,如果你的程式不捕獲這個異常,執行緒就會異常終止,進入TERMINATED狀態,如果你的程式捕獲了這個異常,那麼程式就會繼續執行catch語句塊(可能還有finally語句塊)以及以後的程式碼。

  • 注意sleep()方法是一個靜態方法,也就是說他只對當前物件有效,通過t.sleep()讓t物件進入sleep,這樣的做法是錯誤的,它只會是使當前執行緒被sleep 而不是t執行緒
  1. wait屬於Object的成員方法,一旦一個物件呼叫了wait方法,必須要採用notify()和notifyAll()方法喚醒該程式;如果執行緒擁有某個或某些物件的同步鎖,那麼在呼叫了wait()後,這個執行緒就會釋放它持有的所有同步資源,而不限於這個被呼叫了wait()方法的物件。wait()方法也同樣會在wait的過程中有可能被其他物件呼叫interrupt()方法而產生

hashCode和equals方法的關係

在有些情況下,程式設計者在設計一個類的時候為需要重寫equals方法,比如String類,但是千萬要注意,在重寫equals方法的同時,必須重寫hashCode方法。 也就是說對於兩個物件,如果呼叫equals方法得到的結果為true,則兩個物件的hashcode值必定相等; 如果equals方法得到的結果為false,則兩個物件的hashcode值不一定不同; 如果兩個物件的hashcode值不等,則equals方法得到的結果必定為false; 如果兩個物件的hashcode值相等,則equals方法得到的結果未知。

Object類中有哪些方法,列舉3個以上(可以引導)

Object方法:equals()、toString()、finalize()、hashCode()、getClass()、clone()、wait()、notify()、notifyAll()

String s=new String("xyz")究竟建立String Object分為兩種情況:

  1. 如果String常理池中,已經建立"xyz",則不會繼續建立,此時只建立了一個物件new String("xyz");
  2. 如果String常理池中,沒有建立"xyz",則會建立兩個物件,一個物件的值是"xyz",一個物件new String("xyz")。

什麼是值傳遞和引用傳遞

值傳遞

public class TempTest {

  private void test1(int a) {
    a = 5;
    System.out.println("test1方法中的a=" + a);
  }

  public static void main(String[] args) {
    TempTest t = new TempTest();
    int a = 3;
    t.test1(11);
    System.out.println("main方法中a=" + a);
  }

}
複製程式碼

test1方法中的a=5 main方法中a=3 值傳遞:傳遞的是值的拷貝,傳遞後就互不相關了 引用傳遞:傳遞的是變數所對應的記憶體空間的地址

public class TempTest {
  private void test1(A a) {
    a.age = 20;
    System.out.println("test1方法中a=" + a.age);
  }

  public static void main(String[] args) {
    TempTest t = new TempTest();
    A a = new A();
    a.age = 10;
    t.test1(a);
    System.out.println("main方法中a=" + a.age);
  }
}

class A {
  public int age = 0;
}
複製程式碼

test1方法中a=20 main方法中a=20 傳遞前和傳遞後都指向同一個引用(同一個記憶體空間) 如果不互相影響,方法是在test1方法裡面新new一個例項就可以了

講一下netty

netty通過Reactor模型基於多路複用器接收並處理使用者請求,內部實現了兩個執行緒池,boss執行緒和work執行緒池,其中boss執行緒池的執行緒負責處理請求的accept事件,當接收到accept事件的請求,把對應的socket封裝到一個NioSocketChannel中,並交給work執行緒池,其中work執行緒池負責請求的read和write事件

Nio的原理(同步非阻塞)

服務端和客戶端各自維護一個管理通道的物件,我們稱之為 selector,該對 象能檢測一個或多個通道(channel)上的事件。我們以服務端為例,如果服務 端的 selector 上註冊了讀事件,某時刻客戶端給服務端送了一些資料,阻塞 I/O 這時會呼叫 read()方法阻塞地讀取資料,而 NIO 的服務端會在 selector 中新增 一個讀事件。服務端的處理執行緒會輪詢地訪問 selector,如果訪問 selector 時發 現有感興趣的事件到達,則處理這些事件,如果沒有感興趣的事件到達,則處 理執行緒會一直阻塞直到感興趣的事件到達為止。

這裡寫圖片描述

緩衝區Buffer、通道Channel、選擇器Selector

緩衝區Buffer

  • 緩衝區實際上是一個容器物件,更直接的說,其實就是一個陣列,在NIO庫中,所有資料都是用緩衝區處理的。在讀取資料時,它是直接讀到緩衝區中的; 在寫入資料時,它也是寫入到緩衝區中的;任何時候訪問 NIO 中的資料,都是將它放到緩衝區中。而在面向流I/O系統中,所有資料都是直接寫入或者直接將資料讀取到Stream物件中。

通道Channel

  • 通道是一個物件,通過它可以讀取和寫入資料,當然了所有資料都通過Buffer物件來處理。我們永遠不會將位元組直接寫入通道中,相反是將資料寫入包含一個或者多個位元組的緩衝區。同樣不會直接從通道中讀取位元組,而是將資料從通道讀入緩衝區,再從緩衝區獲取這個位元組。通道與流的不同之處在於 通道是雙向 的。而流只是在一個方向上移動(一個流必須是 InputStream 或者 OutputStream 的子類,比如 InputStream 只能進行讀取操作,OutputStream 只能進行寫操作),而通道是雙向的,可以用於讀、寫或者同時用於讀寫。

選擇器(Selector )

  • NIO 有一個主要的類 Selector,這個類似一個觀察者,只要我們把需要探知 的 socketchannel 告訴 Selector,我們接著做別的事情, 當有事件發生時,他會 通知我們,傳回一組 SelectionKey, 我們讀取這些 Key, 就會獲得我們剛剛註冊 過的 socketchannel, 然後,我們從這個 Channel 中讀取資料,放心,包準能 夠讀到,接著我們可以處理這些資料。
  • Selector 內部原理實際是在做一個 對所註冊的 channel 的輪詢訪問,不斷 地輪詢,一旦輪詢到一個 channel 有所註冊的事情發生,比如資料來了,他就 會站起來報告, 交出一把鑰匙,讓我們 通過這把鑰匙來讀取這個 channel 的內 容。

BIO和NIO的區別

  1. BIO:同步阻塞式IO,伺服器實現模式為一個連線一個執行緒,即客戶端有連線請求時伺服器端就需要啟動一個執行緒進行處理,如果這個連線不做任何事情會造成不必要的執行緒開銷,當然可以通過執行緒池機制改善。
  2. NIO:同步非阻塞式IO,伺服器實現模式為一個請求一個執行緒,即客戶端傳送的連線請求都會註冊到多路複用器上,多路複用器輪詢到連線有I/O請求時才啟動一個執行緒進行處理。

NIO的selector作用

Selector(選擇器)是Java NIO中能夠檢測一到多個NIO通道,並能夠知曉通道是否為諸如讀寫事件做好準備的元件。這樣,一個單獨的執行緒可以管理多個channel,從而管理多個網路連線。

為了實現Selector管理多個SocketChannel,必須將具體的SocketChannel物件註冊到Selector,並宣告需要監聽的事件(這樣Selector才知道需要記錄什麼資料),一共有4種事件:

  1. connect:客戶端連線服務端事件,對應值為SelectionKey.OP_CONNECT(8)
  2. accept:服務端接收客戶端連線事件,對應值為SelectionKey.OP_ACCEPT(16)
  3. read:讀事件,對應值為SelectionKey.OP_READ(1)
  4. write:寫事件,對應值為SelectionKey.OP_WRITE(4)

每次請求到達伺服器,都是從connect開始,connect成功後,服務端開始準備accept,準備就緒,開始讀資料,並處理,最後寫回資料返回。

所以,當SocketChannel有對應的事件發生時,Selector都可以觀察到,並進行相應的處理。

計算機網路

GET 和 POST 區別

(GET) 請注意,查詢字串(名稱/值對)是在 GET 請求的 URL 中傳送的: /test/demo_form.asp?name1=value1&name2=value2

  1. GET 請求可被快取

  2. GET 請求保留在瀏覽器歷史記錄中

  3. GET 請求可被收藏為書籤

  4. GET 請求不應在處理敏感資料時使用

  5. GET 請求有長度限制

  6. GET 請求只應當用於取回資料 POST 方法 (POST) 請注意,查詢字串(名稱/值對)是在 POST 請求的 HTTP 訊息主體中傳送的: POST /test/demo_form.asp HTTP/1.1 Host: w3schools.com name1=value1&name2=value2

  7. POST 請求不會被快取

  8. POST 請求不會保留在瀏覽器歷史記錄中

  9. POST 不能被收藏為書籤

  10. POST 請求對資料長度沒有要求

dns使用的協議

既使用TCP又使用UDP

  • 首先了解一下TCP與UDP傳送位元組的長度限制:
  1. UDP報文的最大長度為512位元組,而TCP則允許報文長度超過512位元組。當DNS查詢超過512位元組時,協議的TC標誌出現刪除標誌,這時則使用TCP傳送。通常傳統的UDP報文一般不會大於512位元組。
  • 區域傳送時使用TCP,主要有一下兩點考慮:
  1. 輔域名伺服器會定時(一般時3小時)向主域名伺服器進行查詢以便了解資料是否有變動。如有變動,則會執行一次區域傳送,進行資料同步。區域傳送將使用TCP而不是UDP,因為資料同步傳送的資料量比一個請求和應答的資料量要多得多。
  2. TCP是一種可靠的連線,保證了資料的準確性。
  • 域名解析時使用UDP協議:
  1. 客戶端向DNS伺服器查詢域名,一般返回的內容都不超過512位元組,用UDP傳輸即可。不用經過TCP三次握手,這樣DNS伺服器負載更低,響應更快。雖然從理論上說,客戶端也可以指定向DNS伺服器查詢的時候使用TCP,但事實上,很多DNS伺服器進行配置的時候,僅支援UDP查詢包。

冪等

一個冪等操作的特點是其任意多次執行所產生的影響均與一次執行的影響相同。冪等函式,或冪等方法,是指可以使用相同引數重複執行,並能獲得相同結果的函式。這些函式不會影響系統狀態,也不用擔心重複執行會對系統造成改變。例如,“getUsername()和setTrue()”函式就是一個冪等函式.

Cookies和session區別

  1. Cookies是一種能夠讓網站伺服器把少量資料儲存到客戶端的硬碟或記憶體,或是從客戶端的硬碟讀取資料的一種技術。Cookies是當你瀏覽某網站時,由Web伺服器置於你硬碟上的一個非常小的文字檔案,它可以記錄你的使用者ID、密碼、瀏覽過的網頁、停留的時間等資訊。 session: 當使用者請求來自應用程式的 Web 頁時,如果該使用者還沒有會話,則 Web 伺服器將自動建立一個 Session 物件。當會話過期或被放棄後,伺服器將終止該會話。 cookie機制:採用的是在客戶端保持狀態的方案,而session機制採用的是在服務端保持狀態的方案。同時我們看到由於伺服器端保持狀態的方案在客戶端也需要儲存一個標識,所以session機制可能需要藉助cookie機制來達到儲存標識的目的。

  2. Session是伺服器用來跟蹤使用者的一種手段,每個Session都有一個唯一標識:session ID。當伺服器建立了Session時,給客戶端傳送的響應報文包含了Set-cookie欄位,其中有一個名為sid的鍵值對,這個鍵值Session ID。客戶端收到後就把Cookie儲存瀏覽器,並且之後傳送的請求報表都包含SessionID。HTTP就是通過Session和Cookie這兩個傳送一起合作來實現跟蹤使用者狀態,Session用於服務端,Cookie用於客戶端

TCP粘包和拆包產生的原因

  1. 應用程式寫入資料的位元組大小大於套接字傳送緩衝區的大小
  2. 進行MSS大小的TCP分段。MSS是最大報文段長度的縮寫。MSS是TCP報文段中的資料欄位的最大長度。資料欄位加上TCP首部才等於整個的TCP報文段。所以MSS並不是TCP報文段的最大長度,而是:MSS=TCP報文段長度-TCP首部長度
  3. 乙太網的payload大於MTU進行IP分片。MTU指:一種通訊協議的某一層上面所能通過的最大資料包大小。如果IP層有一個資料包要傳,而且資料的長度比鏈路層的MTU大,那麼IP層就會進行分片,把資料包分成託乾片,讓每一片都不超過MTU。注意,IP分片可以發生在原始傳送端主機上,也可以發生在中間路由器上。

TCP粘包和拆包的解決策略

  1. 訊息定長。例如100位元組。
  2. 在包尾部增加回車或者空格符等特殊字元進行分割,典型的如FTP協議
  3. 將訊息分為訊息頭和訊息尾。
  4. 其它複雜的協議,如RTMP協議等。

三次握手

第一次握手:建立連線時,客戶端傳送syn包(syn=j)到伺服器,並進入SYN_SEND狀態,等待伺服器確認;

第二次握手:伺服器收到syn包,必須確認客戶的SYN(ack=j+1),同時自己也傳送一個SYN包(syn=k),即SYN+ACK包,此時伺服器進入SYN_RECV狀態;

第三次握手:客戶端收到伺服器的SYN+ACK包,向伺服器傳送確認包ACK(ack=k+1),此包傳送完畢,客戶端和伺服器進入ESTABLISHED狀態,完成三次握手。

完成三次握手,客戶端與伺服器開始傳送資料

四次揮手

  1. 客戶端先傳送FIN,進入FIN_WAIT1狀態
  2. 服務端收到FIN,傳送ACK,進入CLOSE_WAIT狀態,客戶端收到這個ACK,進入FIN_WAIT2狀態
  3. 服務端傳送FIN,進入LAST_ACK狀態
  4. 客戶端收到FIN,傳送ACK,進入TIME_WAIT狀態,服務端收到ACK,進入CLOSE狀態

TIME_WAIT的狀態就是主動斷開的一方(這裡是客戶端),傳送完最後一次ACK之後進入的狀態。並且持續時間還挺長的。客戶端TIME_WAIT持續2倍MSL時長,在linux體系中大概是60s,轉換成CLOSE狀態

TIME_WAIT

TIME_WAIT 是主動關閉連結時形成的,等待2MSL時間,約4分鐘。主要是防止最後一個ACK丟失。 由於TIME_WAIT 的時間會非常長,因此server端應儘量減少主動關閉連線

CLOSE_WAIT

CLOSE_WAIT是被動關閉連線是形成的。根據TCP狀態機,伺服器端收到客戶端傳送的FIN,則按照TCP實現傳送ACK,因此進入CLOSE_WAIT狀態。但如果伺服器端不執行close(),就不能由CLOSE_WAIT遷移到LAST_ACK,則系統中會存在很多CLOSE_WAIT狀態的連線。此時,可能是系統忙於處理讀、寫操作,而未將已收到FIN的連線,進行close。此時,recv/read已收到FIN的連線socket,會返回0。

為什麼需要 TIME_WAIT 狀態?

假設最終的ACK丟失,server將重發FIN,client必須維護TCP狀態資訊以便可以重發最終的ACK,否則會傳送RST,結果server認為發生錯誤。TCP實現必須可靠地終止連線的兩個方向(全雙工關閉),client必須進入 TIME_WAIT 狀態,因為client可能面 臨重發最終ACK的情形。

為什麼 TIME_WAIT 狀態需要保持 2MSL 這麼長的時間?

如果 TIME_WAIT 狀態保持時間不足夠長(比如小於2MSL),第一個連線就正常終止了。第二個擁有相同相關五元組的連線出現,而第一個連線的重複報文到達,干擾了第二個連線。TCP實現必須防止某個連線的重複報文在連線終止後出現,所以讓TIME_WAIT狀態保持時間足夠長(2MSL),連線相應方向上的TCP報文要麼完全響應完畢,要麼被 丟棄。建立第二個連線的時候,不會混淆。

TIME_WAIT 和CLOSE_WAIT狀態socket過多

如果伺服器出了異常,百分之八九十都是下面兩種情況:

1.伺服器保持了大量TIME_WAIT狀態

2.伺服器保持了大量CLOSE_WAIT狀態,簡單來說CLOSE_WAIT數目過大是由於被動關閉連線處理不當導致的。

一次完整的HTTP請求過程

域名解析 --> 發起TCP的3次握手 --> 建立TCP連線後發起http請求 --> 伺服器響應http請求,瀏覽器得到html程式碼 --> 瀏覽器解析html程式碼,並請求html程式碼中的資源(如js、css、圖片等) --> 瀏覽器對頁面進行渲染呈現給使用者

講一下長連線

  • 一、基於http協議的長連線
  1. 在HTTP1.0和HTTP1.1協議中都有對長連線的支援。其中HTTP1.0需要在request中增加”Connection: keep-alive“ header才能夠支援,而HTTP1.1預設支援.
  • http1.0請求與服務端的互動過程:
  1. 客戶端發出帶有包含一個header:”Connection: keep-alive“的請求
  2. 服務端接收到這個請求後,根據http1.0和”Connection: keep-alive“判斷出這是一個長連線,就會在response的header中也增加”Connection: keep-alive“,同是不會關閉已建立的tcp連線.
  3. 客戶端收到服務端的response後,發現其中包含”Connection: keep-alive“,就認為是一個長連線,不關閉這個連線。並用該連線再傳送request.轉到a)
  • 二、發心跳包。每隔幾秒就發一個資料包過去

TCP如何保證可靠傳輸?

  1. 三次握手。
  2. 將資料截斷為合理的長度。應用資料被分割成 TCP 認為最適合傳送的資料塊(按位元組編號,合理分片)
  3. 超時重發。當 TCP 發出一個段後,它啟動一個定時器,如果不 能及時收到一個確認就重發
  4. 對於收到的請求,給出確認響應
  5. 校驗出包有錯,丟棄報文段,不給出響應
  6. 對失序資料進行重新排序,然後才交給應用層
  7. 對於重複資料 , 能夠丟棄重複資料
  8. 流量控制。TCP 連線的每一方都有固定大小的緩衝空間。TCP 的接收端 只允許另一端傳送接收端緩衝區所能接納的資料。這將防止較快主機致使較慢主機的緩衝 區溢位。
  9. 擁塞控制。當網路擁塞時,減少資料的傳送。

詳細介紹http

HTTP協議是Hyper Text Transfer Protocol(超文字傳輸協議)的縮寫,是用於從全球資訊網(WWW:World Wide Web )伺服器傳輸超文字到本地瀏覽器的傳送協議。

特點

  1. 簡單快速:客戶向伺服器請求服務時,只需傳送請求方法和路徑。請求方法常用的有GET、HEAD、POST。每種方法規定了客戶與伺服器聯絡的型別不同。由於HTTP協議簡單,使得HTTP伺服器的程式規模小,因而通訊速度很快。

  2. 靈活:HTTP允許傳輸任意型別的資料物件。正在傳輸的型別由Content-Type加以標記。

  3. 無連線:無連線的含義是限制每次連線只處理一個請求。伺服器處理完客戶的請求,並收到客戶的應答後,即斷開連線。採用這種方式可以節省傳輸時間。

  4. 無狀態:HTTP協議是無狀態協議。無狀態是指協議對於事務處理沒有記憶能力。缺少狀態意味著如果後續處理需要前面的資訊,則它必須重傳,這樣可能導致每次連線傳送的資料量增大。另一方面,在伺服器不需要先前資訊時它的應答就較快。

  5. 支援B/S及C/S模式。

請求訊息Request

  1. 請求行,用來說明請求型別,要訪問的資源以及所使用的HTTP版本.
  2. 請求頭部,緊接著請求行(即第一行)之後的部分,用來說明伺服器要使用的附加資訊 從第二行起為請求頭部,HOST將指出請求的目的地.User-Agent,伺服器端和客戶端指令碼都能訪問它,它是瀏覽器型別檢測邏輯的重要基礎.該資訊由你的瀏覽器來定義,並且在每個請求中自動傳送等等
  3. 空行,請求頭部後面的空行是必須的
  4. 請求資料也叫主體,可以新增任意的其他資料。

響應訊息Response

  1. 狀態行,由HTTP協議版本號, 狀態碼, 狀態訊息 三部分組成。
  2. 訊息報頭,用來說明客戶端要使用的一些附加資訊
  3. 空行,訊息報頭後面的空行是必須的
  4. 響應正文,伺服器返回給客戶端的文字資訊。

狀態碼

  • 200 OK //客戶端請求成功
  • 301 Moved Permanently //永久重定向,使用域名跳轉
  • 302 Found // 臨時重定向,未登陸的使用者訪問使用者中心重定向到登入頁面
  • 400 Bad Request //客戶端請求有語法錯誤,不能被伺服器所理解
  • 401 Unauthorized //請求未經授權,這個狀態程式碼必須和WWW-Authenticate報頭域一起使用
  • 403 Forbidden //伺服器收到請求,但是拒絕提供服務
  • 404 Not Found //請求資源不存在,eg:輸入了錯誤的URL
  • 500 Internal Server Error //伺服器發生不可預期的錯誤
  • 503 Server Unavailable //伺服器當前不能處理客戶端的請求,一段時間後可能恢復正常

http的方法

  1. get:客戶端向服務端發起請求,獲得資源。請求獲得URL處所在的資源。
  2. post:向服務端提交新的請求欄位。請求URL的資源後新增新的資料。
  3. head:請求獲取URL資源的響應報告,即獲得URL資源的頭部
  4. patch:請求區域性修改URL所在資源的資料項
  5. put:請求修改URL所在資源的資料元素。
  6. delete:請求刪除url資源的資料

URI和URL的區別

URI,是uniform resource identifier,統一資源識別符號,用來唯一的標識一個資源。 Web上可用的每種資源如HTML文件、影象、視訊片段、程式等都是一個來URI來定位的

URI一般由三部組成:

  1. 訪問資源的命名機制
  2. 存放資源的主機名
  3. 資源自身的名稱,由路徑表示,著重強調於資源。

URL是uniform resource locator,統一資源定位器,它是一種具體的URI,即URL可以用來標識一個資源,而且還指明瞭如何locate這個資源。 URL是Internet上用來描述資訊資源的字串,主要用在各種WWW客戶程式和伺服器程式上,特別是著名的Mosaic。 採用URL可以用一種統一的格式來描述各種資訊資源,包括檔案、伺服器的地址和目錄等。

URL一般由三部組成:

  1. 協議(或稱為服務方式)
  2. 存有該資源的主機IP地址(有時也包括埠號)
  3. 主機資源的具體地址。如目錄和檔名等

HTTPS和HTTP的區別

  1. https協議需要到CA申請證照,一般免費證照很少,需要交費。
  2. http是超文字傳輸協議,資訊是明文傳輸;https 則是具有安全性的ssl加密傳輸協 議。
  3. http和https使用的是完全不同的連線方式,用的埠也不一樣,前者是80,後者是443。
  4. http的連線很簡單,是無狀態的;HTTPS協議是由SSL+HTTP協議構建的可進行加密傳輸、身份認證的網路協議,比http協議安全。
  5. http預設使用80埠,https預設使用443埠

https是如何保證資料傳輸的安全

https實際就是在TCP層與http層之間加入了SSL/TLS來為上層的安全保駕護航,主要用到對稱加密、非對稱加密、證照,等技術進行客戶端與伺服器的資料加密傳輸,最終達到保證整個通訊的安全性。

  • SSL/TLS協議作用:
  1. 認證使用者和伺服器,確保資料傳送到正確的客戶機和伺服器;
  2. 加密資料以防止資料中途被竊取;
  3. 維護資料的完整性,確保資料在傳輸過程中不被改變。
    這裡寫圖片描述

資料結構與演算法

動態規劃的思想

動態規劃過程是:每次決策依賴於當前狀態,又隨即引起狀態的轉移。一個決策序列就是在變化的狀態中產生出來的,所以,這種多階段最優化決策解決問題的過程就稱為動態規劃。

快速排序的思想

在陣列中找到一個基準數(pivot) 分割槽,將陣列中比基準數大的放到它的右邊,比基準數小的放到它的左邊 繼續對左右區間重複第二步,直到各個區間只有一個數,這時候,陣列也就有序了。

快速排序演算法是不穩定的演算法

27 23 27 3 以第一個27作為pivot中心點,則27與後面那個3交換,形成 3 23 27 27,排序經過一次結束,但最後那個27在排序之初先於初始位置3那個27,所以不穩定。

堆排序的思想

利用大頂堆(小頂堆)堆頂記錄的是最大關鍵字(最小關鍵字)這一特性,使得每次從無序中選擇最大記錄(最小記錄)變得簡單。 其基本思想為(大頂堆): 1)將初始待排序關鍵字序列(R1,R2....Rn)構建成大頂堆,此堆為初始的無須區; 2)將堆頂元素R[1]與最後一個元素R[n]交換,此時得到新的無序區(R1,R2,......Rn-1)和新的有序區(Rn),且滿足R[1,2...n-1]<=R[n]; 3)由於交換後新的堆頂R[1]可能違反堆的性質,因此需要對當前無序區(R1,R2,......Rn-1)調整為新堆,然後再次將R[1]與無序區最後一個元素交換,得到新的無序區(R1,R2....Rn-2)和新的有序區(Rn-1,Rn)。不斷重複此過程直到有序區的元素個數為n-1,則整個排序過程完成。

字典樹

字典樹主要有如下三點性質:

  1. 根節點不包含字元,除根節點意外每個節點只包含一個字元。
  2. 從根節點到某一個節點,路徑上經過的字元連線起來,為該節點對應的字串。
  3. 每個節點的所有子節點包含的字串不相同。

連結串列反轉

  1. 首先讓頭節點與第一個元素節點斷開,但是要注意在斷開之前需要用p指標指向第一個元素節點來儲存第一個元素節點的位置,然後再斷開。在這裡有一個指標q指向一個指標域為空的節點,這個節點用來做為連結串列反轉後的最後一個節點。
  2. 讓第二個元素節點的指標從指向第三個元素節點變為指向第一個元素節點,以此類推,直至指標p指向原連結串列最後一個元素。
  3. p指標指向NULL時,讓原頭節點的指標域指向原來最後一個元素節點。此時連結串列倒置已完成。
linkList reverse(linkList head){
  linkList p,q,pr;
  p = head->next;
  q = NULL;
  head->next = NULL;
  while(p){
    pr = p->next;
    p->next = q;
    q = p;
    p = pr;
  }
  head->next = q;
  return head;
}
複製程式碼

作業系統/Linux

程式有哪些狀態

一般來說,程式有三個狀態,即就緒狀態,執行狀態,阻塞狀態。

  1. 執行態:程式佔用CPU,並在CPU上執行
  2. 就緒態:程式已經具備執行條件,但是CPU還沒有分配過來
  3. 阻塞態:程式因等待某件事發生而暫時不能執行
    這裡寫圖片描述

當然理論上上述三種狀態之間轉換分為六種情況;

  1. 執行——>就緒:1,主要是程式佔用CPU的時間過長,而系統分配給該程式佔用CPU的時間是有限的;2,在採用搶先式優先順序排程演算法的系統中,當有更高優先順序的程式要執行時,該程式就被迫讓出CPU,該程式便由執行狀態轉變為就緒狀態。
  2. 就緒——>執行:執行的程式的時間片用完,排程就轉到就緒佇列中選擇合適的程式分配CPU
  3. 執行——>阻塞:正在執行的程式因發生某等待事件而無法執行,則程式由執行狀態變為阻塞狀態,如發生了I/O請求
  4. 阻塞——>就緒:程式所等待的事件已經發生,就進入就緒佇列

以下兩種狀態是不可能發生的:

  1. 阻塞——>執行:即使給阻塞程式分配CPU,也無法執行,作業系統在進行排程時不會從阻塞佇列進行挑選,而是從就緒佇列中選取

  2. 就緒——>阻塞:就緒態根本就沒有執行,談不上進入阻塞態。

程式間通訊方式

  1. 管道pipe:管道是一種半雙工的通訊方式,資料只能單向流動,而且只能在具有親緣關係的程式間使用。程式的親緣關係通常是指父子程式關係。
  2. 命名管道FIFO:有名管道也是半雙工的通訊方式,但是它允許無親緣關係程式間的通訊。
  3. 訊息佇列MessageQueue:訊息佇列是由訊息的連結串列,存放在核心中並由訊息佇列識別符號標識。訊息佇列克服了訊號傳遞資訊少、管道只能承載無格式位元組流以及緩衝區大小受限等缺點。
  4. 共享儲存SharedMemory:共享記憶體就是對映一段能被其他程式所訪問的記憶體,這段共享記憶體由一個程式建立,但多個程式都可以訪問。共享記憶體是最快的 IPC 方式,它是針對其他程式間通訊方式執行效率低而專門設計的。它往往與其他通訊機制,如訊號兩,配合使用,來實現程式間的同步和通訊。
  5. 訊號量Semaphore:訊號量是一個計數器,可以用來控制多個程式對共享資源的訪問。它常作為一種鎖機制,防止某程式正在訪問共享資源時,其他程式也訪問該資源。因此,主要作為程式間以及同一程式內不同執行緒之間的同步手段。
  6. 套接字Socket:套解口也是一種程式間通訊機制,與其他通訊機制不同的是,它可用於不同及其間的程式通訊。
  7. 訊號 ( sinal ) : 訊號是一種比較複雜的通訊方式,用於通知接收程式某個事件已經發生。

Linux中軟連結和硬連結的區別

ln -s source dist # 建立軟連線 ln source dist # 建立硬連線 建立硬連結時,連結檔案和被連結檔案必須位於同一個檔案系統中,並且不能建立指向目錄的硬連結

  1. 硬連線就像一個檔案有多個檔名,
  2. 軟連線就是產生一個新檔案(這個檔案內容,實際上就是記當要連結原檔案路徑的資訊),這個檔案指向另一個檔案的位置

I/O多路複用

單個執行緒,通過記錄跟蹤每個I/O流(sock)的狀態,來同時管理多個I/O流 。儘量多的提高伺服器的吞吐能力

select, poll, epoll 都是I/O多路複用的具體的實現

設計模式

設計模式主要分三個型別:建立型、結構型和行為型。

  1. 建立型有:
  • Singleton,單例模式:保證一個類只有一個例項,並提供一個訪問它的全域性訪問點

  • Abstract Factory,抽象工廠:提供一個建立一系列相關或相互依賴物件的介面,而無須指定它們的具體類。

  • Factory Method,工廠方法:定義一個用於建立物件的介面,讓子類決定例項化哪一個類,Factory Method使一個類的例項化延遲到了子類。

  • Builder,建造模式:將一個複雜物件的構建與他的表示相分離,使得同樣的構建過程可以建立不同的表示。

  • Prototype,原型模式:用原型例項指定建立物件的種類,並且通過拷貝這些原型來建立新的物件。

  1. 行為型有:
  • Iterator,迭代器模式:提供一個方法順序訪問一個聚合物件的各個元素,而又不需要暴露該物件的內部表示。
  • Observer,觀察者模式:定義物件間一對多的依賴關係,當一個物件的狀態發生改變時,所有依賴於它的物件都得到通知自動更新。
  • Template Method,模板方法:定義一個操作中的演算法的骨架,而將一些步驟延遲到子類中,TemplateMethod使得子類可以不改變一個演算法的結構即可以重定義該演算法得某些特定步驟。
  • Command,命令模式:將一個請求封裝為一個物件,從而使你可以用不同的請求對客戶進行引數化,對請求排隊和記錄請求日誌,以及支援可撤銷的操作。
  • State,狀態模式:允許物件在其內部狀態改變時改變他的行為。物件看起來似乎改變了他的類。
  • Strategy,策略模式:定義一系列的演算法,把他們一個個封裝起來,並使他們可以互相替換,本模式使得演算法可以獨立於使用它們的客戶。
  • China of Responsibility,職責鏈模式:使多個物件都有機會處理請求,從而避免請求的送發者和接收者之間的耦合關係
  • Mediator,中介者模式:用一箇中介物件封裝一些列的物件互動。
  • Visitor,訪問者模式:表示一個作用於某物件結構中的各元素的操作,它使你可以在不改變各元素類的前提下定義作用於這個元素的新操作。
  • Interpreter,直譯器模式:給定一個語言,定義他的文法的一個表示,並定義一個直譯器,這個直譯器使用該表示來解釋語言中的句子
  • Memento,備忘錄模式:在不破壞物件的前提下,捕獲一個物件的內部狀態,並在該物件之外儲存這個狀態。
  1. 結構型有:
  • Composite,組合模式:將物件組合成樹形結構以表示部分整體的關係,Composite使得使用者對單個物件和組合物件的使用具有一致性。
  • Facade,外觀模式:為子系統中的一組介面提供一致的介面,fa?ade提供了一高層介面,這個介面使得子系統更容易使用。
  • Proxy,代理模式:為其他物件提供一種代理以控制對這個物件的訪問
  • Adapter,介面卡模式:將一類的介面轉換成客戶希望的另外一個介面,Adapter模式使得原本由於介面不相容而不能一起工作那些類可以一起工作。
  • Decrator,裝飾模式:動態地給一個物件增加一些額外的職責,就增加的功能來說,Decorator模式相比生成子類更加靈活。
  • Bridge,橋模式:將抽象部分與它的實現部分相分離,使他們可以獨立的變化。
  • Flyweight,享元模式

動態代理和靜態代理有什麼區別

靜態代理

這種代理方式需要代理物件和目標物件實現一樣的介面。在程式執行前,代理類的.class檔案就已經存在了。

優點:

  1. 可以在不修改目標物件的前提下擴充套件目標物件的功能。

缺點:

  1. 冗餘。由於代理物件要實現與目標物件一致的介面,會產生過多的代理類。
  2. 不易維護。一旦介面增加方法,目標物件與代理物件都要進行修改。

動態代理

動態代理利用了JDK API,運用反射機制動態地在記憶體中構建代理物件,從而實現對目標物件的代理功能。動態代理又被稱為JDK代理或介面代理。

動態代理是實現JDK裡的InvocationHandler介面的invoke方法,但注意的是代理的是介面,也就是你的業務類必須要實現介面,通過Proxy裡的newProxyInstance得到代理物件

優點:

  1. 動態代理物件不需要實現介面,但是要求目標物件必須實現介面,否則不能使用動態代理。

靜態代理與動態代理的區別主要

  1. 靜態代理在編譯時就已經實現,編譯完成後代理類是一個實際的class檔案
  2. 動態代理是在執行時動態生成的,即編譯完成後沒有實際的class檔案,而是在執行時動態生成類位元組碼,並載入到JVM中
  3. 靜態代理通常只代理一個類,動態代理是代理一個介面下的多個實現類。
  4. 靜態代理事先知道要代理的是什麼,而動態代理不知道要代理什麼東西,只有在執行時才知道。

JDK中的動態代理和CGLIB

動態代理

  1. JDK中的動態代理: 通過反射類Proxy以及InvocationHandler回撥介面實現的,

  2. 動態代理缺點: JDK中所要進行動態代理的類必須要實現一個介面,也就是說只能對該類所實現介面中定義的方法進行代理,這在實際程式設計中具有一定的侷限性,而且使用反射的效率也並不是很高。

CGLIB

  1. CGLIB原理:動態生成一個要代理類的子類,子類重寫要代理的類的所有不是final的方法。在子類中採用方法攔截的技術攔截所有父類方法的呼叫,順勢織入橫切邏輯。它比使用java反射的JDK動態代理要快。

  2. CGLIB底層:使用位元組碼處理框架ASM,來轉換位元組碼並生成新的類。不鼓勵直接使用ASM,因為它要求你必須對JVM內部結構包括class檔案的格式和指令集都很熟悉。

  3. CGLIB優點:它為沒有實現介面的類提供代理,為JDK的動態代理提供了很好的補充。通常可以使用Java的動態代理建立代理,但當要代理的類沒有實現介面或者為了更好的效能,CGLIB是一個好的選擇。

  4. CGLIB缺點:對於final方法,無法進行代理

場景題和設計題

場景題:設計判斷論文抄襲的系統

  1. 一類是基於字串比較的方法;另一類是基於詞頻統計的方法。
  2. 基於字串比較的方法也稱為數字指紋法,這類方法通過某種選取策略在文件中取一些字串作為“指紋”,把指紋對映到Hash 表中,最後統計Hash
  3. 表中相同的指紋數目或者比率,作為文字相似度依據。
  4. 基於詞頻統計的方法也稱為基於語義的方法。詞頻統計法源於資訊檢索技術中的向量空間模型,該類方法首先都要統計每篇文件中各個單詞的出現次數,然後根據單詞頻度構成文件特徵向量,最後採用點積、餘弦或者類似方式度量兩篇文件的特徵向量,以此作為文件相似度的依據。

設計一個即時聊天的系統

  1. 使用者通過客戶端進入系統,向伺服器發出訊息,請求登陸。
  2. 伺服器收到請求後,向客戶端返回應答訊息,表示同意接受該使用者加入,並順帶將自己服務執行緒所在的監聽埠號告訴使用者。
  3. 客戶端按照伺服器應答中給出的埠號與伺服器建立穩定的連線。
  4. 伺服器通過該連線將當前線上使用者的列表資訊傳給新加入的客戶端。
  5. 客戶端獲得了線上使用者列表,就可以獨立自主地與線上的其他使用者通訊了。
  6. 當使用者退出系統時要及時地通知伺服器。

分散式系統事務一致性解決方案

分散式系統事務一致性解決方案 MQ(事務訊息)

舉個例子,Bob向Smith轉賬,那我們到底是先傳送訊息,還是先執行扣款操作?

好像都可能會出問題。如果先發訊息,扣款操作失敗,那麼Smith的賬戶裡面會多出一筆錢。反過來,如果先執行扣款操作,後傳送訊息,那有可能扣款成功了但是訊息沒發出去,Smith收不到錢。除了上面介紹的通過異常捕獲和回滾的方式外,還有沒有其他的思路呢?

下面以阿里巴巴的RocketMQ中介軟體為例,分析下其設計和實現思路。

RocketMQ第一階段傳送Prepared訊息時,會拿到訊息的地址,第二階段執行本地事物,第三階段通過第一階段拿到的地址去訪問訊息,並修改狀態。細心的讀者可能又發現問題了,如果確認訊息傳送失敗了怎麼辦?RocketMQ會定期掃描訊息叢集中的事物訊息,這時候發現了Prepared訊息,它會向訊息傳送者確認,Bob的錢到底是減了還是沒減呢?如果減了是回滾還是繼續傳送確認訊息呢?RocketMQ會根據傳送端設定的策略來決定是回滾還是繼續傳送確認訊息。這樣就保證了訊息傳送與本地事務同時成功或同時失敗。如下圖:

這裡寫圖片描述

設計高併發的系統?

  1. HTML 頁面靜態化 訪問頻率較高但內容變動較小,使用網站 HTML 靜態化方案來優化訪問速度。將社群 內的帖子、文章進行實時的靜態化,有更新的時候再重新靜態化也是大量使用的策略。 優勢: 一、減輕伺服器負擔。 二、加快頁面開啟速度, 靜態頁面無需 訪問 資料庫,開啟速度較動態頁面有明顯提高; 三、很多搜尋引擎都會優先收錄靜態頁面,不僅被收錄的快,還收錄的全,容易被搜 索引擎找到; 四、HTML 靜態頁面不會受程式相關漏洞的影響,減少攻擊 ,提高安全性。
  2. 圖片伺服器和應用伺服器相分離 現在很多的網站上都會用到大量的圖片,而圖片是網頁傳輸中佔主要的資料量,也是影 響網站效能的主要因素。因此很多網站都會將圖片儲存從網站中分離出來,另外架構一個 或多個伺服器來儲存圖片,將圖片放到一個虛擬目錄中,而網頁上的圖片都用一個 URL 地 址來指向這些伺服器上的圖片的地址,這樣的話網站的效能就明顯提高了。 優勢: 一、分擔 Web 伺服器的 I/O 負載-將耗費資源的圖片服務分離出來,提高伺服器的效能 和穩定性。 二、 能夠專門對圖片伺服器進行優化-為圖片服務設定有針對性的 快取方案,減少頻寬 成本,提高訪問速度。 三、 提高網站的可擴充套件性-通過增加圖片伺服器,提高圖片吞吐能力。
  3. 資料庫 見“資料庫部分的---如果有一個特別大的訪問量到資料庫上,怎麼做優化?”。
  4. 快取 儘量使用快取,包括使用者快取,資訊快取等,多花點記憶體來做快取,可以大量減少與 資料庫的互動,提高效能。 假如我們能減少資料庫頻繁的訪問,那對系統肯定大大有利的。比如一個電子商務系 統的商品搜尋,如果某個關鍵字的商品經常被搜,那就可以考慮這部分商品列表存放到緩 存(記憶體中去),這樣不用每次訪問資料庫,效能大大增加。
  5. 映象 映象是冗餘的一種型別,一個磁碟上的資料在另一個磁碟上存在一個完全相同的副本 即為映象。
  6. 負載均衡 在網站高併發訪問的場景下,使用負載均衡技術(負載均衡伺服器)為一個應用構建 一個由多臺伺服器組成的伺服器叢集,將併發訪問請求分發到多臺伺服器上處理,避免單 一伺服器因負載壓力過大而響應緩慢,使使用者請求具有更好的響應延遲特性。
  7. 併發控制 加鎖,如樂觀鎖和悲觀鎖。
  8. 訊息佇列 通過 mq 一個一個排隊方式,跟 12306 一樣。

設計高負載的系統

  1. 應用無狀態
  2. 有效使用快取
  3. 應用拆分
  4. 資料庫拆分
  5. 非同步通訊
  6. 非結構化資料儲存 ( TFS,NOSQL)
  7. 監控、預警系統
  8. 配置統一管理

訂票系統,某車次只有一張火車票,假定有 1w 個人同時開啟 12306 網站來訂票,如何解決併發問題?(可擴充套件到任何高併發網站要考慮的 併發讀寫問題

使用樂觀鎖,樂觀鎖意思是不鎖定表的情況下,利用業務的控制來解決併發問題,這樣既保證資料的併發 可讀性 ,又保證儲存資料的 排他性,保證效能的同時解決了併發帶來 的髒資料問題。hibernate 中實現樂觀鎖。(樂觀鎖,使用版本標識來確定讀到的資料與提交時的資料是否一致。提交後修改版本標識,不一致時可以採取丟棄和再次嘗試的策略。)

分散式與叢集的區別是什麼

分散式:一個業務分拆多個子業務,部署在不同的伺服器上 叢集:同一個業務,部署在多個伺服器上

實時展現熱門文章,比如近8小時點選量最大的文章前100名

  1. 資料接收
  • 客戶端會為了減輕伺服器的壓力而選擇延遲合併點選請求進行批量傳送
  • 伺服器肯定會有多臺機器多程式部署來接受點選請求,接收到的請求在進行引數解析後,被髮送到儲存單元。為了減輕儲存的壓力,每個程式可能會使用小視窗聚合資料,每隔一小段時間將視窗內的資料聚合起來一起發給儲存單元。
  1. 資料儲存
  • 使用kafka存,ZeroCopy機制併發量很高,資料持久化在磁碟裡成本低。不過kafka的資料一般是有過期時間的,如果想完全記住使用者的點選以便做長期的資料分析,需要要使用hdfs了
  1. 分散式TopN演算法
  • 使用者太多,使用者表按使用者ID雜湊分成了1024張子表。使用者表裡有一個欄位score,表示這個使用者的積分數。現在我們要計算前100名積分最多的使用者以及積分數,該怎麼查詢?
  • 如果是單個表,一個SQL也就搞定了,
  • 如果是多個子表,你得在每個子表上都進行一次TopN查詢,然後聚合結果再做一次TopN查詢。子表查詢可以多執行緒並行,提高聚合效率。
  1. 滑動視窗
  • 8小時的滑動視窗,意味著新的資料來源源不斷的進來,舊的資料時時刻刻在淘汰,在業務可以差幾分鐘。
  • 我們對時間片進行了切分,一分鐘一個槽來進行計數,過期了8小時,移掉第一個,計算topn的帖子,維護視窗,移除過期的槽,然後統計topn,30s~60s呼叫一次
  1. 定時任務
  • 每個子節點都會有一個定時任務去負責維持統計視窗,過期失效的統計資料,計算區域性的topn熱帖。
  • 現在每個子節點都有了各自的區域性topn熱帖,那麼還需要一個主節點去彙總這些區域性熱點,然後計算去全域性熱帖。
  1. 點選去重
  • 首先要從客戶端下手,客戶端本身可以過濾一部分無效點選。同一篇文章在太短的時間內被當前使用者反覆點選,這個模式還是很好發現的。如果間隔時間比較長,那就是讀者的回味點選,屬於文章的正向反饋,應該記錄下來
  • 伺服器還需要防止使用者的防刷行為。如果缺失防刷控制,可以通過這種漏洞來使得自己的文章非法獲得大量點選,進入熱門文章列表,打上熱門標籤,被海量的使用者看到,就會獲得較大的經濟效益,即使這篇文章內容本身吸引力並不足夠。

如何解決電商網站超賣現象

超賣是什麼

  • 因為資料庫底層的寫操作和讀操作可以同時進行,雖然寫操作預設帶有隱式鎖(即對同一資料不能同時進行寫操作)但是讀操作預設是不帶鎖的,所以當使用者1去修改庫存的時候,使用者2依然可以讀到庫存為1,導致兩個使用者同時減一次庫存,所以出現了超賣現象。

解決方案

  1. 使用redis預減庫存
  2. 當庫存大於0,才能更新庫存update sk_goods_seckill set stock_count = stock_count - 1 where goods_id = #{goodsId} and stock_count > 0
  3. 新增唯一索引,UNIQUE KEY u_uid_gid (user_id,goods_id) USING BTREE,防止同一使用者同一商品下兩次訂單
  4. 樂觀鎖,就是在資料庫設計一個版本號的欄位,每次修改都使其+1,這樣在提交時比對提交前的版本號就知道是不是併發提交了,但是有個缺點就是隻能是應用中控制,如果有跨應用修改同一條資料樂觀鎖就沒辦法了,這個時候可以考慮悲觀鎖。
  5. 悲觀鎖,就是直接在資料庫層面將資料鎖死,類似於oralce中使用select xxxxx from xxxx where xx=xx for update,這樣其他執行緒將無法提交資料。
  6. 使用訊息佇列非同步下單

mq非同步呼叫失敗,如何保證資料一致性?

  1. 按你的使用場景,推送資料必須得在資料建立事務成功之後執行,這裡必須有個先後。你可以將推送這個操作非同步執行,訊息佇列有一搬有ack機制,確保訊息沒丟失。這時候監聽訊息佇列的程式會執行推送,如果推送成功做標記。如果推送失敗也標記記錄時間,也可以推到另一個訊息佇列約定多少分鐘重試。實在不行就徹底標記失敗,或者回滾之前建立的資料。這個才是最終一致性。
  2. 如果是並行的操作,就得使用訊息佇列的confirm機制了。

分散式鎖的幾種實現方式

  1. 基於資料庫表
  • 要實現分散式鎖,最簡單的方式可能就是直接建立一張鎖表,然後通過操作該表中的資料來實現了。

  • 我們對method_name(方法名)做了唯一性約束,這裡如果有多個請求同時提交到資料庫的話,資料庫會保證只有一個操作可以成功,那麼我們就可以認為操作成功的那個執行緒獲得了該方法的鎖,可以執行方法體內容。當方法執行完畢之後,想要釋放鎖的話就刪除該記錄

  1. 基於資料庫排他鎖
  • 可以通過資料庫的排他鎖來實現分散式鎖
  • 在查詢語句後面增加for update,資料庫會在查詢過程中給資料庫表增加排他鎖(這裡再多提一句,InnoDB引擎在加鎖的時候,只有通過索引進行檢索的時候才會使用行級鎖,否則會使用表級鎖。這裡我們希望使用行級鎖,就要給method_name新增索引
  • 值得注意的是,這個索引一定要建立成唯一索引,否則會出現多個過載方法之間無法同時被訪問的問題。過載方法的話建議把引數型別也加上。)。當某條記錄被加上排他鎖之後,其他執行緒無法再在該行記錄上增加排他鎖。
  1. 基於快取
  • 比如Tair的put方法,redis的setnx方法等。並且,這些快取服務也都提供了對資料的過期自動刪除的支援,可以直接設定超時時間來控制鎖的釋放。
  1. 基於Zookeeper
  • 每個客戶端對某個方法加鎖時,在zookeeper上的與該方法對應的指定節點的目錄下,生成一個唯一的瞬時有序節點。 判斷是否獲取鎖的方式很簡單,只需要判斷有序節點中序號最小的一個。 當釋放鎖的時候,只需將這個瞬時節點刪除即可。同時,其可以避免服務當機導致的鎖無法釋放,而產生的死鎖問題。

訊息佇列

四種MQ區別

這裡寫圖片描述

如何保證訊息佇列是高可用的

  1. 叢集,以rcoketMQ為例,有多master 模式、多master多slave非同步複製模式、多 master多slave同步雙寫模式。

這裡寫圖片描述
Producer 與 NameServer叢集中的其中一個節點(隨機選擇)建立長連線,定期從 NameServer 獲取 Topic 路由資訊,並向提供 Topic 服務的 Broker Master 建立長連線,且定時向 Broker 傳送心跳。Producer 只能將訊息傳送到 Broker master,但是 Consumer 則不一樣,它同時和提供 Topic 服務的 Master 和 Slave建立長連線,既可以從 Broker Master 訂閱訊息,也可以從 Broker Slave 訂閱訊息。

如何保證訊息不被重複消費

原因:

  • 消費者在消費後,會傳送一個確認資訊給訊息佇列,訊息佇列收到後會將該訊息從訊息佇列中刪除,例如RabbitMQ是傳送一個ACK確認訊息,RocketMQ是返回一個CONSUME_SUCCESS成功標誌,kafka每一個訊息都有一個offset,kafka消費過訊息後,需要提交offset,讓訊息佇列知道自己已經消費過了。因為網路傳輸等等故障,確認資訊沒有傳送到訊息佇列,導致訊息佇列不知道自己已經消費過該訊息了,再次將該訊息分發給其他的消費者。

解決方法:

  1. 訊息可以使用唯一id標識
  2. 如果拿到這個訊息做資料庫的insert操作。給這個訊息做一個唯一主鍵,那麼就算出現重複消費的情況,就會導致主鍵衝突,避免資料庫出現髒資料。
  3. 如果拿到這個訊息做redis的set的操作,無論set幾次結果都是一樣的,set操作是算冪等操作。
  4. 使用第三方介質做消費記錄。以redis為例,給訊息分配一個全域性id,只要消費過該訊息,將< id,message>以K-V形式寫入redis。那消費者開始消費前,先去redis中查詢有沒消費記錄即可。

流行框架

SpringMVC的工作原理

這裡寫圖片描述
SpringMVC流程

  1. 使用者傳送請求至前端控制器DispatcherServlet。
  2. DispatcherServlet收到請求呼叫HandlerMapping處理器對映器。
  3. 處理器對映器找到具體的處理器(可以根據xml配置、註解進行查詢),生成處理器物件及處理器攔截器(如果有則生成)一併返回給DispatcherServlet。
  4. DispatcherServlet呼叫HandlerAdapter處理器介面卡。
  5. HandlerAdapter經過適配呼叫具體的處理器(Controller,也叫後端控制器)。
  6. Controller執行完成返回ModelAndView。
  7. HandlerAdapter將controller執行結果ModelAndView返回給DispatcherServlet。
  8. DispatcherServlet將ModelAndView傳給ViewReslover檢視解析器。
  9. ViewReslover解析後返回具體View。
  10. DispatcherServlet根據View進行渲染檢視(即將模型資料填充至檢視中)。
  11. DispatcherServlet響應使用者。

請求 ---> DispatcherServlet(前端控制器)---> 呼叫HandlerMapping(處理器對映器)---> DispatcherServlet呼叫 HandlerAdapter(處理器介面卡)---> 適配呼叫具體的Controller ---> 返回ModelAndView ---> 傳給ViewReslover檢視解析器 ---> 解析後返回具體View ---> 根據View進行渲染檢視響應使用者

MyBatis原理

MyBatis完成2件事情

  1. 封裝JDBC操作
  2. 利用反射打通Java類與SQL語句之間的相互轉換

MyBatis的主要成員

  1. Configuration MyBatis所有的配置資訊都儲存在Configuration物件之中,配置檔案中的大部分配置都會儲存到該類中
  2. SqlSession 作為MyBatis工作的主要頂層API,表示和資料庫互動時的會話,完成必要資料庫增刪改查功能
  3. Executor MyBatis執行器,是MyBatis 排程的核心,負責SQL語句的生成和查詢快取的維護
  4. StatementHandler 封裝了JDBC Statement操作,負責對JDBC statement 的操作,如設定引數等
  5. ParameterHandler 負責對使用者傳遞的引數轉換成JDBC Statement 所對應的資料型別
  6. ResultSetHandler 負責將JDBC返回的ResultSet結果集物件轉換成List型別的集合
  7. TypeHandler 負責java資料型別和jdbc資料型別(也可以說是資料表列型別)之間的對映和轉換
  8. MappedStatement MappedStatement維護一條select|update|delete|insert節點的封裝
  9. SqlSource 負責根據使用者傳遞的parameterObject,動態地生成SQL語句,將資訊封裝到BoundSql物件中,並返回
  10. BoundSql 表示動態生成的SQL語句以及相應的引數資訊

這裡寫圖片描述

MyBatis快取

MyBatis提供查詢快取,用於減輕資料庫壓力,提高效能。MyBatis提供了一級快取和二級快取。

這裡寫圖片描述

  1. 一級快取是SqlSession級別的快取,每個SqlSession物件都有一個雜湊表用於快取資料,不同SqlSession物件之間快取不共享。同一個SqlSession物件物件執行2遍相同的SQL查詢,在第一次查詢執行完畢後將結果快取起來,這樣第二遍查詢就不用向資料庫查詢了,直接返回快取結果即可。一級快取是MyBatis內部實現的一個特性,使用者不能配置,預設情況下自動支援的快取,使用者沒有定製它的權利

  2. 二級快取是Application應用級別的快取,它的是生命週期很長,跟Application的宣告週期一樣,也就是說它的作用範圍是整個Application應用。MyBatis預設是不開啟二級快取的,可以在配置檔案中使用如下配置來開啟二級快取

<settings>
    <setting name="cacheEnabled" value="true"/>
</settings>
複製程式碼

後記

以上對面試相關的知識點做了系統的整理,希望對大家有所幫助。想要支援樓主的話,在 Github 上點個 Star,還有就是有些知識點是從網上優秀部落格摘抄下來的,如果作者不希望文章部分內容被轉載,可以通知樓主,會及時處理,感謝。

相關文章