腾讯约面了,真的难,感觉已经凉了

0 阅读16分钟

好久没有给大家分享面经了,今天给大家带来一份热乎的最新腾讯面经。

这是咱们训练营的一位朋友最近刚面的,看完之后感觉确实有难度,很多问题都问得很深,不仅考察基础,还考验底层原理和实际场景的思考。

大家可以看看自己能答出多少:

1. 键入一个域名,整体怎么做流转的,要很详细

当你在浏览器输入一个域名(比如www.baidu.com),整个流程大概是这样的:

  1. 本地缓存查询:浏览器先查自己的缓存(比如 Chrome 的 DNS 缓存),看看有没有这个域名对应的 IP。如果有,直接用这个 IP;没有就往下走。
  2. 操作系统缓存查询:浏览器查不到,会问操作系统(比如 Windows 的 hosts 文件或系统 DNS 缓存),如果操作系统有记录,直接返回 IP。
  3. 路由器缓存查询:还没有的话,请求会发到家里或公司的路由器,路由器也有自己的 DNS 缓存,有就直接返回。
  4. DNS 服务器迭代查询:前面都没命中,就会向 ISP(网络服务提供商,比如联通、电信)的本地 DNS 服务器发起请求。本地 DNS 服务器如果有缓存就返回,没有的话就开始 “迭代查询”:
    • 先问 “根域名服务器”(全球共 13 组):.com这个顶级域名由哪些服务器管理?
    • 根服务器返回.com顶级域名服务器的 IP。
    • 本地 DNS 再问.com服务器:baidu.com这个二级域名由哪些服务器管理?
    • .com服务器返回baidu.com的权威 DNS 服务器 IP。
    • 本地 DNS 最后问baidu.com的权威 DNS 服务器:www.baidu.com对应的 IP 是什么?权威服务器返回具体 IP(比如180.101.50.242)。
  5. 建立 TCP 连接:浏览器拿到 IP 后,通过 TCP 三次握手和服务器建立连接(如果是 HTTPS,还要多一步 TLS 握手,协商加密方式、交换密钥)。
  6. 发送 HTTP 请求:连接建立后,浏览器发送 HTTP 请求(包括请求行、请求头、请求体),比如GET /index.html HTTP/1.1
  7. 服务器处理请求:服务器收到请求后,解析请求内容,找到对应的资源(比如静态页面、接口数据),处理后生成 HTTP 响应(状态码、响应头、响应体)。
  8. 返回响应并渲染:服务器把响应通过 TCP 连接发回浏览器,浏览器接收后解析 HTML、CSS、JavaScript,渲染页面,展示给用户。
  9. 关闭连接:如果是 HTTP/1.1 的Connection: close,数据传输完就断开 TCP 连接;如果是keep-alive,连接会保持一段时间,方便后续请求复用。

回答示例: “整个流程大概分为网络解析、建立连接、数据传输和页面渲染四个大阶段。首先是 DNS 解析,会依次从浏览器、操作系统、路由器查缓存,都没有则会向本地 DNS 服务器发起迭代查询。拿到 IP 后,客户端与服务端进行 TCP 三次握手,如果是 HTTPS 还会进行 TLS 握手协商密钥。连接建立后,浏览器发送 HTTP 请求,服务端处理并返回 HTTP 响应。最后浏览器解析 HTML/CSS/JS 进行渲染,同时根据 keep-alive 策略决定是否保留 TCP 连接。”

2. 线上系统出现了 OOM,如果是你,你会怎么排查?请说出详细的工具和排查思路

线上 OOM(Out Of Memory)排查是考察工程能力的一道经典难题,核心思路是保护现场、分析 Dump 文件、定位代码:

  1. 保护线上服务:首先要把出问题的节点摘除流量,或者重启服务,优先保证业务可用性,防止影响扩大。
  2. 获取现场数据
    • 检查是否有配置 -XX:+HeapDumpOnOutOfMemoryError,如果有,系统在 OOM 时会自动生成一份 Dump 文件。
    • 如果没有,且服务还在僵死状态,可以使用 jmap -dump:format=b,file=heap.hprof <pid> 手动导出。
  3. 分析 Dump 文件
    • 将 Dump 文件下载到本地,使用 MAT(Memory Analyzer Tool)或 JProfiler 打开。
    • 查看 Histogram 或 Dominator Tree,找出占用内存最大的对象。
    • 分析这些大对象的 GC Roots 引用链,看是哪些类持有了它们导致无法回收。
  4. 结合代码和监控定位
    • 确认是内存泄漏(对象无限增加无法回收,如 ThreadLocal 未清理)还是突发流量(如一次性查出几十万条数据)。
    • 结合当时的 CPU 监控、慢 SQL 日志以及业务链路进行代码层面的修复。

回答示例: “如果遇到线上 OOM,我的第一反应是先保护线上服务,通过重启或者摘除流量来恢复。接着我会去拿到现场的 Dump 文件。排查的话,我一般分为三步:第一步,看监控大盘,确认是内存泄露还是突发的内存飙升;第二步,如果配置了 OOM 自动 Dump 参数,我会用 MAT 分析 dump 文件,看哪个对象占用了最大内存,以及它的 GC Roots 引用链;第三步,结合代码定位。如果是突发流量导致的,我可能还会分析当时的访问日志和慢 SQL。对于 Go 语言的话,我会直接拉取 pprof 的 heap 数据,重点看 inuse_space。”

3. 尝试推导 redis 是怎么做分布式的,如何保证写入相同数据库,即使某些库发生了崩溃,数据仍然存在

Redis 的分布式方案,核心是 “分片” 和 “高可用”,保证数据一致性和可靠性:

  1. 分布式架构基础
    • 分片(Sharding):把数据分散到多个 Redis 实例,避免单实例压力过大。比如用 “哈希槽”(Redis Cluster 默认 16384 个槽),每个 key 通过CRC16(key) % 16384计算属于哪个槽,每个实例负责一部分槽。
    • 主从复制:每个主节点(Master)有多个从节点(Slave),从节点通过SYNC命令复制主节点的数据,实现读写分离(读请求可以走从节点)。
  2. 保证写入相同数据库(数据一致性)
    • 客户端写入时,会根据 key 的哈希槽找到对应的主节点,直接写入主节点,再由主节点异步同步给从节点(默认)。如果需要强一致性,可以用WAIT命令,等待至少 N 个从节点确认收到数据后再返回。
  3. 崩溃后数据仍存在
    • 持久化:通过 RDB(定时快照)和 AOF( Append Only File,记录每一条写命令)把内存数据存到磁盘,崩溃后重启可以恢复。
    • 哨兵(Sentinel):监控主从节点状态,当主节点崩溃,哨兵会从从节点中选举新的主节点,自动完成故障转移,保证服务不中断。
    • 集群模式:Redis Cluster 中每个槽至少有 1 个主节点和 1 个从节点,主节点挂了,从节点会晋升为主节点,数据不会丢失(只要不是所有副本都挂了)。

回答示例: “关于 Redis 的分布式和高可用,我认为核心机制可以拆解为分片、复制和哨兵/集群机制。分片层面,Redis Cluster 引入了 16384 个哈希槽,客户端通过 CRC16 计算 Key 落入哪个槽,从而定位到具体的主节点;数据复制层面,主节点会异步将命令流同步给从节点,保障数据冗余;在崩溃恢复层面,如果主节点宕机,集群内的节点会通过类似 Raft 的选举机制,将拥有最新数据的从节点提升为新主节点,同时依靠 RDB 和 AOF 持久化机制,即便整个机房断电也能在重启后最大程度恢复数据。”

4. Raft 协议里面为什么是 n/2+1 认为 ok?在网络分区的极端情况下,Raft 是如何避免脑裂的?

Raft 是分布式系统里的共识算法,核心是通过 “多数派”(n/2+1)来保证一致性,原因很简单:避免 “分裂脑”(split brain),确保只有一个合法的领导者(Leader)

  1. 为什么是 n/2+1:如果集群有 3 个节点,n/2+1=2。只要有 2 个节点同意,就能达成共识。如果有 2 个节点分别认为自己是 Leader,它们都需要争取多数派支持,但 3 个节点里最多只有 1 个能拿到 2 票(多数),另一个最多拿 1 票,所以不会出现两个 Leader 同时合法的情况。
  2. 网络分区与任期(Term)机制:在网络分区时,少数派所在的分区可能会不断发起选举,导致其任期(Term)不断增加。但因为它拿不到多数派的票,永远无法成为 Leader。当网络恢复后,原来的老 Leader 发现对方拥有更高的 Term,会自动退化为 Follower,从而保证了整个系统的强一致性。

回答示例: “Raft 协议采用多数派(n/2+1)的核心目的就是为了防止脑裂(Split Brain)。因为在一个集群中,任意两个多数派必定存在至少一个交集节点,这保证了同一任期内绝对不可能选出两个 Leader。如果是极端网络分区导致少数派不断发起选举,它的 Term 会不断增加,但因为拿不到多数派选票,永远无法成为 Leader;当网络恢复时,原来的老 Leader 发现对方拥有更高的 Term,会自动退化为 Follower,从而保证了整个系统的强一致性。”

5. a 函数调用 b 函数,汇编角度怎么发生的

从汇编角度看,函数调用本质是 “栈操作 + 跳转”,步骤大概是这样(以 x86 架构为例):

  1. 准备参数:把 b 函数需要的参数,按调用约定(比如 cdecl 是从右到左)压入栈中(push arg3; push arg2; push arg1)。
  2. 调用 b 函数:执行call b指令,这个指令会做两件事:
    • 把当前指令的下一条地址(返回地址,也就是 a 函数调用 b 之后要执行的代码位置)压入栈中;
    • 跳转到 b 函数的入口地址(即 b 函数的第一条指令)。
  3. b 函数的 “序言”(Prologue):进入 b 函数后,先保存当前栈帧,为局部变量腾空间:
    • push ebp:把 a 函数的栈基址(ebp 寄存器)压入栈,保存上下文;
    • mov ebp, esp:把当前栈顶(esp 寄存器)的值赋给 ebp,作为 b 函数的栈基址;
    • sub esp, N:从栈顶往下挪 N 个字节,给 b 函数的局部变量分配空间。
  4. 执行 b 函数逻辑:执行 b 函数的具体代码,可能会读写局部变量(通过 ebp 偏移访问,比如mov eax, [ebp-4])、调用其他函数等。
  5. b 函数的 “尾声”(Epilogue):执行完后,恢复栈帧,准备返回:
    • mov esp, ebp:把栈顶恢复到 b 函数的栈基址,释放局部变量空间;
    • pop ebp:把之前保存的 a 函数的 ebp 弹回寄存器,恢复 a 函数的栈帧;
  6. 返回 a 函数:执行ret指令,从栈顶弹出之前保存的 “返回地址”,然后跳转到这个地址,继续执行 a 函数剩下的代码。

回答示例: “从底层汇编来看,函数调用的本质是栈帧的切换。当 a 调用 b 时,首先会把 b 需要的参数按调用约定压栈,然后执行 call 指令,这会把 a 的下一条指令地址(返回地址)压栈,并跳转到 b。进入 b 之后,第一步是序言(Prologue),也就是把 a 的栈基址 ebp 压栈保存,并把当前 esp 赋值给 ebp,接着给 b 的局部变量分配空间。b 执行完后,通过尾声(Epilogue)恢复 esp 和 ebp,最后执行 ret 指令弹出返回地址,程序就回到了 a 函数继续往下执行。”

6. MySQL 可重复读(RR)隔离级别下,幻读真的被完全解决了吗?请结合 MVCC 和 Next-Key Lock 详细说明

这是一道非常有深度的 MySQL 底层题。严格来说,MySQL 的 RR 级别并没有 100% 解决幻读

  1. 快照读(普通 SELECT):MySQL 通过 MVCC(多版本并发控制)解决了幻读。事务开启后,第一次查询会生成一个 Read View,之后的查询都基于这个 Read View,即使其他事务插入了新数据,当前事务也看不到,避免了幻读。
  2. 当前读(UPDATE/DELETE/FOR UPDATE):MySQL 通过 Next-Key Lock(记录锁 + 间隙锁)解决了幻读。当执行当前读时,会锁住扫描到的索引记录以及记录之间的间隙,其他事务无法在这个间隙插入新数据,从而避免了幻读。
  3. 幻读依然发生的极端场景:事务 A 先执行快照读,此时事务 B 插入了一条新数据并提交。接着事务 A 对这条新插入的数据执行了 UPDATE 操作(UPDATE 是当前读,能看到最新数据,且更新成功后会将其隐藏列的事务 ID 改为 A 的事务 ID)。此时,事务 A 再次执行快照读,就会把刚才 B 插入的数据查出来,这就发生了幻读现象。

回答示例: “严格来说,MySQL 的 RR 级别并没有100%解决幻读。对于普通的 SELECT(快照读),MySQL 通过 MVCC 机制,利用 Read View 保证了每次读到的都是事务开启时的快照,避免了幻读;对于加锁的 SELECT、UPDATE(当前读),MySQL 通过 Next-Key Lock(记录锁+间隙锁)锁住了范围,防止其他事务插入,也避免了幻读。但是有一种极端情况:事务 A 先进行普通 SELECT,事务 B 插入了一条新数据并提交,接着事务 A 对这条新数据执行了 UPDATE 操作,由于 UPDATE 是当前读,会更新成功,此时事务 A 再次执行普通 SELECT,就会把刚才 B 插入的数据查出来,这就发生了幻读现象。”

7. 两张一亿条的 excel 表,主键相同,怎么合并写入磁盘

一亿条数据太大,内存肯定装不下,核心思路是流式处理 + 外部排序,步骤如下:

  1. 格式转换:Excel 文件本身不适合处理超大数据,先转成 CSV(纯文本,读写快),用工具(如 Python 的 pandas、Java 的 POI)按行读取,避免加载全部数据。
  2. 分块排序:把每张表分成多个小块(比如每个块 100 万条),加载到内存后按主键排序,然后写入临时文件(比如表 1 的临时块tmp1_1.csvtmp1_2.csv,表 2 的tmp2_1.csvtmp2_2.csv)。
  3. 合并排序后的块:对表 1 的所有临时块做 “多路归并排序”,得到一个全局按主键排序的表 1(sorted1.csv);同理处理表 2 得到sorted2.csv
  4. 双指针合并:同时打开 sorted1.csvsorted2.csv,用两个指针分别流式读取记录,比较主键:
    • 主键相同:合并两行数据,写入结果文件;
    • 主键不同:把小的那个主键的记录写入结果文件,移动对应指针;
  5. 写入磁盘:结果文件按批次写入(比如每 10 万条刷一次盘),避免频繁 IO,最后可以转成 Excel 或保留 CSV(看需求)。

回答示例: “一亿条数据全部加载到内存肯定会导致 OOM,这种海量数据处理的核心思路是‘分块处理 + 外部排序’。我的方案是:首先将 Excel 转为更易流式读取的 CSV 格式。然后把两个大文件分别切分成比如 100 个小文件,每个小文件在内存中按主键排序后写入磁盘。接着对这些小文件进行多路归并排序,合并成两个全局有序的大文件。最后,利用双指针法,同时打开这两个有序的大文件,逐行读取对比主键,主键相同的进行合并写入最终文件,这样整个过程内存只占用很小一部分,完美解决。”

8. pagecache 是什么,好处和坏处?如何绕过 pagecahce 直接写入磁盘

PageCache(页缓存) 是操作系统在内存中开辟的一块区域,用来缓存磁盘上的文件数据。程序读写文件时,先和 PageCache 交互,再由操作系统异步把 PageCache 的数据刷到磁盘。

好处

  • 速度快:内存读写比磁盘快 1000 倍以上,重复读写同一数据时,直接从 PageCache 取,减少磁盘 IO;
  • 优化 IO:操作系统会合并多个小写操作(比如多次写 1KB,合并成一次写 4KB),减少磁盘寻道次数;
  • 顺序读写友好:PageCache 会预读相邻数据(比如读了第 1 页,自动预读第 2、3 页),适合日志、视频等顺序访问场景。

坏处

  • 占用内存:如果缓存太多,会挤掉应用程序的内存,导致频繁 GC 或换页(swap);
  • 数据丢失风险:数据存在 PageCache 中但没刷到磁盘时,突然断电会丢失;
  • 写延迟:异步刷盘可能导致数据在内存中滞留,需要主动调用fsync才能保证持久化。

绕过 PageCache 的方法

  • Linux 下打开文件时用O_DIRECT标志(直接 IO),比如open("file.txt", O_WRONLY | O_DIRECT),数据直接写入磁盘,不经过 PageCache;
  • 数据库常用这种方式(比如 MySQL 的innodb_flush_method=O_DIRECT),避免 PageCache 和数据库自身缓存(如 InnoDB Buffer Pool)重复缓存数据;

回答示例: “PageCache 是操作系统层面用于缓存文件数据的内存区域。它的好处是极大地提升了文件读写性能,将随机写优化为顺序写,并且有预读机制;但坏处是会占用系统可用内存,而且由于是异步刷盘,掉电时容易丢失数据。在一些对数据一致性要求极高、或者数据库本身已经实现了完善的缓冲池(比如 MySQL 的 Buffer Pool)的场景下,我们会希望绕过 PageCache,避免双重缓存带来的内存浪费。在 Linux 下,我们可以在 open 文件时加上 O_DIRECT 标志位来实现 Direct IO,直接把数据写入磁盘。”

9. 算法题

(1)K 个一组翻转链表(LeetCode 25 - Hard难度) (2)实现 LFU 缓存机制(LeetCode 460 - Hard难度)

END

写在最后:

最近私信问我面试题的小伙伴实在太多了,一个个回有点回不过来。

我花了两个周末,把星球里大家公认最容易挂的 Go/Java/AI 面试坑点 整理成了一份 PDF 文档。里面不光有题,还有解题思路和避坑指南。

想要的同学,直接关注并私信我 【面试】,我统一发给大家。

wangzhongyang.com 也欢迎大家直接访问我的官网,里面有Go / Java / AI 的资料,免费学习