1. 这份卷子真正想筛出什么样的人
我当年参加滴滴出行2018校园招聘内推笔试的时候,岗位是系统开发工程师。说实话,那个时间节点很多同学都在海投,刷题刷到麻木,但对“系统开发”这个岗位到底考什么,其实没几个人想清楚了。我更倾向于把内推笔试看成一次“预筛选+技术摸底”,它不像统考那样完全按统一分数线卡人,而是在更短的时间里判断你有没有基础的工程素养和系统思维。
为什么这么说?因为滴滴的核心业务场景是出行调度,系统开发工程师要面对的是海量订单流、实时路径匹配、多端状态同步、高并发下的一致性保障这类问题。这类业务有三个特征:高并发、强实时、状态多。所以笔试考点不会只是纯算法刷题,它更像是把“操作系统、网络、数据库、Java并发、分布式常识”揉在一起,用选择题和编程题来考察你有没有形成一套完整的系统认知框架。
我当时看完卷子后最大的感受是:它不是考“你会不会写快排”,而是考“你在高并发场景下敢不敢用快排”。这两件事的差别非常大。前者是记忆问题,后者是工程判断问题。你背下了十种排序算法的手写代码,但遇到“同一时刻数万个司机上报位置,服务端如何高效聚合热点区域数据”这种题目时,如果没有分布式的概念,没有内存分片和消息队列的意识,就只能写出一个在单机上循环遍历的玩具解法。
再说说“系统开发工程师”和“算法工程师”“后端研发工程师”在笔试侧重点上的差异。算法岗更看重模型理解、数学推导、数据结构深度;纯后端研发岗更看重业务接口设计、数据库建模、框架运用;而系统开发岗介于两者之间,它的题目会倾向于:并发场景下的资源争用、 分布式环境下的数据一致性、高吞吐系统的瓶颈分析。这就要求你不仅会写代码,还要理解代码跑在什么环境里、有哪些底层机制在支撑它。
所以不要只刷LeetCode。你要知道,内推笔试的卷子通常在30到60分钟内就要完成一轮技术评估,出题人没有时间去考那些偏门冷知识,他们只会挑“最能反映工程能力”的点来考。把这些点逐个吃透,比盲目刷300道题划算得多。
以下是我对这份卷子考察范围的复盘,按出现频率和重要性排序:
| 考察模块 | 典型考点 | 为什么考 |
|---|---|---|
| 数据结构与算法 | 链表、二叉树、动态规划、Top K问题 | 基础编码能力,快速判断候选人代码功底 |
| 操作系统 | 进程线程、死锁、虚拟内存、IO模型 | 高并发服务必须理解资源调度与隔离 |
| 计算机网络 | TCP握手、TIME_WAIT、HTTP状态码 | 分布式系统绕不开网络通信细节 |
| 数据库 | 索引、事务隔离级别、SQL编写 | 业务数据落地与查询性能保障 |
| Java基础与并发 | JMM、HashMap并发问题、线程池 | 主力开发语言的基础掌握程度 |
| 分布式常识 | CAP理论、负载均衡、缓存策略 | 判断候选人是否有系统级思维 |
这个表格基本还原了那张卷子的出题骨架。你可以对照看看自己还有哪些薄弱项,别等到笔试前一周才发现连TCP和UDP的区别都讲不清楚。
2. 核心考点逐个拆解:每个知识点背后对应什么工程问题
2.1 数据结构与算法:不是考你会背,而是考你会在什么场景下选它
这一块是笔试的重头戏,占分比通常最高。我列举几个当年卷子中出现的高频题,并帮你把“知识点”和“工程场景”之间的那道桥搭起来。
链表类题目,比如“判断链表是否有环”“找链表倒数第K个节点”,几乎所有大厂笔试都会出。滴滴考链表,不是因为它业务里直接用到链表,而是链表操作能很直接地暴露你对指针/引用、边界条件、空间复杂度的掌控能力。当时有一道题让我印象深刻:“给定两个单链表,找出它们的第一个公共节点。”这道题有哈希表法和双指针法,双指针法的精妙之处在于:两个指针分别走完自己的链表后再走对方的链表,最终在公共节点相遇。它体现的“用空间换时间,或者用时间换空间”的权衡思维,恰恰是系统开发中做资源规划的核心思路。
树与图遍历也是必考。比如二叉树层次遍历、最近公共祖先、拓扑排序。我把“最近公共祖先”单独拎出来讲,因为这道题有两个版本的考察方式:普通二叉树上找LCA,和二叉搜索树上找LCA。如果你能想到利用BST的性质(目标值介于root和root之间时,root就是LCA),代码会非常简洁。出题人想看到的不是暴力解法,而是你是否善于利用数据结构的固有性质来优化算法。
动态规划考得不算深,但一定会有一道。比如“最长上升子序列”“编辑距离”这类经典题。我当时拿到的是一个变形题:“一个网格从左上角到右下角,每次只能向右或向下,求路径上数字和的最小值。”这题最基础的解法是二维DP,空间复杂度O(mn),优化后可以压缩到O(n)。在笔试场景下,写出二维DP就能拿大部分分数,但如果你能写出滚动数组优化,并在注释里说明“这个优化将空间复杂度从O(mn)降到了O(n)”,会明显加分。因为它展示了你不是死记硬背DP模板,而是理解了状态转移过程中真正依赖哪些历史状态。
Top K问题在滴滴笔试里出现频率非常高,因为“找出热度最高的K个区域”“选出路况最好的K条路径”这类业务需求太常见了。我不建议一上来就写快速排序,而是分情况讨论:
- 数据量小、内存放得下:直接排序取前K,简单直观。
- 数据量很大、内存放不下:用堆,维护大小为K的最小堆,堆顶就是当前第K大的元素。
- 数据量极大且分散在多台机器上:先每台机器求局部Top K,再合并求全局Top K。
我当时笔试时选择用堆实现,因为卷子明确说了“数据规模为10亿个整数,内存限制为256MB”。你一定得养成读到约束条件再选算法的习惯,这是系统开发工程师的基本素养。
2.2 操作系统:并发服务的底层地基
操作系统的重要性,很多刷题型选手会严重低估。我在笔试里遇到的OS题不算难,但覆盖面很广,主要包括:
进程与线程的区别。这道题几乎必考。标准回答是:进程是资源分配的基本单位,线程是CPU调度的基本单位;进程有独立的地址空间,线程共享所属进程的地址空间;进程切换开销大,线程切换开销小。但如果你只答到这里,只能拿一半分。出题人真正想听到的是:线程共享地址空间,所以多线程编程时要格外注意数据同步,这就是并发编程中锁和原子类存在的前提。
死锁的四个必要条件:互斥、持有并等待、不可剥夺、循环等待。光背概念不够,笔试常常会给你一段代码,问你“这段代码是否会发生死锁?为什么?”你得能准确识别出四个条件分别在哪一行代码中体现。我记得有一道题是经典的“哲学家就餐问题变种”,两个线程各自持有了一把锁,又想获取对方的锁。你需要在答题时明确指出循环等待条件成立,然后给出破解方法:要么破坏持有并等待(一次获取所有资源),要么破坏不可剥夺(尝试获取锁,获取不到就释放已有锁),要么破坏循环等待(规定获取锁的顺序)。
虚拟内存和页面置换算法。这个概念用生活类比最好懂:虚拟内存就像一个只有十平方米的储藏室,但给你配了一份注明“每件物品放在哪个格子”的目录。程序运行时,只把当前需要的物品(页面)拿进储藏室(物理内存),其余放在仓库(磁盘)。页面不够时,就要决定把哪个物品送回仓库,这就是置换算法。LRU是笔试最常考的置换算法,因为它对应“最近最少使用”的业务直觉。这里我建议你亲手写一遍LRU缓存的实现,用HashMap+双向链表,这样笔试遇到“设计一个LRU缓存”的题目时,你直接就能给出O(1)时间复杂度的方案。
IO模型:阻塞IO、非阻塞IO、IO多路复用、异步IO。滴滴的系统开发岗特别爱考这个,因为服务端接入大量司机和乘客的连接时,不能用“一个线程处理一个连接”的模型,那会直接把线程池打爆。你要能讲清楚select/poll/epoll的区别,尤其是epoll的红黑树+就绪链表机制,以及边缘触发和水平触发的差异。这个知识点是面试后续深挖的高频切入点,笔试时遇到别只选个B就完事,建议做题时顺手在草稿纸上画出事件流,确认自己理解没有偏差。
2.3 计算机网络:分布式系统的毛细血管
网络题基本上是送分题,但也是很多人丢分的地方,因为细节太容易混淆。
TCP三次握手。大家都能说出SYN、SYN+ACK、ACK的顺序,但笔试往往会让算一算序列号的变化,或者问“第三次握手失败了会怎样”。第三次握手失败时,服务端会超时重传SYN+ACK,如果多次重传仍失败,则断开连接。这个问题考的其实是TCP可靠传输的机制,本质是“任何一方收到确认前,都不能确认消息已经被可靠送达”。
TIME_WAIT是另一个高压考点。主动关闭连接的一方会进入TIME_WAIT状态,持续2MSL(最大报文段生存时间)。为什么要等2MSL?因为要确保最后一个ACK能让对方收到——如果这个ACK丢了,对方重传FIN,你还有时间响应。同时也要让旧连接中的所有报文在网络中自然消亡,避免污染新连接。这个机制在系统开发里非常重要:高并发短连接场景下,如果服务端大量进入TIME_WAIT状态,会导致端口资源耗尽。当时我看到卷子上这道题时,就在心里庆幸自己曾经处理过类似的生产问题,不然真的只会背“TIME_WAIT是2MSL”而不知道它背后的工程意义。
HTTP和HTTPS。状态码是必考项:200、301、302、304、401、403、404、500、502、503,每个都对应一个具体场景。特别要注意304(Not Modified)和503(Service Unavailable)这种和系统架构密切相关的状态码。304跟HTTP缓存机制挂钩,503则意味着服务过载或正在维护。如果你能把503和“服务限流、熔断降级”联系起来答,那就远超及格线了。
TCP vs UDP的选择,这个考点是送分中的送分题,但要结合场景说才有说服力。TCP面向连接、可靠、有序,适合文件传输、网页访问;UDP无连接、不可靠、低延迟,适合实时音视频、游戏状态同步。滴滴这类出行服务在车辆轨迹上报场景中,有时候会采用UDP+应用层消息序号来兼顾实时性和一定程度的可靠性,这个思路如果能在笔试题里提到,会显得你有真实场景认知。
2.4 数据库:业务数据落地的核心
数据库题在系统开发岗笔试里占的分量,比很多人想象中大得多。
索引是最核心的考点。InnoDB的B+Tree索引,为什么用B+Tree而不是B-Tree或者红黑树?因为B+Tree的非叶子节点不存储数据,可以在相同页大小下容纳更多键值,树更矮,查询更少IO;同时叶子节点通过链表串联,范围查询非常高效。你应该能画出B+Tree的大致结构,并解释聚簇索引和二级索引的区别,以及什么情况下会发生回表。
SQL题也是一定会出现的,比如多表联查、分组统计、子查询。我建议你把“GROUP BY + HAVING”的组合练熟,因为它的坑在于:WHERE是在分组前过滤,HAVING是在分组后过滤。当时卷子上有一道题:“查每个城市订单量超过10000的司机数”,你必须知道过滤条件应该放在HAVING还是WHERE,放错了,返回结果就完全不对。
事务隔离级别是数据库另一个必考点。读未提交、读已提交、可重复读、串行化,这四级隔离性逐个增强,并发性能逐个下降。MySQL InnoDB默认是可重复读。这个知识点要想答好,不能只背名字,要知道每个级别解决了什么问题:读未提交有脏读问题,读已提交解决了脏读但存在不可重复读,可重复读解决了不可重复读但默认下仍可能有幻读问题(InnoDB通过间隙锁解决),串行化则彻底解决幻读但性能最差。如果你能结合MVCC(多版本并发控制)说一下快照读和当前读的区别,那这道题你基本可以拿满分。这个知识点的工程意义非常直接:平台从账户余额中扣款、司机发起提现、订单状态变更,每一个都涉及事务的并发控制,理解隔离级别就是理解业务一致性的底线。
2.5 Java基础与并发:系统开发的主力语言
如果笔试明确偏向Java,那么Java并发编程这块会被重点考察。
HashMap的并发问题是一个老生常谈但极其经典的考法。JDK 1.7中HashMap并发put可能导致死循环(链表环化),JDK 1.8中改进了链表插入方式和扩容机制后不再有这个问题,但仍然存在数据丢失的可能。你如果真的要答好这个点,需要深入理解resize过程的头插法和尾插法区别。我建议你不仅能说出结论,还能画一下链表迁移过程的示意图,这能帮你在笔试后的面试环节直接建立专业印象。
线程池的考察很细:corePoolSize、maximumPoolSize、workQueue之间的关系和任务提交流程。当提交任务数超过corePoolSize时,任务会进入工作队列进行排队,而不是立刻创建新线程;队列也满了,才创建新线程直到maximumPoolSize;仍然处理不过来,才走拒绝策略。这里有一个很常见的误区:很多人以为只要提交任务就会创建线程到maximumPoolSize,实际不是这样。任务队列的选型也会直接影响线程池行为:LinkedBlockingQueue无界队列基本不会触发创建非核心线程,而SynchronousQueue则不缓存任务,来了任务就尝试创建线程。这些细节在笔试选择题中出现频率非常高。
并发工具类:CountDownLatch、CyclicBarrier、Semaphore,这三个要分清使用场景。CountDownLatch是“一个线程等待多个线程完成”,CyclicBarrier是“多个线程互相等待,同时到达某个屏障后继续”,Semaphore是“控制同时访问某个资源的线程数”。我用一个类比帮你记忆:CountDownLatch是公司年会集合,所有人都到齐了,大巴才能发车;CyclicBarrier是百米赛跑,所有运动员都就位了,裁判才能发令;Semaphore是景区限流,每天放固定人数进去,出来一个才放进一个。这套类比在笔试考场上是很好用的速记策略。
2.6 分布式系统常识:高频但容易失分
最后一类考点是分布式系统的基础常识。这部分不需要你读过《数据密集型应用系统设计》这种大部头,但至少要掌握几个核心概念。
CAP理论是必考。一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance),三者不可兼得。这里最容易被忽视的是:分区容错性在分布式系统中是必须满足的,因为网络分区一定会发生,你只能在C和A之间做取舍。这个洞察要答出来,才能展现你真的理解了CAP的精髓,而不只是背出了三个英文字母。
缓存策略的考察通常结合业务场景,比如“春运抢票高峰期,如何防止缓存穿透、缓存击穿、缓存雪崩”。缓存穿透是查一个根本不存在的数据,每次都要打到数据库,解决思路是缓存空值或布隆过滤器;缓存击穿是某个热点key在过期瞬间大量请求打到DB,解决思路是互斥锁或热点数据永不过期+后台更新;缓存雪崩是大量key同时过期或者Redis集群宕机,解决思路是过期时间加随机值、多级缓存、熔断降级。这套东西不是死知识,它直接对应出行业务中“秒级查询热点区域的实时路况”这种场景。
负载均衡算法:轮询、加权轮询、最少连接、一致性哈希。一致性哈希一定要搞懂,因为它在分布式缓存和数据分片中太常用了。你要能解释“虚拟节点”解决了什么问题(数据倾斜),以及为什么加节点时只有少量数据需要迁移。我当时笔试有一道判断题“一致性哈希算法的节点数量变化时,大部分key的映射位置会发生变化”,这显然是错的,但如果你没有真正理解环形哈希空间,很容易被绕进去。
3. 经典笔试真题复盘:从读题到AC的完整思考过程
3.1 编程题一:Top K高频元素
原题大致是:“给定一个非空的整数数组,返回其中出现频率前K高的元素,要求时间复杂度优于O(n log n)。”
我拿到题目后的第一个动作是确认输入约束。如果数组长度是百万级别,那直接Collections.sort + 统计频次的方案就是O(n log n),虽然能过,但明显不符合“优于O(n log n)”的要求。正确的方向是用桶排序:先遍历一次数组,用HashMap统计每个元素出现频率;然后创建一个“频率桶”数组,桶的下标即频率,桶内存放该频率对应的所有元素;最后从高频率桶向低频率桶遍历,收集前K个元素。这个方案的时间复杂度是O(n),空间复杂度是O(n)。
具体代码(Java版本):
public List<Integer> topKFrequent(int[] nums, int k) { Map<Integer, Integer> freqMap = new HashMap<>(); for (int num : nums) { freqMap.put(num, freqMap.getOrDefault(num, 0) + 1); } List<Integer>[] bucket = new List[nums.length + 1]; for (Map.Entry<Integer, Integer> entry : freqMap.entrySet()) { int freq = entry.getValue(); if (bucket[freq] == null) { bucket[freq] = new ArrayList<>(); } bucket[freq].add(entry.getKey()); } List<Integer> result = new ArrayList<>(); for (int i = bucket.length - 1; i >= 0 && result.size() < k; i--) { if (bucket[i] != null) { result.addAll(bucket[i]); } } return result; }我当时写完后特意检查了一个边界情况:如果数组中所有元素出现频率都相同,频率桶数组的最右端可能同时存在多个不同的元素,此时从桶末尾往前取时需要确保不超过K个。这里有个小技巧:result.size() < k不仅控制了循环终止,还防止了数组越界。
这道题在系统开发场景里的对应是“统计一段时间内最热门的搜索词”或“统计哪些区域呼叫量激增”,所以你千万别把它当纯算法题来做,要意识到它是分布式系统中“分词频统计”的简化模型,否则面试追问“如果数据分散在100台机器上你怎么统计”,你会措手不及。
3.2 编程题二:设计一个支持GetMin的栈
原题是“实现一个栈,除了push、pop操作之外,还要支持getMin操作,要求所有操作的时间复杂度均为O(1)。”
这道题经典到不能再经典了,但笔试现场很多人还是会写错。设计思路是使用两个栈:一个数据栈正常存取元素,一个辅助栈专门记录当前的最小值。push时,数据栈正常入栈;辅助栈则压入“当前元素与辅助栈栈顶元素之间的较小值”。pop时,两个栈都出栈。
关键细节在于:辅助栈的元素个数和数据栈保持一致。这样getMin时只需要返回辅助栈栈顶即可。如果辅助栈只在遇到更小值时入栈,那么在最小值被pop出去后,辅助栈的信息就不完整了。
class MinStack { private Deque<Integer> dataStack; private Deque<Integer> minStack; public MinStack() { dataStack = new ArrayDeque<>(); minStack = new ArrayDeque<>(); } public void push(int x) { dataStack.push(x); if (minStack.isEmpty() || x <= minStack.peek()) { minStack.push(x); } } public void pop() { if (dataStack.pop().equals(minStack.peek())) { minStack.pop(); } } public int getMin() { return minStack.peek(); } }注意上面这段代码,辅助栈只在元素“小于等于当前最小值”时才入栈,pop时判断数据栈弹出的元素是否等于辅助栈栈顶值,是则辅助栈同步弹出。这个方案空间效率更高,但需要仔细处理边界。如果你在考场上对自己边界处理没有信心,用“两个栈同步增长”的方案更稳妥,代码可读性也更好。
这道题考察的真正重点不是“双栈技巧”,而是你是否具备“空间换时间”的意识。系统开发里用缓存加速读请求、用预计算减少重复计算,本质上和这道题是同一个思维模式。
3.3 设计类简答题:订单状态机设计
有一道题我印象特别深,因为它在选择题和编程题之外,属于一道简短的设计题:“请设计一个订单状态机,并说明状态流转的触发条件。”
订单状态是出行系统的心脏。你至少得画出几个核心状态:已创建、已接单、已到达上车点、行程中、已到达目的地、已支付、已取消。每个状态迁移都有触发方和条件:
- 已创建到已接单:乘客下单成功后,司机接单。
- 已接单到已到达上车点:司机到达乘客定位的上车点,App端自动或司机手动确认。
- 已到达上车点到行程中:乘客上车,司机点击开始行程。
- 行程中到已到达目的地:司机点击结束行程。
- 已到达目的地到已支付:乘客支付订单,支持余额、免密支付等。
这里出题人想看的不是状态枚举,而是你是否考虑了异常分支:司机取消、乘客取消、超时未接单自动取消、行程中发生事故需要紧急终止,这些都是状态机设计中的隐性要求。我当时在答案里补了一句“所有状态迁移需要保证幂等性和操作记录,便于对账和问题追踪”,明显感觉到面试官在后续面试中特别关注了这一点。
状态机设计的原则叫“单一事实来源”,简单说就是:同一个订单的状态,在任何时刻都只能有一个权威定义,不能App端认为已接单、服务端认为已创建。在分布式环境下,这个“权威”通常由服务端数据库中的订单状态字段决定,客户端的状态展示只是一个投影。
4. 笔试现场的答题节奏与得分策略
这一节我讲讲“怎么做题”本身。很多同学明明知识点都会,但因为节奏不对、取舍不对,最终分数不理想。
先列一个我自己的时间分配参考(假设笔试总时长120分钟,选择+填空占60分钟,编程题占60分钟):
| 题型 | 建议耗时 | 策略 |
|---|---|---|
| 选择题 | 30分钟 | 一遍过,不确定的标记后优先跳过,不要纠结 |
| 填空题 | 15分钟 | 多数是概念补充和简单计算,快速作答 |
| 编程题第一题 | 20分钟 | 先想清楚再动手,保证AC一题 |
| 编程题第二题 | 15分钟 | 如果第一题顺利,这里可以试更高难度;否则优先保证第一题正确 |
| 剩余时间 | 10分钟 | 回头处理标记的选择题难题,整理草稿纸上的思路 |
为什么选择题要“限时跳过”?因为选择题一道也就一两分,你在某道TCP细节题上耗了十分钟,就是拿后面编程题的得分机会去冒险。编程题通常是按用例通过比例给分的,AC一道简单题拿到的分数,远比纠结三道选择题的总分更实在。
编程题的答题顺序也有讲究:先做“题目描述最清晰、输入输出格式最明确”的那道,通常它是整张卷子里最水的。不要以为题号靠后的题一定难,有的卷子第一题反而是最恶心的超长题干阅读题。先把好拿的分拿到手,心态会稳定很多。
再讲一个老手才懂的经验:笔试时要把自己的想法写在代码注释里。比如“如果数据量超过内存限制,可以采用外部排序”或者“此处使用堆是因为需要频繁获取Top K元素”,即使代码没完全跑通,阅卷人看到你的思考路径,也会给过程分。校招笔试的时间窗口有限,不可能每个方案都完美落地,你把思路表达出来,本身就是一种展示。
编程题如果一时没有最优解,先写暴力解也有价值。暴力解能保证你拿到一定比例的测试用例分数,而且暴力解的代码相对简单,不容易出现低级语法错误。写完暴力解后再标注一行“TODO:优化为O(n)解法”,至少说明你清楚问题在哪里。千万别空着不写,空题是一分没有的。
关于编译环境:笔试系统通常支持C++、Java、Python等主流语言。我的建议是选你最熟悉、标准库最丰富的语言。算法题优先Java或Python,因为集合类库能大幅减少编码量;C++虽然效率高但容易在内存和指针上出低级错误。Java的HashMap、PriorityQueue、Deque这些数据结构要能不用查API就直接写出来,这是硬功夫,考前多敲几遍就自然熟了。
5. 去重与反套路:别被培训班的“模板答案”带偏
在讲复习策略之前,我要专门泼一盆冷水。现在网上流传的很多“大厂笔试题模板答案”,其实是培训班总结出来的一刀切套路,它们能帮你应付最基础的版本,但很容易让你在稍微变形一点的题目面前翻车。
举个例子,HashMap的并发问题,很多模板答案是“Java 7会死循环,Java 8没有死循环”。这句话没有错,但它遗漏了更重要的后半句:Java 8虽然修掉了死循环,但在并发put时仍可能导致数据丢失,所以高并发场景仍然不应该直接使用HashMap,应该用ConcurrentHashMap。单凭“死循环被修复”这个结论,无法回答出题人真正的意图。如果你在笔试现场写了“Java 8 HashMap可以并发使用”,那这道题基本就挂了。
再比如TCP的TIME_WAIT,模板答案是“2MSL”。但出题人常会在后面加一个小问:“为什么不是MSL呢?”如果你只是背了答案,就会卡住。正确理解是:2MSL = 1MSL(我的最后一个ACK到达对端的最长时间)+ 1MSL(对端重传FIN到达我的最长时间),这样能确保旧连接的报文段彻底消失,不会干扰新连接。把原理讲到这个深度,才叫真正掌握。
我建议你复习时遵循“三层递进”法:
第一层:掌握概念定义,能回答“是什么”。这一层对应基础选择题,是最低要求。
第二层:掌握原理机制,能解释“为什么”。比如为什么TCP需要三次握手,为什么B+Tree适合磁盘索引。这一层能帮你应对填空、简答和面试追问。
第三层:掌握工程权衡,能在场景中做选择。比如“这个场景选Kafka还是RocketMQ”“这个接口用缓存还是消息队列”。这一层不见得笔试会考,但它决定了你未来能不能真正胜任系统开发工程师。
很多同学复习只停留在第一层,考前刷了上百道选择题,结果笔试分数不错,但一到面试就露馅。笔试和面试是联动的,内推笔试的成绩直接影响面试官对你的初始判断,所以你在卷面上展现的深度,最好能支撑你后续面试的复盘讲述。
时间上怎么规划?如果你还有四周,我建议这样分配:第一周快速过一遍计算机网络和操作系统,配合刷选择题;第二周集中刷数据结构与算法,每天保证三到四道完整编程题,不仅要AC,还要看题解,比较自己的解法和最优解法的差距;第三周转向数据库和Java并发,尤其是索引和线程池这两大高频考点;第四周做模拟笔试,严格限时,用一套往年题或者模拟题完整走一遍流程,提前感受压力。
“刷题”和“看题”的比例也要控制好。有同学一天能刷二十道,但全是“看懂了就过”,实际手写时一个边界条件都写不对。我的经验是:每道题如果三十分钟还写不出来,就去看题解,但看完题解后一定要自己重新把AC代码完整写一遍,而不是直接看代码。能不能白板写出来,是检验是否真正会做的唯一标准。
6. 查漏补缺清单:上考场前再快速过一遍这些点
最后分享一份我当年整理的查漏补缺清单,它不是大而全的知识点百科,而是针对“系统开发工程师”这个岗位笔试里最容易在细节上翻车的点。你可以把它当成考前一小时的快速回忆列表。
ArrayList和LinkedList的区别:ArrayList基于动态数组,随机访问快,尾部插入快;LinkedList基于双向链表,头部插入删除快,但随机访问慢。简单说,ArrayList偏爱“按索引逛商城”,LinkedList偏爱“在队伍两头插队”。
HashMap的默认初始容量是16,负载因子是0.75,扩容阈值是容量乘以负载因子。这个0.75是空间和时间开销的平衡点,不是随便定的。JDK 1.8后链表长度超过8且数组长度大于64时,链表会转成红黑树。
volatile关键字的两层语义:可见性和禁止指令重排序。它不能保证原子性,所以volatile变量不适合做计数器。它的核心应用场景是状态标志位,比如线程启动和停止的标志。
ThreadLocal的内存泄漏问题:ThreadLocalMap的key是弱引用,value是强引用。如果ThreadLocal对象被回收但线程还存活,value就永远无法被访问到,导致内存泄漏。解决办法是使用后调用remove方法清理。
TCP的粘包问题:由于TCP是字节流协议,没有消息边界,应用层需要自己定义消息边界。常见方案是固定长度消息、特殊分隔符、消息头包含长度字段。这个问题在系统开发中非常常见,尤其在做RPC框架或者Socket自定义协议时躲不开。
进程间通信方式:管道、消息队列、共享内存、信号量、Socket。笔试常考的是共享内存为什么是最快的IPC方式——因为它不需要内核态和用户态之间的数据拷贝,而管道和消息队列都需要通过内核中转。
同步和异步、阻塞和非阻塞的区别:同步异步关注的是消息如何通知,阻塞非阻塞关注的是调用方在等待结果时能否干别的事。把它们四个组合起来理解,会发现Java中的NIO是同步非阻塞,Netty虽然使用了异步ChannelFuture,但底层IO模型是IO多路复用加非阻塞模式。这个辨析题考得非常细,很多所谓“精通Netty”的同学都栽在这里。
数据库的乐观锁和悲观锁:悲观锁就是先拿锁再操作,比如SELECT ... FOR UPDATE;乐观锁就是操作时带上版本号或时间戳检查,版本不匹配就重试。出行系统里,司机接单操作就适合乐观锁,因为并发冲突的频率相对低,用版本号控制即可,没必要长时间锁行。
Redis的数据结构和过期策略:String、Hash、List、Set、ZSet,以及惰性删除和定期删除的配合机制。如果考到Redis持久化,至少要说清RDB是快照、AOF是追加日志,以及两者各自优缺点。
一致性哈希的虚拟节点:为什么要引入虚拟节点?因为节点少时哈希环容易数据倾斜,一个机器扛下大部分流量。虚拟节点把一个物理机器映射成多个虚拟位置,让数据分布更均匀,同时某个节点故障时,它的流量可以由多个其他节点分摊,不会造成单一节点过载。
这份清单不是我临时凑出来的,每一行都是往年笔试反复出现的考点。建议你在考试当天早起过一遍,不用深入展开,看到一个点能想起来龙去脉就算过关。如果哪个点想不起来,立刻翻开笔记快速补一眼,不要恋战。
7. 笔试之后:成绩不是终点,复盘才是开始
很多人笔试完就彻底放飞自我了,等收到面试通知才开始慌。我自己的经验是,笔试结束的当天晚上,趁着题目的记忆还温热,立刻做一次复盘。哪怕没有标准答案,也要把自己选的、写的答案完整记下来,然后对照资料逐题分析。这个过程有两个直接好处。
第一个好处:面试大概率会追问笔试内容。面试官拿到的笔试记录里,除了你的分数,还有你的作答详情。他们特别喜欢挑一道你答错的题或者一道你解法很奇怪的题,问“你为什么这样设计”“当时有没有考虑其他方案”。如果你不复盘,面对这种追问只能编,编出来的答案在资深面试官眼里漏洞百出。如果你认真复盘过,就能逻辑清晰地重述当时的选择和当下的反思,这本身就是一次漂亮的面试开场。
第二个好处:复盘能帮你找到自己的知识盲区。比如你发现数据库隔离级别那道题拿不准,说明你对事务并发控制的理解浮于表面,那就去补充MVCC和锁机制的细节。这个盲区今天不补,明天可能在面试中再次暴露。内推笔试某种程度上是给了你一次“预演”的机会,真正的高手会利用它来校准自己的复习方向。
再分享一个很务实的小技巧:每道错题,在复盘笔记里要写清“错因类型”。是概念理解错误、细节记忆模糊、还是做题时太急看错了选项?把错因分类之后,你会发现自己的问题往往集中在某几个模块。比如我当年最大的错因就是“网络题细节记忆模糊”,所以后续复习时把TCP状态机单独画了三遍,面试时遇到相关问题完全不慌。
笔试只是校招长跑中的一站。系统开发工程师这个岗位,真正比拼的不是你会背多少知识点,而是你在一个具体的业务场景里,能不能用系统思维拆解出可行方案。笔试只是这种能力的第一次量化体现。认真对待每一次笔试,考后认真复盘,你的系统设计能力、工程判断力就会在一次次的圈定范围、取舍决策中慢慢立起来。祝你在接下来的校招中拿到想要的Offer。
最后再提一个很多人容易忽略的细节:内推笔试虽然叫“内推”,但仍然有筛选率,内推只是帮你从简历池里被捞出来,笔试成绩不过关一样会被刷。所以不要以为走内推通道就可以放松准备。相反,正因为内推意味着你有更多人脉背书,笔试表现如果太差,反而会让推荐人难堪。以这个心态去对待每一道题,你的答题态度和严谨程度都会不一样。