大家好,我是小林。
周五了,還是得卷一卷。今天分享一篇字節后端面經,因為項目是搞了黑馬點評,這個是用 redis 比較多的項目。
所以這次面經重點拷打了 redis 問題,什么大key,熱 key 問題。其余還拷打了mysql、操作系統、網絡、java 問題。
Redis
你用到了Redis,那么Redis還可以做什么?
Redis 是一種基于內存的數據庫,對數據的讀寫操作都是在內存中完成,因此讀寫速度非常快,常用于緩存,消息隊列、分布式鎖等場景。
img
Redis 提供了多種數據類型來支持不同的業務場景,比如 String(字符串)、Hash(哈希)、 List (列表)、Set(集合)、Zset(有序集合)、Bitmaps(位圖)、HyperLogLog(基數統計)、GEO(地理信息)、Stream(流),并且對數據類型的操作都是原子性的,因為執行命令由單線程負責的,不存在并發競爭的問題。
除此之外,Redis 還支持事務 、持久化、Lua 腳本、多種集群方案(主從復制模式、哨兵模式、切片機群模式)、發布/訂閱模式,內存淘汰機制、過期刪除機制等等。
講下Redis的ZSet
Zset 類型的底層數據結構是由壓縮列表或跳表實現的:
如果有序集合的元素個數小于 128 個,并且每個元素的值小于 64 字節時,Redis 會使用壓縮列表作為 Zset 類型的底層數據結構;
如果有序集合的元素不滿足上面的條件,Redis 會使用跳表作為 Zset 類型的底層數據結構;
在 Redis 7.0 中,壓縮列表數據結構已經廢棄了,交由 listpack 數據結構來實現了。
ZSet 的范圍查詢時間復雜度是多少?
Redis的ZSet的范圍查詢命令ZRANGE的時間復雜度是O(log(N)+M),其中N是有序集合的元素數量,M是返回的元素數量。
講一下跳表
鏈表在查找元素的時候,因為需要逐一查找,所以查詢效率非常低,時間復雜度是O(N),于是就出現了跳表。跳表是在鏈表基礎上改進過來的,實現了一種「多層」的有序鏈表,這樣的好處是能快讀定位數據。
那跳表長什么樣呢?我這里舉個例子,下圖展示了一個層級為 3 的跳表。
img
圖中頭節點有 L0~L2 三個頭指針,分別指向了不同層級的節點,然后每個層級的節點都通過指針連接起來:
L0 層級共有 5 個節點,分別是節點1、2、3、4、5;
L1 層級共有 3 個節點,分別是節點 2、3、5;
L2 層級只有 1 個節點,也就是節點 3 。
如果我們要在鏈表中查找節點 4 這個元素,只能從頭開始遍歷鏈表,需要查找 4 次,而使用了跳表后,只需要查找 2 次就能定位到節點 4,因為可以在頭節點直接從 L2 層級跳到節點 3,然后再往前遍歷找到節點 4。
可以看到,這個查找過程就是在多個層級上跳來跳去,最后定位到元素。當數據量很大時,跳表的查找復雜度就是 O(logN)。
跳表是怎么設置層高的?
跳表在創建節點時候,會生成范圍為[0-1]的一個隨機數,如果這個隨機數小于 0.25(相當于概率 25%),那么層數就增加 1 層,然后繼續生成下一個隨機數,直到隨機數的結果大于 0.25 結束,最終確定該節點的層數。
哈希表是怎么擴容的?
為了避免 rehash 在數據遷移過程中,因拷貝數據的耗時,影響 Redis 性能的情況,所以 Redis 采用了漸進式 rehash,也就是將數據的遷移的工作不再是一次性遷移完成,而是分多次遷移。
漸進式 rehash 步驟如下:
給「哈希表 2」 分配空間;
在 rehash 進行期間,每次哈希表元素進行新增、刪除、查找或者更新操作時,Redis 除了會執行對應的操作之外,還會順序將「哈希表 1 」中索引位置上的所有 key-value 遷移到「哈希表 2」 上;
隨著處理客戶端發起的哈希表操作請求數量越多,最終在某個時間點會把「哈希表 1 」的所有 key-value 遷移到「哈希表 2」,從而完成 rehash 操作。
這樣就巧妙地把一次性大量數據遷移工作的開銷,分攤到了多次處理請求的過程中,避免了一次性 rehash 的耗時操作。
在進行漸進式 rehash 的過程中,會有兩個哈希表,所以在漸進式 rehash 進行期間,哈希表元素的刪除、查找、更新等操作都會在這兩個哈希表進行。
哈希表擴容的時候,有讀請求怎么查?
查找一個 key 的值的話,先會在「哈希表 1」 里面進行查找,如果沒找到,就會繼續到哈希表 2 里面進行找到。
Redis大key如何解決?
對大Key進行拆分。例如將含有數萬成員的一個HASH Key拆分為多個HASH Key,并確保每個Key的成員數量在合理范圍。在Redis集群架構中,拆分大Key能對數據分片間的內存平衡起到顯著作用。
對大Key進行清理。將不適用Redis能力的數據存至其它存儲,并在Redis中刪除此類數據。注意,要使用異步刪除。
監控Redis的內存水位。可以通過監控系統設置合理的Redis內存報警閾值進行提醒,例如Redis內存使用率超過70%、Redis的內存在1小時內增長率超過20%等。
對過期數據進行定期清。堆積大量過期數據會造成大Key的產生,例如在HASH數據類型中以增量的形式不斷寫入大量數據而忽略了數據的時效性。可以通過定時任務的方式對失效數據進行清理。
什么是熱key?
通常以其接收到的Key被請求頻率來判定,例如:
QPS集中在特定的Key:Redis實例的總QPS(每秒查詢率)為10,000,而其中一個Key的每秒訪問量達到了7,000。
帶寬使用率集中在特定的Key:對一個擁有上千個成員且總大小為1 MB的HASH Key每秒發送大量的HGETALL操作請求。
CPU使用時間占比集中在特定的Key:對一個擁有數萬個成員的Key(ZSET類型)每秒發送大量的ZRANGE操作請求。
如何解決熱key問題?
在Redis集群架構中對熱Key進行復制。在Redis集群架構中,由于熱Key的遷移粒度問題,無法將請求分散至其他數據分片,導致單個數據分片的壓力無法下降。此時,可以將對應熱Key進行復制并遷移至其他數據分片,例如將熱Key foo復制出3個內容完全一樣的Key并名為foo2、foo3、foo4,將這三個Key遷移到其他數據分片來解決單個數據分片的熱Key壓力。
使用讀寫分離架構。如果熱Key的產生來自于讀請求,您可以將實例改造成讀寫分離架構來降低每個數據分片的讀請求壓力,甚至可以不斷地增加從節點。但是讀寫分離架構在增加業務代碼復雜度的同時,也會增加Redis集群架構復雜度。不僅要為多個從節點提供轉發層(如Proxy,LVS等)來實現負載均衡,還要考慮從節點數量顯著增加后帶來故障率增加的問題。Redis集群架構變更會為監控、運維、故障處理帶來了更大的挑戰。
mysql
索引優化詳細講講
常見優化索引的方法:
前綴索引優化:使用前綴索引是為了減小索引字段大小,可以增加一個索引頁中存儲的索引值,有效提高索引的查詢速度。在一些大字符串的字段作為索引時,使用前綴索引可以幫助我們減小索引項的大小。
覆蓋索引優化:覆蓋索引是指 SQL 中 query 的所有字段,在索引 B+Tree 的葉子節點上都能找得到的那些索引,從二級索引中查詢得到記錄,而不需要通過聚簇索引查詢獲得,可以避免回表的操作。
主鍵索引最好是自增的:
如果我們使用自增主鍵,那么每次插入的新數據就會按順序添加到當前索引節點的位置,不需要移動已有的數據,當頁面寫滿,就會自動開辟一個新頁面。因為每次插入一條新記錄,都是追加操作,不需要重新移動數據,因此這種插入數據的方法效率非常高。
如果我們使用非自增主鍵,由于每次插入主鍵的索引值都是隨機的,因此每次插入新的數據時,就可能會插入到現有數據頁中間的某個位置,這將不得不移動其它數據來滿足新數據的插入,甚至需要從一個頁面復制數據到另外一個頁面,我們通常將這種情況稱為頁分裂。頁分裂還有可能會造成大量的內存碎片,導致索引結構不緊湊,從而影響查詢效率。
防止索引失效:
當我們使用左或者左右模糊匹配的時候,也就是 like %xx 或者 like %xx%這兩種方式都會造成索引失效;
當我們在查詢條件中對索引列做了計算、函數、類型轉換操作,這些情況下都會造成索引失效;
聯合索引要能正確使用需要遵循最左匹配原則,也就是按照最左優先的方式進行索引的匹配,否則就會導致索引失效。
在 WHERE 子句中,如果在 OR 前的條件列是索引列,而在 OR 后的條件列不是索引列,那么索引會失效。
主鍵索引和非主鍵索引有什么區別?
主鍵索引和非主鍵索引的主要區別在于:
主鍵索引:主鍵是一種特殊的唯一索引,不允許有空值。每個表只能有一個主鍵。主鍵的主要作用是提供一種快速訪問表中特定信息的方式。
非主鍵索引:非主鍵索引,也稱為二級索引或輔助索引,可以有多個。非主鍵索引允許有空值,也允許有重復的值。
當我們進行索引覆蓋查詢的時候,在二級索引上查詢就可以了,就可以不需要回表,
MySQL的事務的幾個特性你知道嗎?
要遵守 4 個特性,分別如下:
原子性(Atomicity):一個事務中的所有操作,要么全部完成,要么全部不完成,不會結束在中間某個環節,而且事務在執行過程中發生錯誤,會被回滾到事務開始前的狀態,就像這個事務從來沒有執行過一樣,就好比買一件商品,購買成功時,則給商家付了錢,商品到手;購買失敗時,則商品在商家手中,消費者的錢也沒花出去。
一致性(Consistency):是指事務操作前和操作后,數據滿足完整性約束,數據庫保持一致性狀態。比如,用戶 A 和用戶 B 在銀行分別有 800 元和 600 元,總共 1400 元,用戶 A 給用戶 B 轉賬 200 元,分為兩個步驟,從 A 的賬戶扣除 200 元和對 B 的賬戶增加 200 元。一致性就是要求上述步驟操作后,最后的結果是用戶 A 還有 600 元,用戶 B 有 800 元,總共 1400 元,而不會出現用戶 A 扣除了 200 元,但用戶 B 未增加的情況(該情況,用戶 A 和 B 均為 600 元,總共 1200 元)。
隔離性(Isolation):數據庫允許多個并發事務同時對其數據進行讀寫和修改的能力,隔離性可以防止多個事務并發執行時由于交叉執行而導致數據的不一致,因為多個事務同時使用相同的數據時,不會相互干擾,每個事務都有一個完整的數據空間,對其他并發事務是隔離的。也就是說,消費者購買商品這個事務,是不影響其他消費者購買的。
持久性(Durability):事務處理結束后,對數據的修改就是永久的,即便系統故障也不會丟失。
操作系統
進程和線程的區別
獨立性:進程是系統資源分配的最小單位,線程是處理器調度的最小單位。每個進程有自己獨立的地址空間和系統資源,而線程則共享所屬進程的資源。
開銷:由于進程有自己獨立的地址空間,所以進程間的切換需要更大的開銷。線程間切換只需要保存和設置少量寄存器內容,開銷小。
數據共享:同一進程內的線程共享進程的資源,如內存、文件描述符等,所以線程間的數據共享和通信更容易。進程間需要通過進程間通信機制來實現數據共享或通信。
分配給進程的資源有哪些?
虛擬內存、文件描述符、信號等等
進程切換和線程切換的區別?
進程切換:進程切換涉及到更多的內容,包括整個進程的地址空間、全局變量、文件描述符等。因此,進程切換的開銷通常比線程切換大。
線程切換:線程切換只涉及到線程的堆棧、寄存器和程序計數器等,不涉及進程級別的資源,因此線程切換的開銷較小。
為什么并發執行線程要加鎖?
并發執行線程需要加鎖主要是為了保護共享數據,防止出現"競態條件"。
"競態條件"是指當多個線程同時訪問和操作同一塊數據時,最終結果依賴于線程的執行順序,這可能導致數據的不一致性。
通過加鎖,我們可以確保在任何時刻只有一個線程能夠訪問共享數據,從而避免"競態條件",確保數據的一致性和完整性。
線程死鎖的條件?
死鎖問題的產生是由兩個或者以上線程并行執行的時候,爭奪資源而互相等待造成的。
死鎖只有同時滿足互斥、持有并等待、不可剝奪、環路等待這四個條件的時候才會發生。
所以要避免死鎖問題,就是要破壞其中一個條件即可,最常用的方法就是使用資源有序分配法來破壞環路等待條件。
網絡
TCP和UDP的區別
連接:TCP 是面向連接的傳輸層協議,傳輸數據前先要建立連接。UDP 是不需要連接,即刻傳輸數據。
服務對象:TCP 是一對一的兩點服務,即一條連接只有兩個端點。UDP 支持一對一、一對多、多對多的交互通信
可靠性:TCP 是可靠交付數據的,數據可以無差錯、不丟失、不重復、按序到達。UDP 是盡最大努力交付,不保證可靠交付數據。
擁塞控制、流量控制:TCP 有擁塞控制和流量控制機制,保證數據傳輸的安全性。UDP 則沒有,即使網絡非常擁堵了,也不會影響 UDP 的發送速率。
傳輸方式:TCP 是流式傳輸,沒有邊界,但保證順序和可靠。UDP 是一個包一個包的發送,是有邊界的,但可能會丟包和亂序。
TCP的連接指的是什么東西
用于保證可靠性和流量控制維護的某些狀態信息,這些信息的組合,包括 Socket、序列號和窗口大小稱為連接。
img
TCP三次握手過程描述一下?
建立連接是通過三次握手來進行的。三次握手的過程如下圖:
TCP 三次握手
一開始,客戶端和服務端都處于 CLOSE 狀態。先是服務端主動監聽某個端口,處于 LISTEN 狀態。
客戶端會隨機初始化序號(client_isn),將此序號置于 TCP 首部的「序號」字段中,同時把 SYN 標志位置為 1,表示 SYN 報文。接著把第一個 SYN 報文發送給服務端,表示向服務端發起連接,該報文不包含應用層數據,之后客戶端處于 SYN-SENT 狀態。
服務端收到客戶端的 SYN 報文后,首先服務端也隨機初始化自己的序號(server_isn),將此序號填入 TCP 首部的「序號」字段中,其次把 TCP 首部的「確認應答號」字段填入 client_isn + 1, 接著把 SYN 和 ACK 標志位置為 1。最后把該報文發給客戶端,該報文也不包含應用層數據,之后服務端處于 SYN-RCVD 狀態。
客戶端收到服務端報文后,還要向服務端回應最后一個應答報文,首先該應答報文 TCP 首部 ACK 標志位置為 1 ,其次「確認應答號」字段填入 server_isn + 1 ,最后把報文發送給服務端,這次報文可以攜帶客戶到服務端的數據,之后客戶端處于 ESTABLISHED 狀態。
服務端收到客戶端的應答報文后,也進入 ESTABLISHED 狀態。
為什么要三次?
三個方面分析三次握手的原因:
三次握手才可以阻止重復歷史連接的初始化(主要原因)
三次握手才可以同步雙方的初始序列號
三次握手才可以避免資源浪費
我們考慮一個場景,客戶端先發送了 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 使用三次握手建立連接的最主要原因是防止「歷史連接」初始化了連接
Java
GC算法有哪幾種?
垃圾收集(GC)算法主要有以下幾種:
標記-清除(Mark-Sweep)算法:首先標記出所有需要回收的對象,在標記完成后統一回收所有被標記的對象。
復制(Copying)算法:將內存分為兩塊,每次只使用其中一塊,當這塊內存用完時,就將還在使用的對象復制到另外一塊上面,然后再把已使用過的內存空間一次清理掉。
標記-整理(Mark-Compact)算法:標記過程與"標記-清除"算法一樣,但后續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內存。
分代收集(Generational Collection)算法:根據對象存活周期的不同將內存劃分為幾塊。一般是把Java堆分為新生代和老年代,這樣我們就可以根據各個年代的特點采用最適當的收集算法。
增量(Incremental)算法:是一種漸進式的垃圾收集算法,它將垃圾收集的工作分為多個小部分分別執行,不需要一次性完成所有的垃圾收集工作,從而減少了垃圾收集時程序的暫停時間。
項目
介紹項目
項目架構是怎么樣的?
項目里為什么要用消息隊列?
請求很多,消息堆積處理不過來了如何應對?
項目都有哪些表?
算法
力扣:乘積最大子數組
歷史好文:我們又出成績了!! 拿了 7 個大廠 offer,我有話說! 被滴滴細節拷打,快頂不住了.... 拿下鵝廠一面! 被字節拷打了!基礎還是太重要了...
-
數據庫
+關注
關注
7文章
3794瀏覽量
64362 -
數據類型
+關注
關注
0文章
236瀏覽量
13618 -
Redis
+關注
關注
0文章
374瀏覽量
10871
原文標題:字節很會面試,追著項目技術拷打
文章出處:【微信號:小林coding,微信公眾號:小林coding】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論