看到“腾讯2015春招后台开发练习卷”这个标题,我第一反应是有些怀念。那会儿校招笔试大多还是纸质试卷加限时答题,风格和现在在线OJ完全不一样,C++、操作系统、网络、数据库四座大山压下来,能两个小时内从容做完的人真不多。但回过头看,这套练习卷其实是一份很完整的后台开发知识地图——它考的不是你会不会背题,而是你有没有具备一个后台开发工程师的基本素养。
这篇文章我想拿这套练习卷当引子,把后台开发笔试背后的考察逻辑、高频考点、答题策略和复习方法一条条拆开讲清楚。适合正在准备校招和社招的候选人,也适合刚进入后台开发方向、想系统梳理基础知识的同学。不管你是哪一类,看完应该都能找到一条自己可以照着做的复习路径。
1. 这套练习卷到底在考什么
1.1 五年过去,后台开发的考察基本盘没有变
单看年份,2015年距离现在已经不近了,技术栈变化非常大。云原生、容器化这些概念当时还不像今天这么普及,很多团队还在用虚拟机部署服务,微服务也没有成为默认选项。但有意思的是,后台开发的底层考察点几乎没怎么变:操作系统怎么管理进程和内存、网络协议栈怎么传输数据、数据结构怎么设计才能更快、数据库怎么建索引才不拖慢接口,这些知识依然是今天面试的高频区。
我最近几年面试候选人,还是会反复问进程和线程的区别、TCP为什么需要TIME_WAIT、哈希冲突怎么解决,这些问题和2015年练习卷上的题目没有本质区别。所以拿到旧练习卷,不要觉得题老就不重视。恰恰相反,经典题才是后台开发面试的压舱石,题目越老,越说明它是被验证过的核心考点,值得多花时间吃透。
1.2 反推能力模型:笔试背后的岗位画像
一份练习卷最有价值的不是给你答案,而是帮你反推公司对后台开发岗的期望画像。我翻过不少校招后台开发卷,题型结构通常比较固定:
- 选择题或填空题,覆盖语言基础、操作系统、网络、数据库等多个知识域;
- 简答题或问答题,考察对某个机制的原理解释,比如死锁产生条件、HTTP和HTTPS的区别;
- 编程题,重点考察算法和数据结构的落地能力,要求手写可运行的代码;
- 开放式设计题,比如设计一个短链接服务、一个排行榜或一个秒杀系统。
这种分布本身就在传递一个信息:后台开发不是单纯的“API调用工程师”,而是需要理解计算机系统运作原理、能在复杂工程场景中做方案设计的技术角色。笔试不是在考你背了多少知识点,而是在考你能否证明自己理解了这个系统。
| 能力维度 | 代表考点 | 考察目的 |
|---|---|---|
| 语言功底 | 虚函数、内存布局、智能指针 | 是否写过真正偏底层的代码 |
| 系统原理 | 进程线程、并发控制、虚拟内存 | 是否理解程序运行的环境 |
| 网络基础 | TCP状态、HTTP流程 | 是否了解服务调用的底层链路 |
| 数据结构 | 链表、树、哈希表、堆 | 是否具备基本算法能力 |
| 工程素养 | 数据库索引、Linux排查 | 是否具备实际开发和排错能力 |
把这五列当成能力地图,你会发现平时复习很容易覆盖前面的语言和算法,但工程素养这块经常被忽略。而练习卷里恰恰是那些看似零散的工程题,最能拉开差距。
2. 高频考点逐个拆解
2.1 C++与内存:虚函数、智能指针一个都不能少
2015年C++11已经普及了,后台开发用C++的团队非常多,所以练习卷里C++相关题目占比通常不低。虚函数是出现频率极高的考点,我见过很多种问法:虚函数表是什么?构造函数能不能是虚函数?为什么析构函数建议声明为虚函数?这些问题表面在考语法,实际在考你有没有理解C++对象模型。
简单展开讲一下。一个类只要包含虚函数,编译器就会给这个类生成一张虚函数表(vtable),每个对象的内存头部会有一个指针vptr指向这张表。调用虚函数时,不会像普通函数那样直接跳转到固定地址,而是先通过vptr找到虚函数表,再从表中取出真正的函数地址。如果析构函数不是虚的,当用基类指针delete派生类对象时,编译器只会调用基类的析构,派生类中申请的资源就可能泄漏。这就是“析构函数建议声明为虚函数”这个结论的底层逻辑。
还常考智能指针。shared_ptr底层的核心机制是引用计数,引用计数归零时自动释放内存,但循环引用会出现严重问题:两个对象互相持有shared_ptr,各自引用计数都无法降到零,内存就泄漏了。于是就有了weak_ptr来打破循环。练习卷如果考到内存管理,通常会给一段代码让你分析是否有泄漏,或者让你对比unique_ptr、shared_ptr、weak_ptr的适用场景。我建议复习的时候把对象的生命周期图画出来,真正理解“所有权”这个概念,比死背接口文档有用得多。
2.2 操作系统:进程线程、死锁和虚拟内存
操作系统是后台开发笔试的另一个大头,也是最容易出大题的模块。进程和线程的区别是必问的,回答时不要只背“进程有独立地址空间,线程共享地址空间”,最好能进一步解释清楚:为什么线程切换比进程切换轻量?因为同一进程内的线程切换不需要切换地址空间和页表,所以开销更小。如果再想加分,可以提一下协程,用户态调度,连内核态都不需要进,切换成本比线程更低。
进程间通信也是个常考点。管道、消息队列、共享内存、信号量、socket,这些机制各自的适用场景和性能差异要能分清。共享内存最快,因为它不需要内核做数据拷贝,但需要自己处理同步;消息队列和管道则经过内核缓冲区,适合小数据量传递,编码简单但性能相对低。面试官听到你能结合场景选方案,会比背定义印象深很多。
死锁几乎是每一套练习卷都会有的题。四个必要条件:互斥、持有并等待、不可剥夺、循环等待。应对策略就是破坏其中一个条件。答辩的时候最好带一个实际例子,比如两个线程各自持有一把锁,同时试图获取对方的锁,这时候就形成了循环等待。这种代码你们平时写并发程序时可能碰到过,能讲出来,说明你是真写过,不是只背了概念。
虚拟内存和分页也很重要。为什么要引入虚拟内存?因为它让每个进程拥有独立的地址空间,内存之间互相隔离,同时允许程序使用比物理内存更大的地址空间。这里可以打一个比方:物理内存像一间合租房,同时住着好几个进程,虚拟内存相当于每个人脑子里都觉得自己住的是独栋别墅,谁也看不见谁,系统在这些“错觉”背后做地址翻译。理解了这一层,再把缺页中断、页表、多级页表串起来,答这类题会比较完整,层次感也出来了。
2.3 网络:三次握手、TIME_WAIT和HTTP一个都跑不掉
后台开发必须有扎实的网络基础,这套练习卷里网络题如果丢分,基本就与大厂Offer无缘了。TCP三次握手为什么不是两次?最核心的原因是防止旧的连接请求突然到达,导致服务器错误地建立连接。想象一个场景:客户端第一个SYN在网络中滞留了,客户端因为没有收到确认而超时重发,发出了第二个SYN;如果只有两次握手,服务器收到滞留的旧SYN后就会建立连接,而客户端已经不想连了,资源白白浪费。三次握手可以让客户端确认自己发出的SYN是否有效,避免这种历史连接错误建立。
TCP的TIME_WAIT状态也常被拿出来问,特别是“为什么在服务器上看到大量TIME_WAIT”。TIME_WAIT有两个作用,一是让被延迟的报文段在网络中自然消失,避免影响后续新连接;二是等待足够时间,确保对端收到最后一个ACK。服务器上TIME_WAIT多,通常意味着主动关闭连接的一方是你的服务,这时候可以通过开启keep-alive减少频繁建连、调优TIME_WAIT复用参数等方式优化。既有理论又有工程实践,这种题目在练习卷里出现率极高。
HTTP部分当年主流还是HTTP/1.1,至少要清楚keep-alive的作用、GET和POST的区别、常见状态码含义(200、301、302、403、404、500、502)。现在面试如果再能补上HTTP/2的多路复用、HTTP/3基于QUIC,会在考官眼里非常加分,但核心还是把TCP和HTTP的关系说清楚。我碰到过很多候选人能背状态码,但一问“浏览器输入URL后发生了什么”就卡住了。这道综合题其实可以串起DNS解析、TCP连接、HTTP请求、服务端处理、响应返回全链路,后台开发练这套题非常有价值。
2.4 数据结构和算法:模板不背,套路要懂
练习卷里的编程题通常不会特别难,重点在基本功和边界处理。链表反转、快排、二分查找、二叉树遍历是最高频的几道题,我建议这些题目都要能手写,而且不能有语法错误。面试官特别爱考链表为空、只有一个节点、节点个数为奇数这些边界条件,很多人在这些地方翻车。
排序算法要会自己分析复杂度。快排平均时间复杂度是O(nlogn),最坏是O(n^2),为什么最坏情况发生在基准选得不好时?因为每次分区都极度不均衡,递归深度接近n。理解了这一点,你就能解释为什么工程中常用随机选基准或者三数取中法。
Top K问题也是后台开发笔试常客,比如求海量数据里最大的K个数。常规解法是维护一个大小为K的小顶堆,堆里存当前最大的K个数,新数据如果比堆顶大,就替换堆顶并调整堆,复杂度是O(nlogK)。这道题背后其实在考察堆这个数据结构,也涉及海量数据的处理思路。答完之后最好补一句:如果数据太大不能一次性加载进内存,可以对数据分片,每片内先求局部Top K,最后再合并。这一下就从“会做题”升级为“会做工程”。
3. 数据库与Linux:笔试里最容易被低估的两块
3.1 数据库:索引、事务、SQL优化都要答出“为什么”
练习卷如果包含数据库题,最经典的话题永远是索引原理。为什么MySQL InnoDB用B+树而不是哈希表或者二叉树?这个问题要答得有层次:哈希表适合等值查询但不适合范围查询,无法支持order by、like前缀匹配这些场景;二叉树的问题是数据量大时树高度太高,每次树节点访问都是一次磁盘IO,高度越高IO次数越多;B+树所有数据都存储在叶子节点,叶子节点用链表串联,天然支持范围扫描,同时每个节点可以存大量键值,树高度通常只有三四层,磁盘IO次数被压到很低。这样回答,考官听到的就不只是一个结论,而是一条完整的推理链。
事务ACID也是高频题。四个特性分别靠什么实现要清楚:原子性靠undo log回滚,持久性靠redo log落盘,隔离性靠锁和MVCC,一致性是所有这些机制共同保证的最终结果。隔离级别从读未提交到串行化,分别解决脏读、不可重复读、幻读的问题。这里最容易被追问“MVCC怎么实现”,核心就三件事:版本链、ReadView、隐藏字段。能把这个讲清楚,说明你对InnoDB内部机制有真实理解。
我记得当年练习卷里还有一道SQL优化题,给出一条慢SQL,问你如何排查。标准思路是先用EXPLAIN看执行计划,检查有没有走索引、有没有回表、有没有filesort,再回头审视SQL本身:是不是select *取了很多不需要的字段,是不是在索引列上做了函数运算导致索引失效,是不是多表join顺序不好。比如有一条订单查询:
select * from orders where user_id = 123 and status = 1 order by create_time desc limit 10;这条查询可以建一个联合索引(user_id, status, create_time),这样where等值条件可以走索引定位,order by字段也可以直接借助索引的有序性,避免额外排序。像这种具体例子比空谈“要建索引”有用得多,练习卷里如果出现这类题,拿分的关键就是把步骤写完整。
3.2 Linux排查思路:从笔试延伸到真实故障处理
练习卷里Linux命令题不算多,可一旦出现就容易丢分,因为它不靠纯记忆,而靠日常积累。比如问你:如何查看一个进程打开了哪些文件?答案是lsof -p pid。如何看端口被哪个进程占用?netstat -tlnp或者ss -tlnp。如何实时看日志输出?tail -f。这些都是后台开发几乎每天要用的命令,笔试考它们,实际上是在看你是不是真的在Linux环境下干过活。
还有一类题目更综合,比如“线上CPU突然飙高,你怎么排查?”这种问题的标准流程是:先用top找到CPU占用最高的进程,再用top -Hp pid找到进程内部CPU最高的线程,然后把线程栈打印出来,结合日志定位到具体代码。这个排查链路听起来简单,但没有实操过的人很难凭想象写完整。我建议准备后台开发的同学,真的找一台测试机器,部署一个会死循环的服务,自己亲手压一压、抓一抓线程栈,走一遍完整流程。体验过之后,你写出来的答案会完全不一样,那种细节感是背多少遍文档都补不出来的。
另外一个容易被忽略的命令题是僵尸进程。什么是僵尸进程?子进程结束但父进程没有调用wait回收它的退出状态,进程就变成僵尸。简单的处理方式是让父进程退出,init进程会收养这些子进程并回收,也可以让父进程注册SIGCHLD信号处理,在子进程结束时自动wait。这道题如果出现在练习卷简答题里,能把僵尸进程产生的原因、排查命令(ps查看状态列的Z)和处理方式写出来,得分基本就稳了。
4. 编程题与设计题的答题策略
4.1 手写代码:先谈思路再动手,过程分同样重要
很多同学做笔试编程题时有个误区:看到题目就急着开写,结果写到一半发现边界没处理,或者方法选得不对,时间全浪费了。我更推荐的节奏是:先花一两分钟把思路想清楚,在草稿纸上写一下数据流向和复杂度分析,然后再动手写代码。阅卷流程虽然以最终代码为准,但很多时候你写出来的注释、函数命名、边界处理,都会影响整体观感,过程分并不只是说说而已。
以“反转链表”为例,这道题几乎每年都会出现。我的写法是这样:
ListNode* reverseList(ListNode* head) { ListNode* prev = nullptr; ListNode* cur = head; while (cur) { ListNode* next = cur->next; cur->next = prev; prev = cur; cur = next; } return prev; }代码很短,但面试官通常会追问一句:为什么这里需要一个临时变量next?原因很简单,把cur->next指向prev之后,原来的链表就断了,如果不提前保存next,下一步就没法继续遍历。能把这一点讲清楚,说明你是真的理解指针操作,而不是死记硬背。再往深一点,递归版怎么写?递归版需要注意每次递归返回的都是新的头节点,一层层回溯时要把指针反向指回去。两种写法各有优劣,建议都练熟。
我估计很多人会问:笔试时间那么紧,哪来时间打草稿?我的答案是,越紧越要打草稿。编程题最怕的不是慢,而是方向错了还在埋头写。花两分钟确定思路,至少能保证你写出来的代码基本方向正确,这比写了一大半推翻重来划算得多。
4.2 设计题:秒杀系统、短URL这类题怎么一步步拆
后台开发笔试的压轴位置偶尔会出现一道开放性设计题,更多时候会出现在面试环节。遇到这种题,最怕的是没有框架,想到哪说到哪。以短URL服务为例,核心需求是把长链接转成短链接,用户访问短链接时跳回原地址。我习惯按下面五个维度去拆:
- 需求分析:要支持短码生成、访问跳转、过期时间、访问统计。
- 容量预估:假设每天新增100万条记录,一年是多少条?QPS大概多高?需要多大存储?这些数字能帮你判断用单机还是集群。
- 方案选型:短码生成是发号器还是对长链接做哈希截断?存储用Redis还是MySQL?要不要分表?
- 关键点设计:发号器要保证全局唯一,可以用数据库自增,也可以用雪花算法;跳转用301还是302?301利于缓存,302利于统计,通常我会选302,然后用额外机制做缓存。
- 扩展考虑:某个短链突然被大量访问怎么办?加一层Redis缓存挡在前面,可以保护DB。
这套框架套到秒杀系统也一样成立:瞬时流量怎么削峰?请求先打到网关,做限流和防刷,再进入消息队列异步削峰,最后才落到数据库更新库存。库存扣减要防止超卖,经典做法是数据库乐观锁:update stock set count = count - 1 where id = ? and count >= 1,影响行数为1才表示扣减成功。
设计题的价值不在于你真的设计出一个能上线的生产系统,而在于你能不能逻辑清晰地一步步推导。建议平时多看一些架构设计文章,自己画数据流向图,用上面的框架多练几个题。练多了,笔试时看到设计题心里就有底,不会抓不住重点。
5. 用一份练习卷完成自测与复盘
5.1 模拟笔试的完整流程
拿到一套练习卷后,最不推荐的做法是直接翻答案。一套卷子最好的用法是模拟一场真实笔试:给自己定一个两小时的倒计时,关掉IDE和编译器,手机放远一点,用纸笔做题。编程题也尽量用手写或记事本写,不要依赖自动补全,这样能暴露你在边界条件和语法细节上的真实水平。
做完之后不要立刻对答案,先把每道题标注清楚:哪些是确定的,哪些是蒙的,哪些完全没思路。标注这个动作很关键,因为你平时看答案觉得“都会”的题目,闭卷一写才发现很多细节根本不记得。把自己放在考试的状态里,才能把“记忆层面的熟悉感”和“理解层面的掌握感”区分开。
顺便说一下时间分配。我一般建议选择题和填空题控制在30分钟左右,简答题40分钟,编程题40分钟,剩余时间留给设计题和检查。当然这个分配要因人而异,但有一个原则是要遵守的:千万不要在一道题上死磕太久,卡住超过十分钟就果断跳过。笔试拼的是总分,不是单题的完美。
5.2 错题复盘:把做错的题变成知识网络
对完答案之后,整理错题是整套练习卷收益最大的一步。我习惯按知识域做统计:操作系统错了几道,网络错了几道,数据库错了几道。如果发现某一类错误率特别高,那这就是你当前最需要补的短板。比如你发现只要涉及TCP状态转换的题都错,那说明你对TIME_WAIT、CLOSE_WAIT这些状态的理解停留在一知半解,需要重新把状态转换图自己画一遍。
复盘的时候可以用一张简单的表记录:
| 题号 | 知识域 | 错误原因 | 关联知识点 | 同类题计划 |
|---|---|---|---|---|
| 第8题 | TCP | 分不清TIME_WAIT与CLOSE_WAIT | 四次挥手、主动关闭方 | 做3道状态类题 |
然后每周回头看看这张表,把错题遮住答案重新做一遍。如果第二次能在几分钟内做对,说明这个知识点吸收得差不多了;如果还是卡,那就要回到基础概念重新学。复盘不是把答案抄一遍就完了,而是要把每一道错题都变成一个知识网络的入口,顺着它把相关的知识点全部拉出来过一遍。
5.3 横向扩展:由一道题引出一类题
练习卷是死的,但知识点是活的。做完一道题之后,我会习惯性地想:这道题还有哪些变体?比如练习卷考了二分查找,那我可以接着做寻找旋转数组最小值、查找峰值元素、在有序矩阵中搜索这些变体;考了进程和线程的区别,那就顺势复习协程、线程池参数、并发队列,甚至自己去写一个有界阻塞队列的demo。
这种“以题带面”的复习方式效率很高,因为每一道母题都是一条知识链的起点。你不需要刷无穷无尽的题,只要把核心母题理解透,再通过变体题扩展思路,就能应对大多数笔试变化。我自己复习后台开发时有个很土但很管用的方法:把做明白的题讲给室友听,能讲清楚说明真懂了,讲不清的地方就是下一步要补的短板。
练习卷毕竟是2015年的内容,部分考点放到今天已经有了新变化,当年很少涉及的云原生、容器、分布式链路追踪,现在已经是后台开发的常见话题。所以用这套卷子做知识体检的同时,也要再结合当下的技术热点做补充。把旧卷当成一张体检单,再照着体检结果制定新的学习计划,这比单纯刷一套旧题有意义得多。后台开发这条路没有捷径,但有了清晰的知识地图和正确的复盘方法,你走起来会比大多数人少踩很多坑。