(高頻問題)221-240 計算機 Java后端 實習 and 秋招 面試高頻問題匯總
221. 超長字段(如TEXT類型)建立索引的不適用性及替代方案
對數(shù)據(jù)庫中的超長字段(如TEXT或BLOB類型)直接建立索引通常是不合適的,這主要源于以下幾個方面的原因:
首先,索引本身會占用大量存儲空間。索引是為了加速查詢而創(chuàng)建的數(shù)據(jù)結構(例如B+樹),其建立和維護會額外占用存儲空間。對于TEXT或BLOB等超長字段,其數(shù)據(jù)體積通常較大,可能達到數(shù)百KB甚至數(shù)MB。直接對這類字段建立索引,會導致索引本身占據(jù)大量存儲空間,造成不必要的磁盤資源浪費。
其次,對超長字段建立索引會顯著增加索引維護的性能開銷。由于數(shù)據(jù)量巨大,索引結構(如B+樹)的節(jié)點會變得龐大,導致插入、更新、刪除等操作時,B+樹的調整成本急劇上升,進而降低整體性能。
此外,索引的查找效率也會受到影響。索引依賴于快速的比較操作,但對超長字段進行比較時,需要掃描大量數(shù)據(jù),這會大幅降低比較效率。在B+樹索引中,過長的索引字段可能導致B+樹層級增加,樹結構更深,從而延長特定值的查找時間。超長字段值的變更還可能引發(fā)頻繁的索引頁面分裂或合并,這些操作會帶來顯著的額外磁盤I/O開銷,進一步拖累系統(tǒng)性能。
值得注意的是,MySQL的InnoDB存儲引擎對單個索引的大小有限制。直接對TEXT字段建立索引,很容易超出其允許的最大索引長度(例如,早期版本默認為767字節(jié),后續(xù)版本如MySQL 5.7及以后,當innodb_large_prefix
開啟時可達3072字節(jié)),這將導致索引創(chuàng)建失敗。
針對超長字段的索引需求,有幾種常見的替代方案:
一種是前綴索引,即僅對字段的前N個字符建立索引。然而,前綴索引的選擇性(區(qū)分度)可能遠不如全字段索引,尤其當字段內容的前綴部分重復度較高時,其提升查詢性能的效果有限。
另一種是使用哈希索引的思路,可以對超長字段內容計算哈希值,并將該哈希值存儲在獨立列上建立索引。這種方法能有效減少索引存儲空間并提升查詢速度,但其局限性在于無法支持范圍查詢,且存在哈希碰撞的可能(盡管概率較低)。
如果核心需求是對文本內容進行搜索,那么全文索引(Full-Text Index)是更合適的選擇。它專為大文本內容的關鍵詞搜索場景設計和優(yōu)化,能夠高效地執(zhí)行基于詞匯的查找。然而,全文索引主要適用于關鍵詞檢索,對于精確匹配或基于排序的查詢場景則不適用。
222. Kafka生產(chǎn)者ACK機制、消息重試與順序性保障
Kafka生產(chǎn)者在發(fā)送消息時,其行為高度依賴于acks
參數(shù)的配置,該參數(shù)決定了生產(chǎn)者在認定消息發(fā)送成功前需要等待多少個副本的確認(ACK)。若生產(chǎn)者發(fā)送五條消息(例如編號1,2,3,4,5)后僅收到部分確認(例如針對1,2,3,5的ACK,缺少了第4條消息的ACK),其后續(xù)處理邏輯將取決于以下因素:
首先是acks
參數(shù)的配置。若**acks=0
,生產(chǎn)者不等待任何來自Broker的確認,消息發(fā)送請求一旦發(fā)出即被視為成功**,這種模式下無法保證消息實際到達Kafka集群,可靠性最低但延遲最小。若**acks=1
(默認值),生產(chǎn)者需等待分區(qū)leader副本成功寫入消息并返回確認后才認為發(fā)送成功**;此模式下,若leader在確認后但在數(shù)據(jù)同步到其他ISR(In-Sync Replicas)副本前宕機,消息可能丟失。若**acks=all
(或等效的-1
),生產(chǎn)者需等待消息被分區(qū)leader寫入并且所有ISR副本都成功同步數(shù)據(jù)后,才認為發(fā)送成功**,這種配置提供了最高的數(shù)據(jù)持久性保障。
其次是消息重試機制。如果生產(chǎn)者配置了retries
參數(shù)大于0(默認情況下,新版本Kafka生產(chǎn)者通常有較大的默認重試次數(shù)),對于未收到預期確認的消息(例如上述場景中未收到第4條消息的ACK),生產(chǎn)者會自動嘗試重新發(fā)送這條消息。生產(chǎn)者內部會維護一個發(fā)送緩沖區(qū),并根據(jù)收到的ACK情況判斷哪些消息需要重發(fā),直至達到最大重試次數(shù)或消息最終被成功確認。
如果消息(如示例中的第4條)確實未能成功發(fā)送,或者其ACK在網(wǎng)絡傳輸中丟失,且在配置的retries
次數(shù)耗盡后仍未收到確認(當acks
配置為1
或all
時),生產(chǎn)者通常會拋出如TimeoutException
或特定可重試/不可重試異常。應用程序此時需要捕獲這些異常并進行相應處理,例如記錄錯誤日志、告警或執(zhí)行更復雜的補償邏輯。
消息重試確實可能導致Kafka中出現(xiàn)重復消息。 當生產(chǎn)者發(fā)送消息后未能在預期時間內收到確認(可能由于網(wǎng)絡波動或Broker短暫不可用),它會根據(jù)retries
配置進行重發(fā)。如果原始消息實際上已成功寫入Broker,但其ACK未能送達生產(chǎn)者,后續(xù)的重試便會導致同一條消息在Broker上存儲多個副本,從而產(chǎn)生重復消息。
為解決重復消息問題,Kafka引入了冪等生產(chǎn)者(Idempotent Producer)。通過設置enable.idempotence=true
(自Kafka 0.11.0版本起),生產(chǎn)者能夠自動處理重試可能引發(fā)的重復寫入問題。冪等性確保了即使在發(fā)生網(wǎng)絡故障或重試的情況下,每條消息在目標主題的單個分區(qū)內也只會被精確寫入一次。 Kafka的冪等性實現(xiàn)依賴于兩個關鍵組件:每個生產(chǎn)者實例會被分配一個唯一的生產(chǎn)者ID(Producer ID, PID),并且對于發(fā)送到特定主題分區(qū)的每條消息,生產(chǎn)者會附加一個單調遞增的序列號(Sequence Number)。Broker端會記錄每個PID在該分區(qū)已成功寫入的最新序列號。當收到新消息時,Broker會檢查其PID和序列號,從而確保每條消息在單個分區(qū)內只被精確寫入一次。
另外,消費者端也可以實現(xiàn)去重邏輯,例如基于消息的唯一業(yè)務標識符(如訂單ID、用戶ID等,通常通過消息的key或value中的特定字段傳遞)進行判斷和過濾。對于更復雜的場景,Kafka還提供了事務性生產(chǎn)者(Transactional Producer),它允許將一系列消息的發(fā)送操作以及可能的偏移量提交操作捆綁成一個原子事務,從而實現(xiàn)精確一次(Exactly-Once Semantics, EOS)的處理。
關于消息的順序性,Kafka本身在單個分區(qū)內保證消息的有序性,即消息按照生產(chǎn)者發(fā)送的順序被寫入分區(qū),消費者也按照這個順序從分區(qū)讀取消息。為確保生產(chǎn)者發(fā)送端的順序性在重試等情況下不被打亂:啟用冪等性時,序列號機制有助于維持順序并防止亂序插入。更嚴格地,可以將生產(chǎn)者參數(shù)**max.in.flight.requests.per.connection
設置為1,確保在任何時刻每個連接上只有一個未確認的請求批次在飛行。這可以防止因重試導致的消息批次亂序,但會犧牲一定的吞吐量。** 此外,通過為具有相同業(yè)務關聯(lián)性的消息設置相同的Key,并使用默認或基于Key的分區(qū)策略,可以確保這些消息被發(fā)送到同一個分區(qū),從而保證了這些特定消息在消費時的相對順序。
223. 自定義棧類 CustomStack
的實現(xiàn)(支持 inc
操作)
題目要求實現(xiàn)一個自定義棧類 CustomStack
,它具有以下功能:
CustomStack(int maxSize)
:構造函數(shù),用maxSize
初始化棧對象,棧中最多能容納maxSize
個元素。棧滿后,push
操作無效。void push(int x)
:如果棧未滿,將x
添加到棧頂。int pop()
:彈出棧頂元素并返回其值。如果棧為空,返回 -1。此處的返回值需要考慮inc
操作的影響。void inc(int k, int val)
:將棧底的k
個元素的值都增加val
。如果棧中元素數(shù)量小于k
,則棧中所有元素的值都增加val
。
該實現(xiàn)的核心在于巧妙地使用了一個輔助數(shù)組 increment
來記錄對棧元素懶惰增加的值。increment[i]
存儲了應該僅僅累加到 stack[i]
上的增量值,但這個增量在 pop
時會影響到其下方的元素。
當執(zhí)行 push(int x)
操作時,如果棧未滿(top < maxSize - 1
),則將元素 x
存入 stack[++top]
。
當執(zhí)行 inc(int k, int val)
操作時,確定實際影響的元素范圍。增量 val
應該施加在從棧底數(shù)起的前 k
個元素中,實際存在的那些元素上。因此,找到這 k
個元素中最頂部的那個元素的索引 index = Math.min(k, top + 1) - 1
。如果 index
合法(即 index >= 0
),則 increment[index] += val
。這意味著,當索引為 index
的元素被彈出時,這個 val
會作為其增量的一部分,并且這個 val
也會在 pop
操作中被傳遞給 index-1
的元素。
當執(zhí)行 pop()
操作時,首先檢查棧是否為空(top == -1
),若為空則返回 -1。否則,棧頂元素的真實值是 stack[top] + increment[top]
。關鍵在于,當棧頂元素 stack[top]
被彈出時,原本施加在其上的增量 increment[top]
需要被“傳遞”給它下面的元素 stack[top-1]
,因為 increment[top]
意味著 stack[0]
到 stack[top]
都受到了這個(以及可能的其他)增量的影響。因此,如果 top > 0
,則執(zhí)行 increment[top-1] += increment[top]
。然后,將 increment[top]
清零,top
指針減一,并返回計算出的棧頂元素值。
class CustomStack { private int[] stack; private int[] increment; // increment[i] 表示對 stack[0]...stack[i] 的額外增量,但在此實現(xiàn)中,它表示對stack[i]的直接增量,并通過pop傳遞 private int maxSize; private int top; // 指向棧頂元素的索引 public CustomStack(int maxSize) { this.maxSize = maxSize; this.stack = new int[maxSize]; this.increment = new int[maxSize]; // 用于記錄懶惰增加的值 this.top = -1; // 初始化棧為空 } public void push(int x) { if (top < maxSize - 1) { // 檢查棧是否已滿 top++; stack[top] = x; // increment[top] 默認為 0,在新元素壓棧時不需要特殊處理 } } public int pop() { if (top == -1) { // 檢查棧是否為空 return -1; } // 計算棧頂元素的實際值,即原始值加上其累積的增量 int result = stack[top] + increment[top]; // 將當前棧頂位置的增量傳遞給下一個位置(即棧中的下一個元素) // 這是因為 increment[top] 的效果是施加于 stack[top] 及其以下所有元素的 // 當 stack[top] 被彈出后,這個增量效果需要由 stack[top-1] 繼續(xù)承載 if (top > 0) { increment[top - 1] += increment[top]; } // 重置當前棧頂位置的增量,因為它已經(jīng)被計算并傳遞 increment[top] = 0; // 更新棧頂指針 top--; return result; } public void inc(int k, int val) { // 確定增量操作實際影響的棧頂元素的索引 // k 是從棧底開始計數(shù),最多影響 k 個元素,或棧中所有元素 // top + 1 是當前棧中元素的數(shù)量 int limit = Math.min(k, top + 1); if (limit > 0) { // 增量施加在從棧底數(shù)起第 limit 個元素上(0-indexed) increment[limit - 1] += val; } } }
224. 查找二叉樹中與目標節(jié)點距離為K的所有節(jié)點
給定一個二叉樹的根節(jié)點 root
,一個樹中的目標節(jié)點 target
,以及一個整數(shù) k
。任務是找出并返回一個包含所有與 target
節(jié)點距離為 k
的其他節(jié)點值的列表。返回的節(jié)點值順序不限。
解決此問題的核心思路是將樹看作一個無向圖,然后從 target
節(jié)點開始執(zhí)行廣度優(yōu)先搜索(BFS)來找到距離為 k
的所有節(jié)點。
- 構建父節(jié)點映射:首先,需要遍歷整個二叉樹,為每個節(jié)點記錄其父節(jié)點。這可以通過一次深度優(yōu)先搜索(DFS)完成,并將映射關系存儲在哈希表 parentMap 中,鍵為子節(jié)點,值為父節(jié)點。這一步是為了能夠在BFS中向上(向父節(jié)點)移動。
- 廣度優(yōu)先搜索(BFS):從 target 節(jié)點開始進行BFS。使用一個隊列 queue 來存儲待訪問的節(jié)點,一個集合 visited 來記錄已訪問過的節(jié)點以避免重復訪問和形成環(huán)路(因為現(xiàn)在可以訪問父節(jié)點)。初始時,將 target 節(jié)點加入隊列和 visited 集合。
- 距離控制與結果收集:BFS按層進行。維護一個 currentDistance 變量,初始為0。在每一輪(層)的BFS中:如果 currentDistance == k,則當前隊列中的所有節(jié)點即為所求。將這些節(jié)點的值加入結果列表 result,然后可以終止搜索并返回 result。如果 currentDistance < k,則處理當前層的所有節(jié)點:從隊列中逐個取出節(jié)點,將其未被訪問過的左子節(jié)點、右子節(jié)點以及父節(jié)點(通過 parentMap 查找)加入隊列,并標記為已訪問。處理完一層節(jié)點后,currentDistance 增加1。
- 終止條件:如果隊列變空而 currentDistance 仍未達到 k,或者 k=0 時直接返回 target 節(jié)點的值(根據(jù)題目,這里k代表距離,所以k=0時結果就是target本身的值)。
import java.util.*; class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(int x) { val = x; } } class Solution { private Map<TreeNode, TreeNode> parentMap = new HashMap<>(); public List<Integer> distanceK(TreeNode root, TreeNode target, int k) { List<Integer> result = new ArrayList<>(); if (root == null || target == null) { return result; } // 1. 構建父節(jié)點映射 buildParentMap(root, null); // 2. 從 target 節(jié)點開始 BFS Queue<TreeNode> queue = new LinkedList<>(); Set<TreeNode> visited = new HashSet<>(); queue.add(target); visited.add(target); int currentDistance = 0; while (!queue.isEmpty()) { int levelSize = queue.size(); // 3. 距離控制與結果收集 if (currentDistance == k) { for (int i = 0; i < levelSize; i++) { result.add(queue.poll().val); } return result; // 找到所有距離為 k 的節(jié)點,直接返回 } for (int i = 0; i < levelSize; i++) { TreeNode currentNode = queue.poll(); // 訪問左子節(jié)點 if (currentNode.left != null && !visited.contains(currentNode.left)) { queue.add(currentNode.left); visited.add(currentNode.left); } // 訪問右子節(jié)點 if (currentNode.right != null && !visited.contains(currentNode.right)) { queue.add(currentNode.right); visited.add(currentNode.right); } // 訪問父節(jié)點 TreeNode parentNode = parentMap.get(currentNode); if (parentNode != null && !visited.contains(parentNode)) { queue.add(parentNode); visited.add(parentNode); } } currentDistance++; } return result; // 如果 BFS 結束仍未找到(例如 k 過大) } private void buildParentMap(TreeNode node, TreeNode parent) { if (node != null) { parentMap.put(node, parent); buildParentMap(node.left, node); buildParentMap(node.right, node); } } }
229. 優(yōu)化Redis客戶端連接過多導致的性能瓶頸
當Redis面臨過多客戶端連接時,確實會消耗大量服務器資源(如內存、CPU、文件描述符),并可能導致查詢性能不穩(wěn)定甚至服務不可用。以下是一些關鍵的優(yōu)化思路:
在客戶端應用程序中采用連接池技術是首要的優(yōu)化手段。連接池能夠復用已建立的TCP連接,避免了每次請求都進行新建連接和斷開連接的開銷,從而顯著減少Redis服務器實際承載的并發(fā)連接數(shù)量,并提升應用響應速度。應合理配置連接池參數(shù),如最大連接數(shù)、最小空閑連接數(shù)、獲取連接的超時時間等,以平衡資源利用率和系統(tǒng)負載。
在Redis服務端,通過maxclients
配置項可以限制服務器允許的最大客戶端連接總數(shù)。此參數(shù)應根據(jù)服務器硬件資源(特別是內存,因為每個連接都會消耗內存)和預期的并發(fā)訪問量進行設定,以防止連接數(shù)超出Redis處理能力上限而導致性能急劇下降或服務不可用。
當單個Redis實例無法滿足連接或負載需求時,可以考慮采用分片或集群架構。通過將數(shù)據(jù)分散到多個Redis實例(手動分片或使用如Codis、Twemproxy等中間件)或部署官方的Redis Cluster,可以將連接請求和數(shù)據(jù)負載均攤到不同節(jié)點上,從而橫向擴展系統(tǒng)的連接承載能力和整體吞吐量。
利用Redis的Pipeline(管道)特性,客戶端可以將多條命令一次性打包發(fā)送給服務器,服務器處理完畢后再將結果一并返回。這能大幅減少網(wǎng)絡往返次數(shù)(RTT),從而在不顯著增加連接數(shù)的情況下提高操作吞吐量。 類似地,應盡可能使用Redis提供的批量操作命令(如MGET
, MSET
, HMGET
, HMSET
等)來合并多個相關的讀寫請求,減少單次操作的連接交互頻率。
持續(xù)監(jiān)控Redis的連接狀況至關重要。通過INFO
同時,在Redis服務端配置**參數(shù),可以使服務器自動關閉長時間處于空閑狀態(tài)的客戶端連接**,從而回收連接資源。
剩余60%內容,訂閱專欄后可繼續(xù)查看/也可單篇購買
曾獲多國內大廠的 ssp 秋招 offer,且是Java5年的沉淀老兵(不是)。專注后端高頻面試與八股知識點,內容系統(tǒng)詳實,覆蓋約 30 萬字面試真題解析、近 400 個熱點問題(包含大量場景題),60 萬字后端核心知識(含計網(wǎng)、操作系統(tǒng)、數(shù)據(jù)庫、性能調優(yōu)等)。同時提供簡歷優(yōu)化、HR 問題應對、自我介紹等通用能力??紤]到歷史格式混亂、質量較低、也在本地積累了大量資料,故準備從頭重構專欄全部內容