成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

不愧是字節,面個實習也滿頭大汗!

開發 前端
需要說明的是 const 類型和 eq_ref 都使用了主鍵或唯一索引,不過這兩個類型有所區別,const 是與常量進行比較,查詢效率會更快,而 eq_ref 通常用于多表聯查中。

大家好,我是小林。

最近有一些同學問我,要準備到什么程度才能去找大廠日常實習?

其實大廠的日常實習面試和校招面試差不太多,面試的問題都差不多,八股+項目+算法,都必須要準備,只是說實習面試要求可能不會太嚴格,比如你實習的算法,即使沒寫出來,能說出大概的思路,其實也是能過的,秋招的話,可能沒寫出算法,大概率就涼了。

針對后端這一塊的話,編程語言+MySQL+Redis+網絡協議+操作系統+后端項目+算法,這幾大塊都需要好好準備一波,后端面試都是圍繞這幾大塊知識進行考察。

雖然后端技術棧還包含消息隊列、分布式、微服務,但是這些針對校招來說不是必須要掌握的內容,時間有限的情況下,可以延后學習。

今天就分享一個字節日常實習的一面和二面的Java后端面經,我把問題分類了一下,主要是網絡+操作系統+Java+MySQL+Redis+算法這幾大塊問題,也做對應的解答,內容比較多,同學們可以收藏,后續慢慢細看&復習。

網絡

http和https的區別是什么?

HTTP 與 HTTPS 網絡層HTTP 與 HTTPS 網絡層

  • HTTP 是超文本傳輸協議,信息是明文傳輸,存在安全風險的問題。HTTPS 則解決 HTTP 不安全的缺陷,在 TCP 和 HTTP 網絡層之間加入了 SSL/TLS 安全協議,使得報文能夠加密傳輸。
  • HTTP 連接建立相對簡單, TCP 三次握手之后便可進行 HTTP 的報文傳輸。而 HTTPS 在 TCP 三次握手之后,還需進行 SSL/TLS 的握手過程,才可進入加密報文傳輸。
  • 兩者的默認端口不一樣,HTTP 默認端口號是 80,HTTPS 默認端口號是 443。
  • HTTPS 協議需要向 CA(證書權威機構)申請數字證書,來保證服務器的身份是可信的。

域名和ip地址如何對映的?

主要通過DNS域名解析來完成的。域名解析的工作流程:

  1. 客戶端首先會發出一個 DNS 請求,問 www.server.com 的 IP 是啥,并發給本地 DNS 服務器(也就是客戶端的 TCP/IP 設置中填寫的 DNS 服務器地址)。
  2. 本地域名服務器收到客戶端的請求后,如果緩存里的表格能找到 www.server.com,則它直接返回 IP 地址。如果沒有,本地 DNS 會去問它的根域名服務器:“老大, 能告訴我 www.server.com 的 IP 地址嗎?” 根域名服務器是最高層次的,它不直接用于域名解析,但能指明一條道路。
  3. 根 DNS 收到來自本地 DNS 的請求后,發現后置是 .com,說:“www.server.com 這個域名歸 .com 區域管理”,我給你 .com 頂級域名服務器地址給你,你去問問它吧。”
  4. 本地 DNS 收到頂級域名服務器的地址后,發起請求問“老二, 你能告訴我 www.server.com 的 IP 地址嗎?”
  5. 頂級域名服務器說:“我給你負責 www.server.com 區域的權威 DNS 服務器的地址,你去問它應該能問到”。
  6. 本地 DNS 于是轉向問權威 DNS 服務器:“老三,www.server.com對應的IP是啥呀?” server.com 的權威 DNS 服務器,它是域名解析結果的原出處。為啥叫權威呢?就是我的域名我做主。
  7. 權威 DNS 服務器查詢后將對應的 IP 地址 X.X.X.X 告訴本地 DNS。
  8. 本地 DNS 再將 IP 地址返回客戶端,客戶端和目標建立連接。

至此,我們完成了 DNS 的解析過程。現在總結一下,整個過程我畫成了一個圖。

域名解析的工作流程域名解析的工作流程

get和post的區別?

根據 RFC 規范,GET 的語義是從服務器獲取指定的資源,這個資源可以是靜態的文本、頁面、圖片視頻等。GET 請求的參數位置一般是寫在 URL 中,URL 規定只能支持 ASCII,所以 GET 請求的參數只允許 ASCII 字符 ,而且瀏覽器會對 URL 的長度有限制(HTTP協議本身對 URL長度并沒有做任何規定)。

比如,你打開我的文章,瀏覽器就會發送 GET 請求給服務器,服務器就會返回文章的所有文字及資源。

GET 請求GET 請求

根據 RFC 規范,POST 的語義是根據請求負荷(報文body)對指定的資源做出處理,具體的處理方式視資源類型而不同。POST 請求攜帶數據的位置一般是寫在報文 body 中,body 中的數據可以是任意格式的數據,只要客戶端與服務端協商好即可,而且瀏覽器不會對 body 大小做限制。

比如,你在我文章底部,敲入了留言后點擊「提交」(暗示你們留言),瀏覽器就會執行一次 POST 請求,把你的留言文字放進了報文 body 里,然后拼接好 POST 請求頭,通過 TCP 協議發送給服務器。

POST 請求POST 請求

tcp 建立連接的三次握手過程介紹一下

TCP 是面向連接的協議,所以使用 TCP 前必須先建立連接,而建立連接是通過三次握手來進行的。三次握手的過程如下圖:

TCP 三次握手TCP 三次握手

  • 一開始,客戶端和服務端都處于 CLOSE 狀態。先是服務端主動監聽某個端口,處于 LISTEN 狀態

第一個報文 —— SYN 報文第一個報文 —— SYN 報文

  • 客戶端會隨機初始化序號(client_isn),將此序號置于 TCP 首部的「序號」字段中,同時把 SYN 標志位置為 1,表示 SYN 報文。接著把第一個 SYN 報文發送給服務端,表示向服務端發起連接,該報文不包含應用層數據,之后客戶端處于 SYN-SENT 狀態。

第二個報文 —— SYN + ACK 報文第二個報文 —— SYN + ACK 報文

  • 服務端收到客戶端的 SYN 報文后,首先服務端也隨機初始化自己的序號(server_isn),將此序號填入 TCP 首部的「序號」字段中,其次把 TCP 首部的「確認應答號」字段填入 client_isn + 1, 接著把 SYN 和 ACK 標志位置為 1。最后把該報文發給客戶端,該報文也不包含應用層數據,之后服務端處于 SYN-RCVD 狀態。

第三個報文 —— ACK 報文第三個報文 —— ACK 報文

  • 客戶端收到服務端報文后,還要向服務端回應最后一個應答報文,首先該應答報文 TCP 首部 ACK 標志位置為 1 ,其次「確認應答號」字段填入 server_isn + 1 ,最后把報文發送給服務端,這次報文可以攜帶客戶到服務端的數據,之后客戶端處于 ESTABLISHED 狀態。
  • 服務端收到客戶端的應答報文后,也進入 ESTABLISHED 狀態。

為什么不是 2 次或者 4 次握手?

三次握手的原因:

  • 三次握手才可以阻止重復歷史連接的初始化(主要原因)
  • 三次握手才可以同步雙方的初始序列號
  • 三次握手才可以避免資源浪費

原因一:避免歷史連接

我們來看看 RFC 793 指出的 TCP 連接使用三次握手的首要原因:

The principle reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion.

簡單來說,三次握手的首要原因是為了防止舊的重復連接初始化造成混亂。

我們考慮一個場景,客戶端先發送了 SYN(seq = 90)報文,然后客戶端宕機了,而且這個 SYN 報文還被網絡阻塞了,服務端并沒有收到,接著客戶端重啟后,又重新向服務端建立連接,發送了 SYN(seq = 100)報文(注意!不是重傳 SYN,重傳的 SYN 的序列號是一樣的)。

看看三次握手是如何阻止歷史連接的:

三次握手避免歷史連接三次握手避免歷史連接

客戶端連續發送多次 SYN(都是同一個四元組)建立連接的報文,在網絡擁堵情況下:

  • 一個「舊 SYN 報文」比「最新的 SYN」 報文早到達了服務端,那么此時服務端就會回一個 SYN + ACK 報文給客戶端,此報文中的確認號是 91(90+1)。
  • 客戶端收到后,發現自己期望收到的確認號應該是 100 + 1,而不是 90 + 1,于是就會回 RST 報文。
  • 服務端收到 RST 報文后,就會釋放連接。
  • 后續最新的 SYN 抵達了服務端后,客戶端與服務端就可以正常的完成三次握手了。

上述中的「舊 SYN 報文」稱為歷史連接,TCP 使用三次握手建立連接的最主要原因就是防止「歷史連接」初始化了連接。

如果是兩次握手連接,就無法阻止歷史連接,那為什么 TCP 兩次握手為什么無法阻止歷史連接呢?

我先直接說結論,主要是因為在兩次握手的情況下,服務端沒有中間狀態給客戶端來阻止歷史連接,導致服務端可能建立一個歷史連接,造成資源浪費。

你想想,在兩次握手的情況下,服務端在收到 SYN 報文后,就進入 ESTABLISHED 狀態,意味著這時可以給對方發送數據,但是客戶端此時還沒有進入 ESTABLISHED 狀態,假設這次是歷史連接,客戶端判斷到此次連接為歷史連接,那么就會回 RST 報文來斷開連接,而服務端在第一次握手的時候就進入 ESTABLISHED 狀態,所以它可以發送數據的,但是它并不知道這個是歷史連接,它只有在收到 RST 報文后,才會斷開連接。

兩次握手無法阻止歷史連接

可以看到,如果采用兩次握手建立 TCP 連接的場景下,服務端在向客戶端發送數據前,并沒有阻止掉歷史連接,導致服務端建立了一個歷史連接,又白白發送了數據,妥妥地浪費了服務端的資源。

因此,要解決這種現象,最好就是在服務端發送數據前,也就是建立連接之前,要阻止掉歷史連接,這樣就不會造成資源浪費,而要實現這個功能,就需要三次握手。

所以,TCP 使用三次握手建立連接的最主要原因是防止「歷史連接」初始化了連接。

原因二:同步雙方初始序列號

TCP 協議的通信雙方, 都必須維護一個「序列號」, 序列號是可靠傳輸的一個關鍵因素,它的作用:

  • 接收方可以去除重復的數據;
  • 接收方可以根據數據包的序列號按序接收;
  • 可以標識發送出去的數據包中, 哪些是已經被對方收到的(通過 ACK 報文中的序列號知道);

可見,序列號在 TCP 連接中占據著非常重要的作用,所以當客戶端發送攜帶「初始序列號」的 SYN 報文的時候,需要服務端回一個 ACK 應答報文,表示客戶端的 SYN 報文已被服務端成功接收,那當服務端發送「初始序列號」給客戶端的時候,依然也要得到客戶端的應答回應,這樣一來一回,才能確保雙方的初始序列號能被可靠的同步。

四次握手與三次握手四次握手與三次握手

四次握手其實也能夠可靠的同步雙方的初始化序號,但由于第二步和第三步可以優化成一步,所以就成了「三次握手」。

而兩次握手只保證了一方的初始序列號能被對方成功接收,沒辦法保證雙方的初始序列號都能被確認接收。

原因三:避免資源浪費

如果只有「兩次握手」,當客戶端發生的 SYN 報文在網絡中阻塞,客戶端沒有接收到 ACK 報文,就會重新發送 SYN ,由于沒有第三次握手,服務端不清楚客戶端是否收到了自己回復的 ACK 報文,所以服務端每收到一個 SYN 就只能先主動建立一個連接,這會造成什么情況呢?

如果客戶端發送的 SYN 報文在網絡中阻塞了,重復發送多次 SYN 報文,那么服務端在收到請求后就會建立多個冗余的無效鏈接,造成不必要的資源浪費。

兩次握手會造成資源浪費兩次握手會造成資源浪費

即兩次握手會造成消息滯留情況下,服務端重復接受無用的連接請求 SYN 報文,而造成重復分配資源。

操作系統

已知一個進程名,如何殺掉這個進程?

在Linux 操作系統,可以使用kill命令來殺死進程。

首先,使用ps命令查找進程的PID(進程ID),然后使用kill命令加上PID來終止進程。例如:

ps -ef | grep <進程名>   // 查找進程的PID
kill <PID>              // 終止進程

進程間通信有哪些方式?

  • Linux 內核提供了不少進程間通信的方式,其中最簡單的方式就是管道,管道分為「匿名管道」和「命名管道」。
  • 匿名管道顧名思義,它沒有名字標識,匿名管道是特殊文件只存在于內存,沒有存在于文件系統中,shell 命令中的「|」豎線就是匿名管道,通信的數據是無格式的流并且大小受限,通信的方式是單向的,數據只能在一個方向上流動,如果要雙向通信,需要創建兩個管道,再來匿名管道是只能用于存在父子關系的進程間通信,匿名管道的生命周期隨著進程創建而建立,隨著進程終止而消失。
  • 命名管道突破了匿名管道只能在親緣關系進程間的通信限制,因為使用命名管道的前提,需要在文件系統創建一個類型為 p 的設備文件,那么毫無關系的進程就可以通過這個設備文件進行通信。另外,不管是匿名管道還是命名管道,進程寫入的數據都是緩存在內核中,另一個進程讀取數據時候自然也是從內核中獲取,同時通信數據都遵循先進先出原則,不支持 lseek 之類的文件定位操作。
  • 消息隊列克服了管道通信的數據是無格式的字節流的問題,消息隊列實際上是保存在內核的「消息鏈表」,消息隊列的消息體是可以用戶自定義的數據類型,發送數據時,會被分成一個一個獨立的消息體,當然接收數據時,也要與發送方發送的消息體的數據類型保持一致,這樣才能保證讀取的數據是正確的。消息隊列通信的速度不是最及時的,畢竟每次數據的寫入和讀取都需要經過用戶態與內核態之間的拷貝過程。
  • 共享內存可以解決消息隊列通信中用戶態與內核態之間數據拷貝過程帶來的開銷,它直接分配一個共享空間,每個進程都可以直接訪問,就像訪問進程自己的空間一樣快捷方便,不需要陷入內核態或者系統調用,大大提高了通信的速度,享有最快的進程間通信方式之名。但是便捷高效的共享內存通信,帶來新的問題,多進程競爭同個共享資源會造成數據的錯亂。
  • 那么,就需要信號量來保護共享資源,以確保任何時刻只能有一個進程訪問共享資源,這種方式就是互斥訪問。信號量不僅可以實現訪問的互斥性,還可以實現進程間的同步,信號量其實是一個計數器,表示的是資源個數,其值可以通過兩個原子操作來控制,分別是 P 操作和 V 操作。
  • 與信號量名字很相似的叫信號,它倆名字雖然相似,但功能一點兒都不一樣。信號是異步通信機制,信號可以在應用進程和內核之間直接交互,內核也可以利用信號來通知用戶空間的進程發生了哪些系統事件,信號事件的來源主要有硬件來源(如鍵盤 Cltr+C )和軟件來源(如 kill 命令),一旦有信號發生,進程有三種方式響應信號 1. 執行默認操作、2. 捕捉信號、3. 忽略信號。有兩個信號是應用進程無法捕捉和忽略的,即 SIGKILL 和 SIGSTOP,這是為了方便我們能在任何時候結束或停止某個進程。
  • 前面說到的通信機制,都是工作于同一臺主機,如果要與不同主機的進程間通信,那么就需要 Socket 通信了。Socket 實際上不僅用于不同的主機進程間通信,還可以用于本地主機進程間通信,可根據創建 Socket 的類型不同,分為三種常見的通信方式,一個是基于 TCP 協議的通信方式,一個是基于 UDP 協議的通信方式,一個是本地進程間通信方式。

kill -9 使用的是哪種?

  • 信號

fork創建子進程有哪些特點呢?

  • 父子進程:fork調用后,會創建一個新的子進程,該子進程與父進程幾乎完全相同,包括代碼、數據和打開文件等。子進程從fork調用的位置開始執行,父進程和子進程在fork調用之后的代碼處繼續執行。
  • 資源繼承:子進程繼承了父進程的大部分資源,包括打開的文件、文件描述符、信號處理器等。但是有些資源(如互斥鎖和定時器)可能需要進行特殊處理,以避免競爭條件或資源泄漏。
  • 內存:父進程和子進程擁有獨立的虛擬內存空間,每個進程都有自己的內存映射表。子進程通過寫時復制(copy-on-write)機制與父進程共享物理內存,只有在需要修改內存內容時才會進行復制。
  • 父子關系:父進程可以通過fork的返回值判斷是否為子進程。父進程的fork返回子進程的PID,而子進程的fork返回0。這樣可以根據返回值的不同,在父子進程中執行不同的邏輯。

Copy On Write技術介紹一下

在創建子進程的過程中,操作系統會把父進程的「頁表」復制一份給子進程,這個頁表記錄著虛擬地址和物理地址映射關系,而不會復制物理內存,也就是說,兩者的虛擬空間不同,但其對應的物理空間是同一個。

圖片圖片

這樣一來,子進程就共享了父進程的物理內存數據了,這樣能夠節約物理內存資源,頁表對應的頁表項的屬性會標記該物理內存的權限為只讀。

當父進程或者子進程在向共享內存發起寫操作時,CPU 就會觸發寫保護中斷,這個「寫保護中斷」是由于違反權限導致的,然后操作系統會在「寫保護中斷處理函數」里進行物理內存的復制,并重新設置其內存映射關系,將父子進程的內存讀寫權限設置為可讀寫,最后才會對內存進行寫操作,這個過程被稱為「**寫時復制(Copy On Write)**」。

圖片圖片

寫時復制顧名思義,在發生寫操作的時候,操作系統才會去復制物理內存,這樣是為了防止 fork 創建子進程時,由于物理內存數據的復制時間過長而導致父進程長時間阻塞的問題。

Java

ArrayList和LinkedList的區別?

  • 底層數據結構:ArrayList使用數組作為底層數據結構,而LinkedList使用雙向鏈表作為底層數據結構
  • 隨機訪問性能:ArrayList支持通過索引直接訪問元素,因為底層數組的連續存儲特性,所以時間復雜度為O(1)。而LinkedList需要從頭或尾部開始遍歷鏈表,時間復雜度為O(n)。
  • 插入和刪除操作:ArrayList在尾部插入和刪除元素的時間復雜度為O(1),因為它只需要調整數組的長度即可。但在中間或頭部插入和刪除元素時,需要將后續元素進行移動,時間復雜度為O(n)。而LinkedList在任意位置插入和刪除元素的時間復雜度為O(1),因為只需要調整節點的指針即可。
  • 內存占用:ArrayList在每個元素中都存儲了實際的數據,而LinkedList在每個節點中存儲了數據和前后節點的指針。因此,相同數量的元素情況下,LinkedList通常比ArrayList占用更多的內存空間。

在使用時候,這兩者的應用場景是什么?

  • 如果需要頻繁進行隨機訪問操作,而對插入和刪除操作要求不高,可以選擇ArrayList。
  • 如果需要頻繁進行插入和刪除操作,而對隨機訪問操作要求不高,可以選擇LinkedList。

HashMap的底層數據結構是什么?

在 JDK 1.7 版本之前, HashMap 數據結構是數組和鏈表,HashMap通過哈希算法將元素的鍵(Key)映射到數組中的槽位(Bucket)。如果多個鍵映射到同一個槽位,它們會以鏈表的形式存儲在同一個槽位上,因為鏈表的查詢時間是O(n),所以沖突很嚴重,一個索引上的鏈表非常長,效率就很低了。

所以在 JDK 1.8版本的時候做了優化,當一個鏈表的長度超過8的時候就轉換數據結構,不再使用鏈表存儲,而是使用紅黑樹,查找時使用紅黑樹,時間復雜度O(log n),可以提高查詢性能,但是在數量較少時,即數量小于6時,會將紅黑樹轉換回鏈表。

圖片圖片

HashMap會產生哪些并發安全?

  • JDK 1.7 HashMap 采用數組 + 鏈表的數據結構,多線程背景下,在數組擴容的時候,存在 Entry 鏈死循環和數據丟失問題。
  • JDK 1.8 HashMap 采用數組 + 鏈表 + 紅黑二叉樹的數據結構,優化了 1.7 中數組擴容的方案,解決了 Entry 鏈死循環和數據丟失問題。但是多線程背景下,put 方法存在數據覆蓋的問題。

說一下ConcurrentHashMap是如何實現的線程安全的?

JDK 1.7 ConcurrentHashMap

在 JDK 1.7 中它使用的是數組加鏈表的形式實現的,而數組又分為:大數組 Segment 和小數組 HashEntry。Segment 是一種可重入鎖(ReentrantLock),在 ConcurrentHashMap 里扮演鎖的角色;HashEntry 則用于存儲鍵值對數據。一個 ConcurrentHashMap 里包含一個 Segment 數組,一個 Segment 里包含一個 HashEntry 數組,每個 HashEntry 是一個鏈表結構的元素。

分段鎖技術將數據分成一段一段的存儲,然后給每一段數據配一把鎖,當一個線程占用鎖訪問其中一個段數據的時候,其他段的數據也能被其他線程訪問,能夠實現真正的并發訪問。

JDK 1.8 ConcurrentHashMap

在 JDK 1.7 中,ConcurrentHashMap 雖然是線程安全的,但因為它的底層實現是數組 + 鏈表的形式,所以在數據比較多的情況下訪問是很慢的,因為要遍歷整個鏈表,而 JDK 1.8 則使用了數組 + 鏈表/紅黑樹的方式優化了 ConcurrentHashMap 的實現,具體實現結構如下:

JDK 1.8 ConcurrentHashMap 主要通過 volatile + CAS 或者 synchronized 來實現的線程安全的。

添加元素時首先會判斷容器是否為空:

  • 如果為空則使用  volatile  加  CAS  來初始化,如果容器不為空,則根據存儲的元素計算該位置是否為空。

如果根據存儲的元素計算結果為空,則利用  CAS  設置該節點;

如果根據存儲的元素計算結果不為空,則使用 synchronized  ,然后,遍歷桶中的數據,并替換或新增節點到桶中,最后再判斷是否需要轉為紅黑樹,這樣就能保證并發訪問時的線程安全了。

如果把上面的執行用一句話歸納的話,就相當于是ConcurrentHashMap通過對頭結點加鎖來保證線程安全的,鎖的粒度相比 Segment 來說更小了,發生沖突和加鎖的頻率降低了,并發操作的性能就提高了。而且 JDK 1.8 使用的是紅黑樹優化了之前的固定鏈表,那么當數據量比較大的時候,查詢性能也得到了很大的提升,從之前的 O(n) 優化到了 O(logn) 的時間復雜度。

除了ConcurrentHashMap,還可以如何將HashMap變為線程安全的?

  • 將 HashMap 替換成HashTable,HashTable是線程安全的。
  • 使用Collections.synchronizedMap方法:可以使用Collections.synchronizedMap方法創建一個線程安全的HashMap,它會返回一個包裝后的線程安全的Map對象。該方法使用了內部的同步機制來保證線程安全,但在高并發環境下可能性能較低。
Map<KeyType, ValueType> synchronizedMap = Collections.synchronizedMap(new HashMap<>());

Redis

Redis常見的數據結構有哪些?以及底層是如何實現的?

Redis 提供了豐富的數據類型,常見的有五種數據類型:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)。

圖片圖片

  • String 類型的應用場景:緩存對象、常規計數、分布式鎖、共享 session 信息等。
  • List 類型的應用場景:消息隊列(但是有兩個問題:1. 生產者需要自行實現全局唯一 ID;2. 不能以消費組形式消費數據)等。
  • Hash 類型:緩存對象、購物車等。
  • Set 類型:聚合計算(并集、交集、差集)場景,比如點贊、共同關注、抽獎活動等。
  • Zset 類型:排序場景,比如排行榜、電話和姓名排序等。

我畫了一張 Redis 數據類型和底層數據結構的對應關圖,左邊是 Redis 3.0版本的,也就是《Redis 設計與實現》這本書講解的版本,現在看還是有點過時了,右邊是現在 Redis 7.0 版本的。

String 類型內部實現

String 類型的底層的數據結構實現主要是 SDS(簡單動態字符串)。SDS 和我們認識的 C 字符串不太一樣,之所以沒有使用 C 語言的字符串表示,因為 SDS 相比于 C 的原生字符串:

  • SDS 不僅可以保存文本數據,還可以保存二進制數據。因為 SDS 使用 len 屬性的值而不是空字符來判斷字符串是否結束,并且 SDS 的所有 API 都會以處理二進制的方式來處理 SDS 存放在 buf[] 數組里的數據。所以 SDS 不光能存放文本數據,而且能保存圖片、音頻、視頻、壓縮文件這樣的二進制數據。
  • **SDS 獲取字符串長度的時間復雜度是 O(1)**。因為 C 語言的字符串并不記錄自身長度,所以獲取長度的復雜度為 O(n);而 SDS 結構里用 len 屬性記錄了字符串長度,所以復雜度為 O(1)。
  • Redis 的 SDS API 是安全的,拼接字符串不會造成緩沖區溢出。因為 SDS 在拼接字符串之前會檢查 SDS 空間是否滿足要求,如果空間不夠會自動擴容,所以不會導致緩沖區溢出的問題。

List 類型內部實現

List 類型的底層數據結構是由雙向鏈表或壓縮列表實現的:

  • 如果列表的元素個數小于 512 個(默認值,可由 list-max-ziplist-entries 配置),列表每個元素的值都小于 64 字節(默認值,可由 list-max-ziplist-value 配置),Redis 會使用壓縮列表作為 List 類型的底層數據結構;
  • 如果列表的元素不滿足上面的條件,Redis 會使用雙向鏈表作為 List 類型的底層數據結構;

但是在 Redis 3.2 版本之后,List 數據類型底層數據結構就只由 quicklist 實現了,替代了雙向鏈表和壓縮列表。

Hash 類型內部實現

Hash 類型的底層數據結構是由壓縮列表或哈希表實現的:

  • 如果哈希類型元素個數小于 512 個(默認值,可由 hash-max-ziplist-entries 配置),所有值小于 64 字節(默認值,可由 hash-max-ziplist-value 配置)的話,Redis 會使用壓縮列表作為 Hash 類型的底層數據結構;
  • 如果哈希類型元素不滿足上面條件,Redis 會使用哈希表作為 Hash 類型的底層數據結構。

在 Redis 7.0 中,壓縮列表數據結構已經廢棄了,交由 listpack 數據結構來實現了。

Set 類型內部實現

Set 類型的底層數據結構是由哈希表或整數集合實現的:

  • 如果集合中的元素都是整數且元素個數小于 512 (默認值,set-maxintset-entries配置)個,Redis 會使用整數集合作為 Set 類型的底層數據結構;
  • 如果集合中的元素不滿足上面條件,則 Redis 使用哈希表作為 Set 類型的底層數據結構。

ZSet 類型內部實現

Zset 類型的底層數據結構是由壓縮列表或跳表實現的:

  • 如果有序集合的元素個數小于 128 個,并且每個元素的值小于 64 字節時,Redis 會使用壓縮列表作為 Zset 類型的底層數據結構;
  • 如果有序集合的元素不滿足上面的條件,Redis 會使用跳表作為 Zset 類型的底層數據結構;

在 Redis 7.0 中,壓縮列表數據結構已經廢棄了,交由 listpack 數據結構來實現了。

項目中為什么引入Redis呢?和本地緩存有哪些區別?

  • 主要是為了提升查詢效率,減少對MySQL的訪問。
  • Redis 提供了豐富的數據類型,能滿足各種業務場景,并且Redis還可以進行集群擴展,以及實現分布式鎖,這是本地緩存做不到的。

如果Redis數據超過內存限制,該如何處理?

在配置文件 redis.conf 中,可以通過參數 maxmemory <bytes> 來設定最大運行內存,只有在 Redis 的運行內存達到了我們設置的最大運行內存,才會觸發內存淘汰策略。不同位數的操作系統,maxmemory 的默認值是不同的:

  • 在 64 位操作系統中,maxmemory 的默認值是 0,表示沒有內存大小限制,那么不管用戶存放多少數據到 Redis 中,Redis 也不會對可用內存進行檢查,直到 Redis 實例因內存不足而崩潰也無作為。
  • 在 32 位操作系統中,maxmemory 的默認值是 3G,因為 32 位的機器最大只支持 4GB 的內存,而系統本身就需要一定的內存資源來支持運行,所以 32 位操作系統限制最大 3 GB 的可用內存是非常合理的,這樣可以避免因為內存不足而導致 Redis 實例崩潰。

Redis 內存淘汰策略共有八種,這八種策略大體分為「不進行數據淘汰」和「進行數據淘汰」兩類策略。

1、不進行數據淘汰的策略

noeviction(Redis3.0之后,默認的內存淘汰策略) :它表示當運行內存超過最大設置內存時,不淘汰任何數據,這時如果有新的數據寫入,會報錯通知禁止寫入,不淘汰任何數據,但是如果沒用數據寫入的話,只是單純的查詢或者刪除操作的話,還是可以正常工作。

2、進行數據淘汰的策略

針對「進行數據淘汰」這一類策略,又可以細分為「在設置了過期時間的數據中進行淘汰」和「在所有數據范圍內進行淘汰」這兩類策略。

在設置了過期時間的數據中進行淘汰:

  • volatile-random:隨機淘汰設置了過期時間的任意鍵值;
  • volatile-ttl:優先淘汰更早過期的鍵值。
  • volatile-lru(Redis3.0 之前,默認的內存淘汰策略):淘汰所有設置了過期時間的鍵值中,最久未使用的鍵值;
  • volatile-lfu(Redis 4.0 后新增的內存淘汰策略):淘汰所有設置了過期時間的鍵值中,最少使用的鍵值;

在所有數據范圍內進行淘汰:

  • allkeys-random:隨機淘汰任意鍵值;
  • allkeys-lru:淘汰整個鍵值中最久未使用的鍵值;
  • allkeys-lfu(Redis 4.0 后新增的內存淘汰策略):淘汰整個鍵值中最少使用的鍵值。

Redis緩存過期如何實現的?底層呢?

Redis 選擇「惰性刪除+定期刪除」這兩種策略配和使用,以求在合理使用 CPU 時間和避免內存浪費之間取得平衡。

Redis 是怎么實現惰性刪除的?

Redis 的惰性刪除策略由 db.c 文件中的 expireIfNeeded 函數實現,代碼如下:

int expireIfNeeded(redisDb *db, robj *key) {
    // 判斷 key 是否過期
    if (!keyIsExpired(db,key)) return 0;
    ....
    /* 刪除過期鍵 */
    ....
    // 如果 server.lazyfree_lazy_expire 為 1 表示異步刪除,反之同步刪除;
    return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :
                                         dbSyncDelete(db,key);
}

Redis 在訪問或者修改 key 之前,都會調用 expireIfNeeded 函數對其進行檢查,檢查 key 是否過期:

  • 如果過期,則刪除該 key,至于選擇異步刪除,還是選擇同步刪除,根據 lazyfree_lazy_expire 參數配置決定(Redis 4.0版本開始提供參數),然后返回 null 客戶端;
  • 如果沒有過期,不做任何處理,然后返回正常的鍵值對給客戶端;

惰性刪除的流程圖如下:

圖片圖片

Redis 是怎么實現定期刪除的?

再回憶一下,定期刪除策略的做法:每隔一段時間「隨機」從數據庫中取出一定數量的 key 進行檢查,并刪除其中的過期key。

1、這個間隔檢查的時間是多長呢?

在 Redis 中,默認每秒進行 10 次過期檢查一次數據庫,此配置可通過 Redis 的配置文件 redis.conf 進行配置,配置鍵為 hz 它的默認值是 hz 10。

特別強調下,每次檢查數據庫并不是遍歷過期字典中的所有 key,而是從數據庫中隨機抽取一定數量的 key 進行過期檢查。

2、隨機抽查的數量是多少呢?

我查了下源碼,定期刪除的實現在 expire.c 文件下的 activeExpireCycle 函數中,其中隨機抽查的數量由 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 定義的,它是寫死在代碼中的,數值是 20。

也就是說,數據庫每輪抽查時,會隨機選擇 20 個 key 判斷是否過期。

接下來,詳細說說 Redis 的定期刪除的流程:

  1. 從過期字典中隨機抽取 20 個 key;
  2. 檢查這 20 個 key 是否過期,并刪除已過期的 key;
  3. 如果本輪檢查的已過期 key 的數量,超過 5 個(20/4),也就是「已過期 key 的數量」占比「隨機抽取 key 的數量」大于 25%,則繼續重復步驟 1;如果已過期的 key 比例小于 25%,則停止繼續刪除過期 key,然后等待下一輪再檢查。

可以看到,定期刪除是一個循環的流程。

那 Redis 為了保證定期刪除不會出現循環過度,導致線程卡死現象,為此增加了定期刪除循環流程的時間上限,默認不會超過 25ms。

針對定期刪除的流程,我寫了個偽代碼:

do {
    //已過期的數量
    expired = 0;
    //隨機抽取的數量
    num = 20;
    while (num--) {
        //1. 從過期字典中隨機抽取 1 個 key
        //2. 判斷該 key 是否過期,如果已過期則進行刪除,同時對 expired++
    }
    
    // 超過時間限制則退出
    if (timelimit_exit) return;

  /* 如果本輪檢查的已過期 key 的數量,超過 25%,則繼續隨機抽查,否則退出本輪檢查 */
} while (expired > 20/4);

定期刪除的流程如下:

圖片圖片

介紹RDB持久化機制的詳細過程吧

因為 AOF 日志記錄的是操作命令,不是實際的數據,所以用 AOF 方法做故障恢復時,需要全量把日志都執行一遍,一旦 AOF 日志非常多,勢必會造成 Redis 的恢復操作緩慢。

為了解決這個問題,Redis 增加了 RDB 快照。所謂的快照,就是記錄某一個瞬間東西,比如當我們給風景拍照時,那一個瞬間的畫面和信息就記錄到了一張照片。

所以,RDB 快照就是記錄某一個瞬間的內存數據,記錄的是實際數據,而 AOF 文件記錄的是命令操作的日志,而不是實際的數據。

因此在 Redis 恢復數據時, RDB 恢復數據的效率會比 AOF 高些,因為直接將 RDB 文件讀入內存就可以,不需要像 AOF 那樣還需要額外執行操作命令的步驟才能恢復數據。

Redis 還可以通過配置文件的選項來實現每隔一段時間自動執行一次 bgsave 命令,默認會提供以下配置:

save 900 1
save 300 10
save 60 10000

別看選項名叫 save,實際上執行的是 bgsave 命令,也就是會創建子進程來生成 RDB 快照文件。只要滿足上面條件的任意一個,就會執行 bgsave,它們的意思分別是:

  • 900 秒之內,對數據庫進行了至少 1 次修改;
  • 300 秒之內,對數據庫進行了至少 10 次修改;
  • 60 秒之內,對數據庫進行了至少 10000 次修改。

這里提一點,Redis 的快照是全量快照,也就是說每次執行快照,都是把內存中的「所有數據」都記錄到磁盤中。所以執行快照是一個比較重的操作,如果頻率太頻繁,可能會對 Redis 性能產生影響。如果頻率太低,服務器故障時,丟失的數據會更多。

執行 bgsave 過程中,Redis 依然可以繼續處理操作命令的,也就是數據是能被修改的,關鍵的技術就在于寫時復制技術(Copy-On-Write, COW)。

執行 bgsave 命令的時候,會通過 fork() 創建子進程,此時子進程和父進程是共享同一片內存數據的,因為創建子進程的時候,會復制父進程的頁表,但是頁表指向的物理內存還是一個,此時如果主線程執行讀操作,則主線程和 bgsave 子進程互相不影響。

圖片圖片

如果主線程執行寫操作,則被修改的數據會復制一份副本,然后 bgsave 子進程會把該副本數據寫入 RDB 文件,在這個過程中,主線程仍然可以直接修改原來的數據。

圖片圖片

RDB持久化會阻塞主線程嘛?

Redis 提供了兩個命令來生成 RDB 文件,分別是 save 和 bgsave,他們的區別就在于是否在「主線程」里執行:

  • 執行了 save 命令,就會在主線程生成 RDB 文件,由于和執行操作命令在同一個線程,所以如果寫入 RDB 文件的時間太長,會阻塞主線程;
  • 執行了 bgsave 命令,會創建一個子進程來生成 RDB 文件,這樣可以避免主線程的阻塞;

MySQL

MySQL的char和varchar有什么區別?

區別如下:

  • 存儲方式:char類型是固定長度的字符類型,它會以指定的長度存儲每個字符,無論實際存儲的字符長度是多少。varchar類型是可變長度的字符類型,它會根據實際存儲的字符長度來動態調整存儲空間。
  • 空間占用:由于char類型是固定長度的,所以它在存儲時會占用固定的空間,無論實際存儲的字符長度是多少。而varchar類型在存儲時只會占用實際使用的空間,因此在存儲短字符時,varchar比char更節省空間。
  • 應用場景:如果的數據長度固定且相對較短,可以考慮使用char類型;如果您的數據長度可變或相對較長,可以考慮使用varchar類型。

索引有哪些分類嘛?

可以按照四個角度來分類索引。

  • 按「數據結構」分類:B+tree索引、Hash索引、Full-text索引。
  • 按「物理存儲」分類:聚簇索引(主鍵索引)、二級索引(輔助索引)。
  • 按「字段特性」分類:主鍵索引、唯一索引、普通索引、前綴索引。
  • 按「字段個數」分類:單列索引、聯合索引。

有什么索引優化的方式?

  • 前綴索引優化:前綴索引顧名思義就是使用某個字段中字符串的前幾個字符建立索引,使用前綴索引是為了減小索引字段大小,可以增加一個索引頁中存儲的索引值,有效提高索引的查詢速度。在一些大字符串的字段作為索引時,使用前綴索引可以幫助我們減小索引項的大小。
  • 覆蓋索引優化:覆蓋索引是指 SQL 中 query 的所有字段,在索引 B+Tree 的葉子節點上都能找得到的那些索引,從二級索引中查詢得到記錄,而不需要通過聚簇索引查詢獲得,可以避免回表的操作。
  • 主鍵索引最好是自增的;如果我們使用非自增主鍵,由于每次插入主鍵的索引值都是隨機的,因此每次插入新的數據時,就可能會插入到現有數據頁中間的某個位置,這將不得不移動其它數據來滿足新數據的插入,甚至需要從一個頁面復制數據到另外一個頁面,我們通常將這種情況稱為頁分裂。頁分裂還有可能會造成大量的內存碎片,導致索引結構不緊湊,從而影響查詢效率。
  • 防止索引失效:用上了索引并不意味著查詢的時候會使用到索引,所以我們心里要清楚有哪些情況會導致索引失效,從而避免寫出索引失效的查詢語句,否則這樣的查詢效率是很低的。

說一下Explain會返回哪些參數?

下圖,就是一個沒有使用索引,并且是一個全表掃描的查詢語句。

圖片圖片

對于執行計劃,參數有:

  • possible_keys 字段表示可能用到的索引;
  • key 字段表示實際用的索引,如果這一項為 NULL,說明沒有使用索引;
  • key_len 表示索引的長度;
  • rows 表示掃描的數據行數。
  • type 表示數據掃描類型,我們需要重點看這個。

type 字段就是描述了找到所需數據時使用的掃描方式是什么,常見掃描類型的執行效率從低到高的順序為:

  • All(全表掃描);
  • index(全索引掃描);
  • range(索引范圍掃描);
  • ref(非唯一索引掃描);
  • eq_ref(唯一索引掃描);
  • const(結果只有一條的主鍵或唯一索引掃描)。

在這些情況里,all 是最壞的情況,因為采用了全表掃描的方式。index 和 all 差不多,只不過 index 對索引表進行全掃描,這樣做的好處是不再需要對數據進行排序,但是開銷依然很大。所以,要盡量避免全表掃描和全索引掃描。

range 表示采用了索引范圍掃描,一般在 where 子句中使用 < 、>、in、between 等關鍵詞,只檢索給定范圍的行,屬于范圍查找。從這一級別開始,索引的作用會越來越明顯,因此我們需要盡量讓 SQL 查詢可以使用到 range 這一級別及以上的 type 訪問方式。

ref 類型表示采用了非唯一索引,或者是唯一索引的非唯一性前綴,返回數據返回可能是多條。因為雖然使用了索引,但該索引列的值并不唯一,有重復。這樣即使使用索引快速查找到了第一條數據,仍然不能停止,要進行目標值附近的小范圍掃描。但它的好處是它并不需要掃全表,因為索引是有序的,即便有重復值,也是在一個非常小的范圍內掃描。

eq_ref 類型是使用主鍵或唯一索引時產生的訪問方式,通常使用在多表聯查中。比如,對兩張表進行聯查,關聯條件是兩張表的 user_id 相等,且 user_id 是唯一索引,那么使用 EXPLAIN 進行執行計劃查看的時候,type 就會顯示 eq_ref。

const 類型表示使用了主鍵或者唯一索引與常量值進行比較,比如 select name from product where id=1。

需要說明的是 const 類型和 eq_ref 都使用了主鍵或唯一索引,不過這兩個類型有所區別,const 是與常量進行比較,查詢效率會更快,而 eq_ref 通常用于多表聯查中。

除了關注 type,我們也要關注 extra 顯示的結果。

這里說幾個重要的參考指標:

  • Using filesort :當查詢語句中包含 group by 操作,而且無法利用索引完成排序操作的時候, 這時不得不選擇相應的排序算法進行,甚至可能會通過文件排序,效率是很低的,所以要避免這種問題的出現。
  • Using temporary:使了用臨時表保存中間結果,MySQL 在對查詢結果排序時使用臨時表,常見于排序 order by 和分組查詢 group by。效率低,要避免這種問題的出現。
  • Using index:所需數據只需在索引即可全部獲得,不須要再到表中取數據,也就是使用了覆蓋索引,避免了回表操作,效率不錯。

算法

  • 樹的層次遍歷
  • 輸出樹的最大寬度
  • 合并k個有序數組
責任編輯:武曉燕 來源: 小林coding
相關推薦

2020-05-15 14:31:07

CPU分頁交換虛擬內存

2023-09-28 14:10:38

騰訊天美面試

2011-10-31 10:24:03

Linux死機

2022-02-14 08:11:07

黑客網絡攻擊任天堂

2021-05-26 16:30:15

Windows 功能系統

2022-01-17 14:24:09

共享字節面試

2020-03-11 19:40:09

架構架構設計Java

2021-06-30 17:38:03

Trie 樹字符Java

2025-03-28 10:47:05

開發注解Java

2024-08-30 08:59:15

2021-03-01 11:53:15

面試偽共享CPU

2025-04-08 09:20:00

Sentinel限流微服務

2024-10-11 10:18:27

2022-03-30 10:10:17

字節碼棧空間

2021-09-01 05:41:03

Promise CLI項目

2022-11-30 17:13:05

MySQLDynamic存儲

2024-07-30 14:01:51

Java字節碼JVM?

2022-08-17 08:17:01

SPI機制接口

2009-10-29 10:11:33

Ubuntu 9.10Karmic Koal操作系統

2021-07-20 05:37:17

項目面試簡歷
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 亚洲风情在线观看 | 欧美久 | 91av在线不卡 | 亚洲午夜精品 | 伊人伊成久久人综合网站 | 日韩在线播放第一页 | 久久精品aaa| 成人免费视频 | 亚洲一区二区免费视频 | 亚洲性在线 | 国产一区999 | 久久av一区二区三区 | 国产一区二区电影网 | 午夜天堂精品久久久久 | 久久久久久亚洲 | 久久精品成人热国产成 | 不卡av电影在线播放 | 自拍偷拍第一页 | 成人在线免费网站 | 美女露尿口视频 | 亚洲精品一区中文字幕乱码 | 亚洲协和影视 | 亚洲精品一区二区在线观看 | 欧美激情久久久 | 米奇狠狠鲁 | www312aⅴ欧美在线看 | 草久久 | 亚洲 日本 欧美 中文幕 | 亚洲一区二区三区视频免费观看 | 国产精品呻吟久久av凹凸 | 成人免费一区二区三区视频网站 | 色爱综合网 | 日韩精品在线播放 | 无码日韩精品一区二区免费 | 在线免费观看成人 | 欧美一级做性受免费大片免费 | 久一精品 | 99re热精品视频国产免费 | 日本精品视频在线观看 | 亚洲人成在线播放 | 久久一区二区三区四区 |