腾讯提前批是真难

前面几天分享了拼多多面经,有些朋友觉得挺简单的;今天分享一下腾讯提前批的面经,这个有点难度:
键入一个域名,整体怎么做流转的,要很详细
当你在浏览器输入一个域名(比如www.baidu.com
),整个流程大概是这样的:
本地缓存查询:浏览器先查自己的缓存(比如 Chrome 的 DNS 缓存),看看有没有这个域名对应的 IP。如果有,直接用这个 IP;没有就往下走。
操作系统缓存查询:浏览器查不到,会问操作系统(比如 Windows 的 hosts 文件或系统 DNS 缓存),如果操作系统有记录,直接返回 IP。
路由器缓存查询:还没有的话,请求会发到家里或公司的路由器,路由器也有自己的 DNS 缓存,有就直接返回。
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
)。建立 TCP 连接:浏览器拿到 IP 后,通过 TCP 三次握手和服务器建立连接(如果是 HTTPS,还要多一步 TLS 握手,协商加密方式、交换密钥)。
发送 HTTP 请求:连接建立后,浏览器发送 HTTP 请求(包括请求行、请求头、请求体),比如
GET /index.html HTTP/1.1
。服务器处理请求:服务器收到请求后,解析请求内容,找到对应的资源(比如静态页面、接口数据),处理后生成 HTTP 响应(状态码、响应头、响应体)。
返回响应并渲染:服务器把响应通过 TCP 连接发回浏览器,浏览器接收后解析 HTML、CSS、JavaScript,渲染页面,展示给用户。
关闭连接:如果是 HTTP/1.1 的
Connection: close
,数据传输完就断开 TCP 连接;如果是keep-alive
,连接会保持一段时间,方便后续请求复用。
http 协议中,对于粘包问题,我们可以怎么解决?追问:在 http 协议中,怎么判断拆包后组装后是组装完了,而不是一部分
首先得说,HTTP 基于 TCP,而 TCP 本身是 “流式协议”,没有消息边界,所以可能出现粘包(多个请求 / 响应粘在一起)或拆包(一个请求 / 响应被分成多段)。但 HTTP 协议本身设计了机制来解决这个问题:
解决粘包的核心思路:明确告诉接收方 “一个完整的 HTTP 消息有多长”,主要有 3 种方式:
Content-Length:在响应头里加
Content-Length: 1024
,表示响应体大小是 1024 字节。接收方收到这么多字节后,就知道这个消息结束了。分块传输(Transfer-Encoding: chunked):如果响应体大小不确定(比如动态生成的内容),用分块传输。每个块开头会标明当前块的大小(十六进制),最后用一个 “0 长度块”(
0\r\n\r\n
)表示结束。Connection: close:HTTP/1.0 默认用这个,服务器处理完请求后直接关闭 TCP 连接。接收方发现连接断了,就知道消息结束了(但效率低,不推荐)。
追问答案:判断组装完成的依据就是上面这几种机制:
如果有
Content-Length
,接收方累计收到的字节数等于这个值,就说明组装完了;如果是分块传输,收到 “0 长度块” 后,说明整个消息结束;
如果是
Connection: close
,TCP 连接关闭时,就认为消息结束。
尝试推导 redis 是怎么做分布式的,如何保证写入相同数据库,即使某些库发生了崩溃,数据仍然存在
Redis 的分布式方案,核心是 “分片” 和 “高可用”,保证数据一致性和可靠性:
分布式架构基础:
分片(Sharding):把数据分散到多个 Redis 实例,避免单实例压力过大。比如用 “哈希槽”(Redis Cluster 默认 16384 个槽),每个 key 通过
CRC16(key) % 16384
计算属于哪个槽,每个实例负责一部分槽。主从复制:每个主节点(Master)有多个从节点(Slave),从节点通过
SYNC
命令复制主节点的数据,实现读写分离(读请求可以走从节点)。保证写入相同数据库(数据一致性):
客户端写入时,会根据 key 的哈希槽找到对应的主节点,直接写入主节点,再由主节点异步同步给从节点(默认)。如果需要强一致性,可以用
WAIT
命令,等待至少 N 个从节点确认收到数据后再返回。崩溃后数据仍存在:
持久化:通过 RDB(定时快照)和 AOF( Append Only File,记录每一条写命令)把内存数据存到磁盘,崩溃后重启可以恢复。
哨兵(Sentinel):监控主从节点状态,当主节点崩溃,哨兵会从从节点中选举新的主节点,自动完成故障转移,保证服务不中断。
集群模式:Redis Cluster 中每个槽至少有 1 个主节点和 1 个从节点,主节点挂了,从节点会晋升为主节点,数据不会丢失(只要不是所有副本都挂了)。
raft 协议里面为什么是 n/2+1 认为 ok
Raft 是分布式系统里的共识算法,核心是通过 “多数派”(n/2+1)来保证一致性,原因很简单:避免 “分裂脑”(split brain),确保只有一个合法的领导者(Leader)。
举个例子:如果集群有 3 个节点,n/2+1=2。只要有 2 个节点同意,就能达成共识。这时候即使 1 个节点故障,剩下 2 个还能正常工作;如果有 2 个节点分别认为自己是 Leader,它们都需要争取多数派支持,但 3 个节点里最多只有 1 个能拿到 2 票(多数),另一个最多拿 1 票,所以不会出现两个 Leader 同时合法的情况。
再比如 5 个节点,需要 3 票同意。即使 2 个节点故障,剩下 3 个还能工作;且任何情况下,最多只有一个节点能拿到 3 票,保证一致性。如果用 “半数”(n/2),3 个节点只要 2 票,但 2 票可能出现两个节点各拿 2 票的情况(比如网络分区时),导致分裂脑。所以 n/2+1 是最小的 “多数派”,既能容忍部分节点故障,又能避免冲突。
a 函数调用 b 函数,汇编角度怎么发生的
从汇编角度看,函数调用本质是 “栈操作 + 跳转”,步骤大概是这样(以 x86 架构为例):
准备参数:把 b 函数需要的参数,按调用约定(比如 cdecl 是从右到左)压入栈中(
push arg3; push arg2; push arg1
)。调用 b 函数:执行 call b 指令,这个指令会做两件事:
把当前指令的下一条地址(返回地址,也就是 a 函数调用 b 之后要执行的代码位置)压入栈中;
跳转到 b 函数的入口地址(即 b 函数的第一条指令)。
b 函数的 “序言”(Prologue):进入 b 函数后,先保存当前栈帧,为局部变量腾空间:
push ebp
:把 a 函数的栈基址(ebp 寄存器)压入栈,保存上下文;mov ebp, esp
:把当前栈顶(esp 寄存器)的值赋给 ebp,作为 b 函数的栈基址;sub esp, N
:从栈顶往下挪 N 个字节,给 b 函数的局部变量分配空间。执行 b 函数逻辑:执行 b 函数的具体代码,可能会读写局部变量(通过 ebp 偏移访问,比如
mov eax, [ebp-4]
)、调用其他函数等。b 函数的 “尾声”(Epilogue):执行完后,恢复栈帧,准备返回:
mov esp, ebp
:把栈顶恢复到 b 函数的栈基址,释放局部变量空间;pop ebp
:把之前保存的 a 函数的 ebp 弹回寄存器,恢复 a 函数的栈帧;返回 a 函数:执行
ret
指令,从栈顶弹出之前保存的 “返回地址”,然后跳转到这个地址,继续执行 a 函数剩下的代码。
redis push 命令怎么做幂等
Redis 的lpush
、rpush
这些 push 命令默认不是幂等的 —— 重复执行会多次往列表里加元素(比如lpush list 1
执行两次,列表会有两个 1)。要实现幂等,核心是让重复执行的命令产生相同的结果,可以这样做:
给元素加唯一标识:比如每个要 push 的元素带一个唯一 ID(如 UUID),push 前先查列表里有没有这个 ID(用
lrange
遍历或lpos
查找),有就不推,没有再推。但遍历列表效率低,适合短列表。用 Lua 脚本原子执行:把 “检查是否存在 + push” 做成一个 Lua 脚本(Redis 保证脚本原子性),比如:
这样重复调用脚本,只会 push 一次元素。
业务层去重:客户端自己记录已经 push 过的元素 ID,发送前先检查本地记录,避免重复发送。
用集合(Set)代替列表:如果业务允许,用
SADD
(集合的添加命令,本身是幂等的,重复加同一个元素只会存一次),但集合是无序的,适合不需要顺序的场景。
两张一亿条的 excel 表,主键相同,怎么合并写入磁盘
一亿条数据太大,内存肯定装不下,核心思路是流式处理 + 外部排序,步骤如下:
格式转换:Excel 文件本身不适合处理超大数据,先转成 CSV(纯文本,读写快),用工具(如 Python 的 pandas、Java 的 POI)按行读取,避免加载全部数据。
分块排序:
把每张表分成多个小块(比如每个块 100 万条),加载到内存后按主键排序,然后写入临时文件(比如表 1 的临时块
tmp1_1.csv
、tmp1_2.csv
,表 2 的tmp2_1.csv
、tmp2_2.csv
)。合并排序后的块:
对表 1 的所有临时块做 “归并排序”,得到一个全局按主键排序的表 1(
sorted1.csv
);同理处理表 2 得到sorted2.csv
。双指针合并:
同时打开 sorted1.csv 和 sorted2.csv,用两个指针分别读取记录,比较主键:
主键相同:合并两行数据(比如取表 1 的某些字段 + 表 2 的某些字段),写入结果文件;
主键不同:把小的那个主键的记录写入结果文件,移动对应指针;
这样从头到尾流式处理,不需要加载全部数据到内存。
写入磁盘:结果文件按批次写入(比如每 10 万条刷一次盘),避免频繁 IO,最后可以转成 Excel 或保留 CSV(看需求)。
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)重复缓存数据;注意:直接 IO 要求数据对齐(比如按扇区大小对齐),否则会报错,且性能不一定更好(少了缓存优化),适合对数据一致性要求极高的场景(如金融交易)。
设计模式中的开放关闭原则是什么
开放关闭原则(OCP,Open-Closed Principle)是设计模式的核心原则之一,简单说就是:软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。
意思是,当需要新增功能时,应该通过 “扩展已有代码”(比如加新类、新方法)来实现,而不是修改原来已经写好且稳定运行的代码。
举个例子:如果有个Shape
类,原来支持圆形(Circle
)和矩形(Rectangle
)的面积计算。现在要加三角形(Triangle
),按照 OCP,应该新增一个Triangle
类继承Shape
,实现自己的getArea()
方法,而不是去修改Shape
或已有的Circle
、Rectangle
类。
这样做的好处是:减少修改旧代码带来的风险(比如改坏原来的功能),提高代码复用性和可维护性,尤其适合大型项目。
算法:(1)字符串转 16 进制,并且 16 进制转字符串 (2)实现 lru,并且 key.size << value.size
欢迎关注 ❤
我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。
没准能让你能刷到自己意向公司的最新面试题呢。
感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:面试群。
版权声明: 本文为 InfoQ 作者【王中阳Go】的原创文章。
原文链接:【http://xie.infoq.cn/article/682ba0480df34b6490fb825a9】。文章转载请联系作者。
评论