(高频问题)221-240 计算机 Java后端 实习 and 秋招 面试高频问题汇总
221. 超长字段(如TEXT类型)建立索引的不适用性及替代方案
对数据库中的超长字段(如TEXT或BLOB类型)直接建立索引通常是不合适的,这主要源于以下几个方面的原因:
首先,索引本身会占用大量存储空间。索引是为了加速查询而创建的数据结构(例如B+树),其建立和维护会额外占用存储空间。对于TEXT或BLOB等超长字段,其数据体积通常较大,可能达到数百KB甚至数MB。直接对这类字段建立索引,会导致索引本身占据大量存储空间,造成不必要的磁盘资源浪费。
其次,对超长字段建立索引会显著增加索引维护的性能开销。由于数据量巨大,索引结构(如B+树)的节点会变得庞大,导致插入、更新、删除等操作时,B+树的调整成本急剧上升,进而降低整体性能。
此外,索引的查找效率也会受到影响。索引依赖于快速的比较操作,但对超长字段进行比较时,需要扫描大量数据,这会大幅降低比较效率。在B+树索引中,过长的索引字段可能导致B+树层级增加,树结构更深,从而延长特定值的查找时间。超长字段值的变更还可能引发频繁的索引页面分裂或合并,这些操作会带来显著的额外磁盘I/O开销,进一步拖累系统性能。
值得注意的是,MySQL的InnoDB存储引擎对单个索引的大小有限制。直接对TEXT字段建立索引,很容易超出其允许的最大索引长度(例如,早期版本默认为767字节,后续版本如MySQL 5.7及以后,当innodb_large_prefix
开启时可达3072字节),这将导致索引创建失败。
针对超长字段的索引需求,有几种常见的替代方案:
一种是前缀索引,即仅对字段的前N个字符建立索引。然而,前缀索引的选择性(区分度)可能远不如全字段索引,尤其当字段内容的前缀部分重复度较高时,其提升查询性能的效果有限。
另一种是使用哈希索引的思路,可以对超长字段内容计算哈希值,并将该哈希值存储在独立列上建立索引。这种方法能有效减少索引存储空间并提升查询速度,但其局限性在于无法支持范围查询,且存在哈希碰撞的可能(尽管概率较低)。
如果核心需求是对文本内容进行搜索,那么全文索引(Full-Text Index)是更合适的选择。它专为大文本内容的关键词搜索场景设计和优化,能够高效地执行基于词汇的查找。然而,全文索引主要适用于关键词检索,对于精确匹配或基于排序的查询场景则不适用。
222. Kafka生产者ACK机制、消息重试与顺序性保障
Kafka生产者在发送消息时,其行为高度依赖于acks
参数的配置,该参数决定了生产者在认定消息发送成功前需要等待多少个副本的确认(ACK)。若生产者发送五条消息(例如编号1,2,3,4,5)后仅收到部分确认(例如针对1,2,3,5的ACK,缺少了第4条消息的ACK),其后续处理逻辑将取决于以下因素:
首先是acks
参数的配置。若**acks=0
,生产者不等待任何来自Broker的确认,消息发送请求一旦发出即被视为成功**,这种模式下无法保证消息实际到达Kafka集群,可靠性最低但延迟最小。若**acks=1
(默认值),生产者需等待分区leader副本成功写入消息并返回确认后才认为发送成功**;此模式下,若leader在确认后但在数据同步到其他ISR(In-Sync Replicas)副本前宕机,消息可能丢失。若**acks=all
(或等效的-1
),生产者需等待消息被分区leader写入并且所有ISR副本都成功同步数据后,才认为发送成功**,这种配置提供了最高的数据持久性保障。
其次是消息重试机制。如果生产者配置了retries
参数大于0(默认情况下,新版本Kafka生产者通常有较大的默认重试次数),对于未收到预期确认的消息(例如上述场景中未收到第4条消息的ACK),生产者会自动尝试重新发送这条消息。生产者内部会维护一个发送缓冲区,并根据收到的ACK情况判断哪些消息需要重发,直至达到最大重试次数或消息最终被成功确认。
如果消息(如示例中的第4条)确实未能成功发送,或者其ACK在网络传输中丢失,且在配置的retries
次数耗尽后仍未收到确认(当acks
配置为1
或all
时),生产者通常会抛出如TimeoutException
或特定可重试/不可重试异常。应用程序此时需要捕获这些异常并进行相应处理,例如记录错误日志、告警或执行更复杂的补偿逻辑。
消息重试确实可能导致Kafka中出现重复消息。 当生产者发送消息后未能在预期时间内收到确认(可能由于网络波动或Broker短暂不可用),它会根据retries
配置进行重发。如果原始消息实际上已成功写入Broker,但其ACK未能送达生产者,后续的重试便会导致同一条消息在Broker上存储多个副本,从而产生重复消息。
为解决重复消息问题,Kafka引入了幂等生产者(Idempotent Producer)。通过设置enable.idempotence=true
(自Kafka 0.11.0版本起),生产者能够自动处理重试可能引发的重复写入问题。幂等性确保了即使在发生网络故障或重试的情况下,每条消息在目标主题的单个分区内也只会被精确写入一次。 Kafka的幂等性实现依赖于两个关键组件:每个生产者实例会被分配一个唯一的生产者ID(Producer ID, PID),并且对于发送到特定主题分区的每条消息,生产者会附加一个单调递增的序列号(Sequence Number)。Broker端会记录每个PID在该分区已成功写入的最新序列号。当收到新消息时,Broker会检查其PID和序列号,从而确保每条消息在单个分区内只被精确写入一次。
另外,消费者端也可以实现去重逻辑,例如基于消息的唯一业务标识符(如订单ID、用户ID等,通常通过消息的key或value中的特定字段传递)进行判断和过滤。对于更复杂的场景,Kafka还提供了事务性生产者(Transactional Producer),它允许将一系列消息的发送操作以及可能的偏移量提交操作捆绑成一个原子事务,从而实现精确一次(Exactly-Once Semantics, EOS)的处理。
关于消息的顺序性,Kafka本身在单个分区内保证消息的有序性,即消息按照生产者发送的顺序被写入分区,消费者也按照这个顺序从分区读取消息。为确保生产者发送端的顺序性在重试等情况下不被打乱:启用幂等性时,序列号机制有助于维持顺序并防止乱序插入。更严格地,可以将生产者参数**max.in.flight.requests.per.connection
设置为1,确保在任何时刻每个连接上只有一个未确认的请求批次在飞行。这可以防止因重试导致的消息批次乱序,但会牺牲一定的吞吐量。** 此外,通过为具有相同业务关联性的消息设置相同的Key,并使用默认或基于Key的分区策略,可以确保这些消息被发送到同一个分区,从而保证了这些特定消息在消费时的相对顺序。
223. 自定义栈类 CustomStack
的实现(支持 inc
操作)
题目要求实现一个自定义栈类 CustomStack
,它具有以下功能:
CustomStack(int maxSize)
:构造函数,用maxSize
初始化栈对象,栈中最多能容纳maxSize
个元素。栈满后,push
操作无效。void push(int x)
:如果栈未满,将x
添加到栈顶。int pop()
:弹出栈顶元素并返回其值。如果栈为空,返回 -1。此处的返回值需要考虑inc
操作的影响。void inc(int k, int val)
:将栈底的k
个元素的值都增加val
。如果栈中元素数量小于k
,则栈中所有元素的值都增加val
。
该实现的核心在于巧妙地使用了一个辅助数组 increment
来记录对栈元素懒惰增加的值。increment[i]
存储了应该仅仅累加到 stack[i]
上的增量值,但这个增量在 pop
时会影响到其下方的元素。
当执行 push(int x)
操作时,如果栈未满(top < maxSize - 1
),则将元素 x
存入 stack[++top]
。
当执行 inc(int k, int val)
操作时,确定实际影响的元素范围。增量 val
应该施加在从栈底数起的前 k
个元素中,实际存在的那些元素上。因此,找到这 k
个元素中最顶部的那个元素的索引 index = Math.min(k, top + 1) - 1
。如果 index
合法(即 index >= 0
),则 increment[index] += val
。这意味着,当索引为 index
的元素被弹出时,这个 val
会作为其增量的一部分,并且这个 val
也会在 pop
操作中被传递给 index-1
的元素。
当执行 pop()
操作时,首先检查栈是否为空(top == -1
),若为空则返回 -1。否则,栈顶元素的真实值是 stack[top] + increment[top]
。关键在于,当栈顶元素 stack[top]
被弹出时,原本施加在其上的增量 increment[top]
需要被“传递”给它下面的元素 stack[top-1]
,因为 increment[top]
意味着 stack[0]
到 stack[top]
都受到了这个(以及可能的其他)增量的影响。因此,如果 top > 0
,则执行 increment[top-1] += increment[top]
。然后,将 increment[top]
清零,top
指针减一,并返回计算出的栈顶元素值。
class CustomStack { private int[] stack; private int[] increment; // increment[i] 表示对 stack[0]...stack[i] 的额外增量,但在此实现中,它表示对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] 继续承载 if (top > 0) { increment[top - 1] += increment[top]; } // 重置当前栈顶位置的增量,因为它已经被计算并传递 increment[top] = 0; // 更新栈顶指针 top--; return result; } public void inc(int k, int val) { // 确定增量操作实际影响的栈顶元素的索引 // k 是从栈底开始计数,最多影响 k 个元素,或栈中所有元素 // top + 1 是当前栈中元素的数量 int limit = Math.min(k, top + 1); if (limit > 0) { // 增量施加在从栈底数起第 limit 个元素上(0-indexed) increment[limit - 1] += val; } } }
224. 查找二叉树中与目标节点距离为K的所有节点
给定一个二叉树的根节点 root
,一个树中的目标节点 target
,以及一个整数 k
。任务是找出并返回一个包含所有与 target
节点距离为 k
的其他节点值的列表。返回的节点值顺序不限。
解决此问题的核心思路是将树看作一个无向图,然后从 target
节点开始执行广度优先搜索(BFS)来找到距离为 k
的所有节点。
- 构建父节点映射:首先,需要遍历整个二叉树,为每个节点记录其父节点。这可以通过一次深度优先搜索(DFS)完成,并将映射关系存储在哈希表 parentMap 中,键为子节点,值为父节点。这一步是为了能够在BFS中向上(向父节点)移动。
- 广度优先搜索(BFS):从 target 节点开始进行BFS。使用一个队列 queue 来存储待访问的节点,一个集合 visited 来记录已访问过的节点以避免重复访问和形成环路(因为现在可以访问父节点)。初始时,将 target 节点加入队列和 visited 集合。
- 距离控制与结果收集:BFS按层进行。维护一个 currentDistance 变量,初始为0。在每一轮(层)的BFS中:如果 currentDistance == k,则当前队列中的所有节点即为所求。将这些节点的值加入结果列表 result,然后可以终止搜索并返回 result。如果 currentDistance < k,则处理当前层的所有节点:从队列中逐个取出节点,将其未被访问过的左子节点、右子节点以及父节点(通过 parentMap 查找)加入队列,并标记为已访问。处理完一层节点后,currentDistance 增加1。
- 终止条件:如果队列变空而 currentDistance 仍未达到 k,或者 k=0 时直接返回 target 节点的值(根据题目,这里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. 构建父节点映射 buildParentMap(root, null); // 2. 从 target 节点开始 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 的节点,直接返回 } for (int i = 0; i < levelSize; i++) { TreeNode currentNode = queue.poll(); // 访问左子节点 if (currentNode.left != null && !visited.contains(currentNode.left)) { queue.add(currentNode.left); visited.add(currentNode.left); } // 访问右子节点 if (currentNode.right != null && !visited.contains(currentNode.right)) { queue.add(currentNode.right); visited.add(currentNode.right); } // 访问父节点 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. 优化Redis客户端连接过多导致的性能瓶颈
当Redis面临过多客户端连接时,确实会消耗大量服务器资源(如内存、CPU、文件描述符),并可能导致查询性能不稳定甚至服务不可用。以下是一些关键的优化思路:
在客户端应用程序中采用连接池技术是首要的优化手段。连接池能够复用已建立的TCP连接,避免了每次请求都进行新建连接和断开连接的开销,从而显著减少Redis服务器实际承载的并发连接数量,并提升应用响应速度。应合理配置连接池参数,如最大连接数、最小空闲连接数、获取连接的超时时间等,以平衡资源利用率和系统负载。
在Redis服务端,通过maxclients
配置项可以限制服务器允许的最大客户端连接总数。此参数应根据服务器硬件资源(特别是内存,因为每个连接都会消耗内存)和预期的并发访问量进行设定,以防止连接数超出Redis处理能力上限而导致性能急剧下降或服务不可用。
当单个Redis实例无法满足连接或负载需求时,可以考虑采用分片或集群架构。通过将数据分散到多个Redis实例(手动分片或使用如Codis、Twemproxy等中间件)或部署官方的Redis Cluster,可以将连接请求和数据负载均摊到不同节点上,从而横向扩展系统的连接承载能力和整体吞吐量。
利用Redis的Pipeline(管道)特性,客户端可以将多条命令一次性打包发送给服务器,服务器处理完毕后再将结果一并返回。这能大幅减少网络往返次数(RTT),从而在不显著增加连接数的情况下提高操作吞吐量。 类似地,应尽可能使用Redis提供的批量操作命令(如MGET
, MSET
, HMGET
, HMSET
等)来合并多个相关的读写请求,减少单次操作的连接交互频率。
持续监控Redis的连接状况至关重要。通过INFO
同时,在Redis服务端配置**参数,可以使服务器自动关闭长时间处于空闲状态的客户端连接**,从而回收连接资源。
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
曾获多国内大厂的 ssp 秋招 offer,且是Java5年的沉淀老兵(不是)。专注后端高频面试与八股知识点,内容系统详实,覆盖约 30 万字面试真题解析、近 400 个热点问题(包含大量场景题),60 万字后端核心知识(含计网、操作系统、数据库、性能调优等)。同时提供简历优化、HR 问题应对、自我介绍等通用能力。考虑到历史格式混乱、质量较低、也在本地积累了大量资料,故准备从头重构专栏全部内容