拼多多服务端四面凉经:线下四轮从手撕顺子到 HTTPS 与虚拟内存
- 轮次
- 一面+二面+三面+四面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
一面(技术面)
- 问了一些与项目有关的内容
- 除了分布式乐观锁,考虑过使用 token 的幂等方式吗?
- 三次握手时 TIME_WAIT 的原因是什么?
- CAP 理论
- 除了 RabbitMQ 还认识哪些消息队列?这些消息队列有什么区别?
- 反问:业务和技术现在更应该向哪方面努力?(面试官答:木桶效应,应该全面)
- 手撕:随机抽 1–6 张扑克牌,大小王可以代替任意牌,能不能组成长度为 6 的顺子?
二面(HR 面)
- 哪里人?
- 为什么实习时间这么晚?
- 公司怎么样?
- 家里是否同意来这么远工作?
- 父母职业?
- 伴侣情况?
- 兴趣爱好?
- 对多多的了解程度?
- 朋友对你的评价?
- 期望薪资?
三面(技术面)
- 实习项目
- 描述一下项目流程
- HTTPS 和 HTTP 有什么区别?
- ping 接口用什么协议?
- 讲一下 Redis
- Redis 为什么快?
- 多路复用有哪些模型?
- MySQL 有什么了解?
- 手撕:给一段数字字符串,求所有合法的 IPv4 地址
四面(主管面)
- 实习项目了解
- 并发问题如何处理?
- 先说说有哪些锁?(自旋锁、互斥锁分别是什么)
- 读写锁了解吗?如果一直有读请求,写锁会被饿死吗?
- 如果内存只有 2G,但是现在有一个 2.5G 的程序,能跑起来吗?
- 虚拟内存深挖
- 讲一下一台电脑的数据如何到另一台电脑的全流程
- 如果你是项目负责人,一台服务挂了并产生很大影响,如何解决?
- 手撕:二叉树的中序遍历(不可以使用递归)
《参考解析》
TIME_WAIT 存在两个理由,要都说出来。 一是保证最后一个 ACK 能到达对端:主动关闭方发出 FIN、收到对方的 FIN 并回 ACK 后,如果这个 ACK 丢了,对方会重传 FIN,此时主动方必须还在,否则会回 RST 让对端认为异常;等待 2MSL(报文最大生存时间的两倍)正好覆盖「对方重传 FIN + 自己重发 ACK」的窗口。二是让本次连接的四元组彻底失效:避免旧连接的延迟报文被新连接(相同四元组)误收。运维侧的直接后果是大量短连接会在服务端堆积 TIME_WAIT 并占用端口,缓解手段是连接复用(长连接/连接池)、tcp_tw_reuse(仅对客户端出向连接安全)以及调低 tcp_fin_timeout,但 tcp_tw_recycle 在现代内核里已被移除、不要用。
幂等与 CAP、消息队列的对比,答法要成体系。 幂等的常见方案是唯一业务键 + 去重表(或 Redis SETNX)、状态机条件更新(WHERE status = 'INIT',影响行数为 0 即已处理)、以及 token 机制(提交前先领 token,服务端校验并作废);相比分布式乐观锁(版本号 CAS),token 更适合防止重复提交场景,乐观锁更适合并发更新冲突场景,两者解决的问题不完全一样。CAP 的关键是分区容忍 P 在分布式系统里不可放弃,所以实际是在 C 和 A 之间取舍:金融账务偏 CP(宁可不可用也不能不一致),社交 Feed 偏 AP(允许短暂不一致)。消息队列的对比要落到投递语义、吞吐、顺序性、延迟、运维成本几个维度:RabbitMQ 路由灵活、延迟低、单机吞吐一般,适合业务解耦;Kafka 吞吐高、天然分区有序、支持回溯消费,适合日志与流处理,但延迟和运维成本更高;RocketMQ 在事务消息、延迟消息、顺序消息上更贴近电商业务;Pulsar 存算分离。没有全能选手,按场景选。
HTTPS 与 ping。 HTTPS 在 HTTP 之下加了 TLS:握手阶段用非对称加密协商出对称密钥并验证服务端证书(防中间人靠的是证书链 + 域名校验 + 信任根,而不是加密本身),之后用对称加密传输数据,同时 MAC/AEAD 保证完整性。ping 走的是 ICMP 协议(回显请求/应答),不经过 TCP/UDP 端口;也正因如此,很多环境会直接封禁 ICMP,ping 不通不代表服务不可用。Redis 为什么快要能分层说:纯内存操作、单线程(6.0 起只有网络 I/O 多线程)避免锁与上下文切换、I/O 多路复用(epoll)支撑高并发连接、高效的数据结构(跳表、listpack、渐进式 rehash)、以及 RESP 协议本身极简。多路复用模型按演进顺序是 select(fd 上限 1024、每次全量拷贝并轮询)、poll(去掉了上限,仍是轮询)、epoll(红黑树管理 fd + 就绪链表回调,只返回就绪的 fd,边缘/水平触发可选),此外还有 kqueue、IOCP。
「2G 内存跑 2.5G 程序」考的是虚拟内存。 能跑起来的原因是程序看到的地址空间是虚拟的,与物理内存解耦:操作系统按需分页加载,只有真正访问到的页才映射到物理内存,未访问的部分留在磁盘(换页文件/swap);同时虚拟内存还提供了地址隔离、连续地址空间的抽象,以及文件映射(mmap)和写时复制(fork、COW)这些机制。能否真的跑起来取决于工作集大小和换页频率:工作集远小于 2G 就基本流畅,工作集超过物理内存就会频繁缺页(thrashing),表现为卡顿甚至 OOM Killer 杀进程。页表部分可以补一句 MMU 负责地址翻译、TLB 缓存最近映射、多级页表节省空间但增加访存次数。
锁的分类与读写锁写锁饿死。 按是否阻塞分:自旋锁(忙等,不放弃 CPU,适合临界区极短且不能睡眠的场景,多核下才有意义,单核自旋就是浪费)、互斥锁(拿不到就睡眠,适合临界区较长)。按兼容性分:共享锁(读锁)与排他锁(写锁)。按实现分:悲观锁与乐观锁(版本号/CAS)。读写锁的写锁饿死是真实风险:如果读请求持续不断,读锁总能拿到,排队的写锁就可能永远等不到。缓解手段是写优先或公平模式(有写者等待时不再放行新读者,如 Java 的 ReentrantReadWriteLock(true))以及给读锁加超时让写者有插队机会。这题常被追到「那读多写少还该用读写锁吗」——现代实现里读写锁的收益主要在多核、读临界区不短的场景,读临界区极短时用普通互斥锁往往更快。
手撕部分的关键是边界。 扑克牌顺子那题,作者的简化做法是:先排序,检查除大小王外是否有重复,再用「去掉大小王后的最大值减最小值是否 ≤ 5」判断(因为长度为 6 时跨度最大是 5)。这个思路在无重复的前提下成立,完整解法可以更严谨——用计数加滑动窗口判断 6 张窗口内是否覆盖足够的连续值、且缺口数不超过王的个数。合法 IPv4 地址要按回溯写并处理前导零、每段 0–255、段数恰好 4;作者提到漏了全 0 的情况("01"、"001" 这类前导零应当剪掉)。非递归中序遍历的标准写法是用显式栈一路向左压栈,弹出时访问并转向右子树——作者用队列实现,本质上偏离了中序的定义,这也是面试官一眼看出的点。