面灵AI→

腾讯后台开发一面面经(wxg视频号)

轮次
一面
结果
已挂
时间
2026-09
来源
牛客网

《面试题目》

  1. 手撕 3 道算法题(其中一道考取模版快速幂)。
  2. 自我介绍。
  3. 512M 内存的操作系统,申请 1G 的内存空间,会发生什么?
  4. Linux 上一个进程开了 100 个线程,kill 掉这个进程时,怎么保证 100 个线程都能被 kill 掉?
  5. epoll 用过吗?边缘触发和水平触发有什么区别?ET 模式怎么保证读完所有数据?
  6. TCP 四次挥手的时候,TIME_WAIT 阶段发生了什么?
  7. TIME_WAIT 要等多久?给一个具体时间。
  8. TCP 怎么实现可靠传输?
  9. UDP 在弱网会丢包,有没有什么协议把 TCP 和 UDP 的优点结合起来?为什么有了 TCP 还要有 UDP?
  10. 什么叫连接复用?连接复用下怎么区分是谁的数据?
  11. MySQL 分页查询指定 LIMIT offset 时,为什么越往后越慢?什么情况下会这样?实际项目怎么解决?
  12. 你了解哪些缓存?比如 KV、TTL 等。
  13. Redis 里我想更新数据库和缓存,这个时候 Redis 中的数据怎么办?
  14. 如果是先更新数据库再更新缓存,数据库更新成功但删 Redis 失败了,会怎样?
  15. 假设用消息队列解决删除失败重试的问题:删除命令(值从 A 更新为 B)进了队列,此时突然来了一个把数据库更新为旧值 A 的请求,之后队列里的删除命令才执行、删掉了缓存,这种情况怎么办?
  16. 100 枚硬币,30 枚朝上、70 枚朝下,怎么分成两堆,让两堆中朝下的硬币数量相同?可以反转硬币。
  17. 25 匹马、5 个跑道,每次比赛只能拿到排名,最少多少次能知道跑得最快的 5 匹马并给出排名?
  18. 反问环节:团队做什么业务、用什么技术栈。

《参考解析》

512M 内存申请 1G 会怎样:malloc 拿到的只是虚拟地址空间,能不能成功首先看 Linux 的 overcommit 策略(vm.overcommit_memory):0(默认启发式)通常允许,返回一个有效指针;2 则严格按 CommitLimit 拒绝,直接返回 NULL。就算 malloc 成功,物理页也在首次写入时才由缺页中断按需分配(demand paging),此时如果物理内存 + swap 不够,就会触发 OOM Killer 把进程杀掉,或者当前线程收到 SIGSEGV。另外两个边界:32 位进程用户态地址空间只有 3G/1G 划分,申请 1G 还要有足够大的连续虚拟地址空洞,碎片化时会直接失败;Java 这类托管运行时还要叠加堆参数(-Xmx)和直接内存的限制。

kill 一个进程能不能带走全部 100 个线程:能。kill <pid> 把 SIGTERM 发给进程(线程组),内核默认动作是终止整个线程组,不需要逐个线程处理。真正可能”kill 不掉”的情况是:①某个线程卡在不可中断睡眠(D 状态,等磁盘 IO/NFS),要等 IO 返回;②线程自己用 pthread_sigmask 屏蔽了 SIGTERM,或注册了处理器却永远不退出;③进程 fork 出的子进程不在同一线程组里,需要按进程组杀(kill -- -<pgid>)或 pkill -P。工程上的可靠做法是:先发 SIGTERM 并 waitpid 等一个超时(K8s 默认 30s,即 terminationGracePeriodSeconds),超时再 SIGKILL(SIGKILL 不可捕获,必然生效);应用层注册信号处理器做优雅停机,逐个设置取消标志、join 或 pthread_cancel 各线程,交出资源。

epoll 的 LT 与 ET:水平触发(LT,默认)只要 fd 的读缓冲区还有数据,每次 epoll_wait 都会返回,所以可以只读一部分、下次继续;边缘触发(ET)只在状态变化的那一刻通知一次,之后不再重复通知。ET 的效率更高(epoll_wait 返回次数少、系统调用少),但必须遵守两条纪律:①fd 一定要设成非阻塞(O_NONBLOCK),否则最后一次 read 会把整个线程阻塞住;②必须循环读直到返回 -1 且 errno == EAGAIN/EWOULDBLOCK,才算把数据读干净。同理,监听 fd 在 ET 下 accept 也要循环调用直到 EAGAIN,否则会漏掉同时到达的连接。ET 常配合「one loop per thread + 非阻塞 IO」的 Reactor 模型使用。

TIME_WAIT:四次挥手里主动关闭方在发出最后一个 ACK 后进入 TIME_WAIT,等待 2MSL。Linux 上这个值固定是 60 秒(net.ipv4.tcp_fin_timeout 只影响 FIN_WAIT2 和孤儿连接,改它不影响 TIME_WAIT)。存在的两个理由:①如果最后的 ACK 丢了,对端会重传 FIN,此时本端还能重发 ACK;②让本次连接在网络中残留的报文自然消散,避免被同四元组的新连接误收造成数据错乱。危害是作为主动关闭方的服务器会堆积大量 TIME_WAIT 占用本地端口。缓解手段:net.ipv4.tcp_tw_reuse=1(需要开启时间戳,只适用于主动发起连接的客户端场景)、SO_REUSEADDR、改用长连接/连接池减少连接数、让客户端先关闭连接、以及 net.ipv4.ip_local_port_range 扩端口范围。tcp_tw_recycle 在新内核已被移除,不要提它。

深分页为什么越翻越慢:LIMIT 1000000, 10 的语义是”扫描并丢弃前 1000000 行,再取 10 行”,代价与 offset 成正比;如果排序字段没有合适索引,还要做全表扫描 + Using filesort,越往后越慢。解决方案按优先级:①游标/键集分页——记住上一页最后一条的 id,下一页写成 WHERE id > :last_id ORDER BY id LIMIT 10,走索引直接定位,是 O(1) 级别的;②延迟关联——SELECT t.* FROM t JOIN (SELECT id FROM t ORDER BY id LIMIT 1000000, 10) x USING(id),让子查询走覆盖索引少回表;③业务上限,禁止跳到任意深页,或强制收窄筛选条件(时间范围);④数据量极大时改用 ES 的 search_after。注意 LIMIT offset 大时优化器还可能放弃索引直接全表扫,所以线上要配合慢查询监控。

先更新数据库再删缓存失败:结果是缓存里长期保留旧值,读请求全部读到脏数据,直到 key 过期或下一次更新。所以缓存必须设 TTL 兜底,这是底线。在此之上:①删除失败就重试——同步重试几次仍失败则投递到 MQ 异步重试,消费端做幂等(删除操作天然幂等);②更稳的是订阅 binlog(Canal/Debezium)异步删缓存,把删除动作从业务代码里解耦,业务更新成功就一定会有一次删除;③读多写少用延迟双删。

问题 15 的乱序场景其实不可怕:因为「删除缓存」是幂等的、且方向上安全——消息队列里的删除命令晚到,执行后缓存被清空,下次读请求会从数据库回源,而此时数据库里是正确的值,回填进缓存的也是正确值。真正危险的是更新缓存而不是删除缓存:并发下把旧值写回缓存会长期污染。所以第一原则是”更新数据库 + 删除缓存”,而不是”更新数据库 + 更新缓存”。如果必须做到严格防乱序:给缓存值加版本号/时间戳,回填时用 CAS(Lua 脚本比较版本)只允许新版本覆盖;或者让同一个 key 的更新走同一个 MQ 分区串行消费,用 binlog 的位点顺序保证先后。

100 枚硬币(30 正 70 反)分两堆:随便取出 30 枚作为 A 堆,剩下 70 枚为 B 堆。设 A 堆里反面有 x 枚,那么 B 堆里的反面就是 70 − x 枚。现在把 A 堆的 30 枚全部翻转,A 堆的反面数变成 30 − x。比较一下:A 堆 30 − x,B 堆 70 − x,两者相等(因为 A 堆总数 30 + B 堆总数 70 = 100)。所以做法固定:任取与”正面数量”相同的枚数(30 枚)为一堆,整体翻转即可。

25 匹马 5 条跑道:先分 5 组各跑一次(5 次),得到每组内部排名;第 6 次让 5 个组的第一名比一次,设结果为 A1 > B1 > C1 > D1 > E1。此时可以淘汰:D 组、E 组全部(连本组第一都排在后面),C 组除 C1 外全部(C2 至少输给 A1、B1、C1 三匹),B 组除 B1、B2 外全部,A 组只保留 A1、A2、A3。A1 已经确定是最快的。第 7 次让 A2、A3、B1、B2、C1 比,取前 4 名,加上 A1 就是最快的 5 匹——所以找出最快 5 匹最少 7 次。如果还要求这 5 匹的完整排名,第 7 次里落选的那几匹之间还要再加赛一次,答案是 8 次。回答时要把”7 次出集合、8 次出排名”这个区别讲明白,这是这道题的分水岭。