尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

寒武纪后端笔试复盘:从操作系统到系统设计的硬核考点

寒武纪后端笔试复盘:从操作系统到系统设计的硬核考点 寒武纪2019秋招后端岗笔试二这批题我到现在还记得挺清楚倒不是说难度高到让人记仇而是这批题目的出题风格和市面上常见的后端八股文清单很不一样。它不光是问你“知道什么”还会逼着你把知识点串起来用。第二批试卷整体覆盖面很广操作系统、网络、数据库、C、算法、系统设计都有涉及而且因为寒武纪本身是芯片公司后端笔试里还会有不少和底层、并发、性能相关的内容。如果你是正在准备后端岗位秋招的人或者想看看芯片公司后端笔试和互联网大厂后端笔试有什么区别这篇复盘应该能给你不少参考。我会把当时考到的题目类型、我的做题思路、参考答案要点以及后来复盘时发现的坑都整理出来尽量还原考场上的真实情况。1. 笔试整体情况回顾1.1 试卷结构与考察方向先交代一下这套题的整体构成。第二批试卷一共分为四大部分20道不定项选择题、4道简答题、2道编程题、1道系统设计题。考试时间是90分钟总分100分选择题和简答题各占20分编程题每道15分系统设计题30分。从分值分布就能看出来系统设计题才是这套卷子的重头戏前面所有基础题都是在给最后这道设计题做铺垫。选择题的覆盖面非常杂从C的虚函数表、指针和引用的区别到Linux的进程状态、TCP的拥塞控制再到数据库索引失效的场景都有涉及。简答题则偏向原理类比如“简述HTTPS握手过程”“说说你对内存对齐的理解”这类。编程题一题考了链表相关操作另一题是字符串处理整体难度中等偏上但代码量不小需要在规定时间内写出完整可运行的实现。系统设计题给了个短网址服务的需求要求完成容量估算、表结构设计、整体架构描述。这套卷子最明显的特征就是底层和原理考得特别多。互联网公司的后端笔试通常更偏业务框架、微服务、分布式那一套但这套卷子明显更看重你对操作系统、编译原理、网络协议这些基础知识的掌握程度。这和寒武纪的业务性质有关芯片公司的后端系统往往需要和底层硬件打交道对性能的要求更高所以他们更关注候选人对系统底层机制的理解深度。1.2 寒武纪后端岗的特殊之处寒武纪做的是AI芯片所以它的后端岗位和一般互联网公司的后端有本质区别。互联网后端面对的典型场景是大量用户的HTTP请求、业务逻辑处理、数据存储读写而寒武纪后端面对的场景更偏AI推理服务、模型部署、算子库的调用甚至要和芯片驱动层打交道。这就决定了他们在笔试里会特别关注几个方向第一是C的掌握程度。整套卷子几乎全部围绕C展开连选择题里的多线程题目都用的是C的语法风格而不是Java或Go。这是因为芯片公司的底层SDK、推理框架基本都用C开发后端服务也大概率是C写的。第二是内存管理。选择题和简答题里反复出现内存相关的考点比如内存对齐、堆和栈的区别、智能指针的使用场景这些都是C后端开发的日常必备知识。第三是并发编程。AI推理服务的一个典型特征是计算密集型和I/O密集型混合如何设计高效的并发模型直接决定服务性能所以笔试里线程安全、锁、线程池、并发数据结构这些是重点考察对象。如果你投的是互联网公司后端可以大量准备Redis、消息队列、微服务治理这些内容但投寒武纪这类芯片公司需要把重心放在C功底、操作系统原理、并发编程和性能优化上。这是一个方向性的差别提前搞清楚目标公司到底需要什么样的人复习效率会高很多。后续如果有朋友想投寒武纪或者其他芯片公司的后端岗我的建议是Java那一套企业级框架可以放一放把精力集中在C、Linux系统编程、网络编程和高性能服务设计这些方向上。这套笔试像是照着这个方向量身定制的非常能筛出真正有底层功底的候选人。1.3 时间分配与做题策略90分钟做完整套题其实挺紧张的我那次的做题策略是先做选择再做简答然后做编程最后留30分钟左右给系统设计题。事后复盘觉得这个时间分配基本合理但有几处可以优化。选择题20道大概花了25分钟不定项选择的坑在于少选多选都不得分所以对于模棱两可的选项要非常谨慎拿不准的宁可少选也不要乱选。简答题4道花了20分钟这部分重要的是答到点子上不用写太多废话。编程题两道花了30分钟一题链表反转类的题目比较顺利另一题字符串处理的题目因为要考虑的边界情况比较多耗了一些时间。系统设计题最后用了差不多35分钟这道题分值最高多花时间是值得的但前提是前面的题已经把该拿的分拿到了。如果重新做一次我会把简答题的时间压缩到15分钟以内编程题遇到卡壳的先跳过把系统设计题的框架先搭出来再回头补。系统设计题不能放到最后潦草写30分的大题如果只写两三行框架基本等于放弃这30分了。2. 计算机基础题解析操作系统与网络2.1 进程与线程一道送分题的陷阱简答题第一题是“简述进程和线程的区别与联系”。看起来是送分题但想拿到满分需要答得足够全面。我的答题思路是先用一句话说清楚两者的根本区别——进程是资源分配的基本单位线程是CPU调度的基本单位然后从资源开销、通信方式、独立性、崩溃影响这几个维度具体展开。资源开销方面进程拥有独立的地址空间创建进程需要分配独立的PCB、页表、文件描述符表等资源开销比较大线程共享所属进程的地址空间和资源创建和切换的开销比进程小得多。通信方面进程间通信需要借助管道、消息队列、共享内存、Socket这些机制而同一进程内的线程可以直接读写共享变量通信成本低。但这也意味着线程之间同步问题更突出需要借助互斥锁、条件变量、信号量等机制保证线程安全。独立性方面进程之间是相互隔离的一个进程崩溃通常不会影响其他进程而同一进程内的多个线程共享地址空间一个线程出现野指针操作可能直接拖垮整个进程。这是用线程做并发的最大风险点也是笔试里容易遗漏的得分点。我还在答案里补充了一个容易被忽略的细节进程是资源分配的基本单位但这并不意味着进程没有调度属性。在Linux的2.6内核之前进程确实是内核调度的基本单位2.6内核之后引入了线程组的概念调度器以线程为基本调度实体。严格来说线程是CPU调度的基本单位这个结论在Linux上成立但在某些实时操作系统上可能略有不同。这些细节能体现你对操作系统原理的掌握不是停留在教科书层面而是真的深入到了内核实现层面。2.2 TCP三次握手与TIME_WAIT的深层逻辑网络部分的简答题考了一道“为什么TCP建立连接需要三次握手而不是两次或四次”。这道题我答得比较顺畅因为我之前专门梳理过这个问题的完整逻辑。三次握手的最核心目的是让通信双方确认彼此的收发能力都正常并同步初始序列号。第一次握手客户端发送SYN报文服务端收到后能确认客户端的发送能力正常、服务端的接收能力正常但客户端此时还不知道服务端的收发能力是否正常。第二次握手服务端回复SYNACK客户端收到后能确认服务端的发送能力正常、自己的接收能力正常同时也能确认自己第一次发的SYN被服务端正确收到了。第三次握手客户端再发一个ACK服务端收到后能确认客户端的接收能力正常因为它发送的SYN被正确应答了。这样双方都确认了彼此的收发能力正常连接才能可靠建立。如果只有两次握手存在一个经典问题客户端第一次发送的SYN报文在网络中滞留超时客户端重传SYN并完成数据传输和释放连接后滞留在网络中的旧SYN报文才到达服务端。服务端只经过两次握手就会认为这是一个新的连接请求于是发送SYNACK并分配资源但客户端收到后不会理睬这个确认因为客户端根本没有发起这条连接于是服务端的资源就被白白占用了。三次握手通过最后一次ACK避免了这个问题因为服务端在收到ACK之前不会进入ESTABLISHED状态。我在答案里还补充了为什么不用四次握手三次握手已经能够可靠确认双方收发能力正常并同步序列号四次握手只是增加了一次往返没有实质性的额外收益反而增加建立连接的延迟。这道题还延伸出了TIME_WAIT的讨论。服务端主动关闭连接后连接会进入TIME_WAIT状态并等待2MSL最大报文段生存时间之后才释放。原因是防止最后一个ACK丢失后无法重发同时确保旧连接中的所有报文都在网络中消失避免影响新连接。实际做后端调优时高并发短连接场景下会遇到大量TIME_WAIT堆积的问题我当时在做服务端开发时也踩过这个坑可以用调节内核参数或改用长连接机制解决。笔试时把这些实际经验写进去会让答案显得比较丰满。2.3 HTTP状态码与REST接口设计网络部分还有一道选择题考察HTTP状态码的理解给的场景是“客户端创建资源成功应该返回哪个状态码”选项有200、201、202、204。正确答案是201 Created。这个题本身不难但干扰项设计得很巧妙因为很多人只知道200是成功却不清楚201专门表示创建资源成功202表示请求已接受但尚未处理完成204表示请求成功但没有返回内容。REST接口设计和状态码是紧密相关的GET对应200POST创建资源对应201DELETE删除资源对应204服务端校验失败对应400认证失败对应401没有权限对应403资源不存在对应404服务器内部异常对应500。这套映射关系是后端开发的基本功笔试里可能只考一个状态码但面试环节大概率会让你完整设计一套REST接口状态码的规范使用会是其中一个考察点。我在笔试时把状态码相关的知识梳理成了一套自己的记忆方法。2xx系列是成功其中201专门用于创建、204专门用于无内容返回3xx系列是重定向其中301是永久重定向、302是临时重定向、304是命中缓存4xx系列是客户端错误最常见的是400、401、403、404、405、4095xx系列是服务端错误最常见的是500和502、503。这样梳理之后选择题基本不会丢分。2.4 死锁与资源分配从条件到实际排查操作系统简答题里有一道“产生死锁的四个必要条件是什么如何破坏这些条件来预防死锁”。四个必要条件是互斥条件、请求并保持条件、不可剥夺条件、循环等待条件。互斥条件指资源一次只能被一个进程使用请求并保持条件指进程持有至少一个资源同时又去请求其他资源而该资源可能被其他进程持有不可剥夺条件指进程已获得的资源在未使用完之前不能被强行剥夺循环等待条件指若干进程之间形成一种头尾相接的循环等待资源关系。预防死锁的思路就是分别破坏这四个条件。破坏互斥条件可以通过把独占资源改为共享资源来实现但现实中很多资源天然就是互斥的所以这个方案实际应用很少。破坏请求并保持条件可以通过资源一次性分配实现即要求进程在执行前一次性申请所有需要的资源但这会降低资源利用率导致大量资源长期被闲置。破坏不可剥夺条件可以通过允许强行剥夺资源实现比如某个进程请求新资源时得不到满足就释放它已占有的资源但这种方案可能导致进程之前的工作成果丢失。破坏循环等待条件是最常用的方案通过给资源编号并要求进程按编号顺序申请资源从源头上避免循环等待。这道题我在实际排查多线程死锁时用过很多次。排查的常规步骤是先通过jstack或gdb查看线程转储找出线程都在等待哪些锁然后画出等待关系图看是否存在环。如果存在环就根据环中的资源依赖关系分析是哪段代码加锁顺序不一致导致的。定位到具体代码后修复方式通常是统一加锁顺序或者用tryLock超时机制替代盲目阻塞加锁。3. 数据库与缓存索引、事务与高并发读3.1 索引为什么用B树一道必考题的完整答法数据库部分的选择题考了一道“为什么InnoDB索引选择B树而不是哈希表、红黑树或二叉搜索树”。这道题考察的不只是对B树的了解还要能横向对比不同数据结构的适用场景并且明白数据库索引的底层需求到底是什么。数据库索引的核心需求是既要支持等值查询又要支持范围查询同时还要尽量减少磁盘I/O次数。哈希表支持等值查询效率极高的时间复杂度可以做到O(1)但无法支持范围查询哈希冲突严重时性能退化也很严重所以不考虑。二叉搜索树虽然在理想情况下查询时间复杂度是O(logn)但极端情况下会退化为链表导致时间复杂度变成O(n)而且树的高度随数据量增大而增大每次节点访问都对应一次磁盘I/O树太高意味着磁盘I/O次数太多。红黑树是一种自平衡二叉搜索树能保证树的高度维持在O(logn)但即使维护了平衡树的高度依然比B树高得多。对于一个1000万行数据的表红黑树的高度大约在20多而B树因为每个节点可以存储多个键值通常三层到四层就能覆盖千万级别的数据量。加上B树的叶子节点通过指针连接成一个有序链表天然支持高效的范围查询。相比之下红黑树做范围查询需要中序遍历效率低不少。B树相比B树的优势也值得展开说说。B树的内节点也存储数据导致相同容量的内节点能存储的键值数量比B树少也就是扇出更小树更高磁盘I/O次数更多。B树的所有数据都存在叶子节点内节点只存索引键扇出更大树更矮I/O次数更少而且叶子节点形成有序链表范围扫描只需要沿着链表顺序遍历不需要回溯。我记得笔试后复盘时还补了聚簇索引和非聚簇索引的区别。InnoDB的主键索引是聚簇索引叶子节点直接存储整行数据二级索引的叶子节点存储的是主键值所以通过二级索引查询时如果查询的列不在索引中就会发生回表操作再通过主键进行一次聚簇索引查询。这个知识点选择题里有没有考到我不太确定但面试环节被追问的概率很高。3.2 事务隔离级别与MVCC的内在逻辑数据库简答题考了一道“MySQL的四种事务隔离级别分别是什么分别解决了什么问题”。四种隔离级别从低到高依次是读未提交、读已提交、可重复读、串行化分别解决脏读、不可重复读、幻读这些问题。读未提交允许一个事务读到另一个事务尚未提交的数据可能出现脏读虽然并发性能最好但数据一致性最差。读已提交保证一个事务只能读到已经提交的数据解决了脏读但可能出现不可重复读即同一个事务内两次读取同一行数据结果却因为其他事务提交而不同。可重复读是MySQL默认的隔离级别保证了同一个事务内多次读取同一行数据结果一致理论上仍可能出现幻读即同一个事务内两次范围查询得到的行数不同。InnoDB通过间隙锁在可重复读级别下基本解决了幻读问题。串行化是最高的隔离级别所有事务串行执行完全避免了脏读、不可重复读和幻读但并发性能最差。这道题如果没有追问答到隔离级别的定义就够了。但如果面试官继续追问“InnoDB的可重复读是如何实现的”就得讲MVCC了。MVCC通过版本链和ReadView实现每一行记录都有隐藏的事务ID字段和回滚指针字段修改数据时不直接覆盖旧值而是生成一个新版本通过回滚指针连成版本链。事务执行快照读时根据ReadView判断当前应该看到版本链中的哪个版本从而实现不同隔离级别下的读一致性。我在笔试答案里还专门写了可重复读和读已提交在MVCC实现上的差异。读已提交是每一条SELECT语句都生成一个新的ReadView所以两次SELECT可能看到不同版本的数据这就是不可重复读的根源。可重复读是事务第一次执行SELECT时才生成ReadView之后所有的快照读都复用这个ReadView从而保证同一个事务内看到的版本一致。这个细节很多复习资料都不会讲透我当时写上去之后自己都觉得有点秀。3.3 缓存雪崩、穿透与击穿笔试里的高并发三板斧选择题里有一道拿Redis做缓存的场景题考的是缓存穿透和缓存击穿的区别。这两个概念名称类似但场景完全不同。缓存穿透是指查询一个根本不存在于数据库中的数据缓存里没有数据库里也没有导致每次请求都要落到数据库层数据库压力剧增缓存形同虚设。解决方案通常有两个思路一是把空值也缓存起来设置一个较短的过期时间防止同一批不存在的key反复打到数据库二是使用布隆过滤器在缓存和数据库之前加一层过滤器白名单内的key才允许继续查询从源头过滤掉绝大部分不存在的key。布隆过滤器存在误判率判断不存在的一定不存在但误判存在的可能实际不存在所以用布隆过滤器只能拦截一部分穿透请求需要合理设置位数组大小和哈希函数数量来控制误判率。缓存击穿是指一个热点key在过期瞬间大量请求同时涌向数据库。和穿透的区别在于击穿是真实数据只是缓存刚好过期。解决方案是加互斥锁当缓存过期时只有一个线程能进入数据库查询其他线程等待锁释放后重新读取缓存另一种方案是热点key不设置过期时间改用后台任务异步更新缓存避免缓存失效瞬间的并发压力。缓存雪崩是指大量key同时过期或者Redis实例宕机导致所有请求全部落到数据库数据库压力瞬间飙升甚至崩溃。解决方案包括给各key的过期时间加一个随机值防止同时过期部署Redis集群保证高可用以及做多级缓存让请求从本地缓存或CDN层消化大部分压力。这三个概念在做后端笔试时几乎必考建议背熟。4. 算法与数据结构手撕代码的实战现场4.1 单链表反转从递归到迭代的演进编程题第一题是单链表反转。这道题几乎是后端岗位笔试标配但要求是手写完整实现并考虑边界情况。我选择用迭代实现思路是维护三个指针prev、cur、next每次循环将cur的next指向prev然后三个指针整体后移直到cur为空。struct ListNode { int val; ListNode *next; ListNode(int x) : val(x), next(NULL) {} }; 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; }写这道题最容易踩的坑是忘记保存cur-next。如果你先把cur-next改成了prev再想访问原来的下一个节点就找不到了链表就此断开。所以进入循环第一步就是保存next这是整个算法的关键。除了迭代写法递归解法也是面试官喜欢追问的。递归的核心思路是假设当前节点之后的链表已经完成反转然后让当前节点的下一个节点指回当前节点。代码很简洁但理解起来比迭代版本要绕一些。ListNode* reverseList(ListNode* head) { if (!head || !head-next) { return head; } ListNode* newHead reverseList(head-next); head-next-next head; head-next nullptr; return newHead; }递归写法的关键点在head-next-next head这句让当前节点的下一个节点反转指回当前节点然后head-next置空避免形成环。笔试时如果时间紧张建议用迭代写法递归虽然简洁但容易出现悬空指针的问题。4.2 LRU缓存面试官最爱的综合题简答题里虽然没有直接要求实现LRU但选择题有一道是让选择最适合实现LRU的数据结构组合。正确答案是哈希表加双向链表。哈希表负责O(1)的get操作定位节点双向链表负责O(1)的插入和删除操作同时维护访问顺序。LRU的get操作步骤是从哈希表中查找key对应的节点如果不存在返回-1如果存在把这个节点从双向链表中摘除然后移动到链表头部。put操作步骤是如果key已存在更新value并将节点移动到头部如果key不存在创建新节点加入头部并加入哈希表如果此时缓存容量超出上限从链表尾部删除最后一个节点同时删除哈希表中对应的键。手写LRU是我面试准备时反复练习的一道题笔试时虽然没要求手写但我在备考时梳理过一个精简版本的实现思路。核心点在于双向链表的节点既要存key也要存value因为删除尾部节点时需要知道这个节点的key才能去哈希表里删除对应的键。这是个容易忽略的细节设计只存value的节点会在这里翻车。这道题还有很多变体比如LFU最不经常使用淘汰算法需要结合访问频率和时间两个维度来淘汰。LFU的经典实现是哈希表加多个双向链表每个链表对应一个访问频率。实现复杂度比LRU高不少笔试如果遇到优先把LRU写稳就够用了。4.3 Top K问题海量数据下的解法分层选择题里有一道关于海量数据Top K问题的题目大意是“有100亿个整数如何在内存有限的情况下找出最大的100个数”。这个题考察的不是某个固定算法而是不同数据规模下应选择不同方案的工程判断能力。如果数据量小到可以直接载入内存最直接的做法是用堆排序或者快速选择算法。找出最大的100个可以在内存中维护一个大小为100的小顶堆遍历所有数据如果当前元素比堆顶元素大就弹出堆顶并插入当前元素遍历结束后堆中就是最大的100个数。时间复杂度是O(nlog100)近似O(n)空间复杂度是O(100)。如果数据量大到无法一次性载入内存比如100亿个整数大约是40GB内存就需要分治策略。把文件切分成多个小文件保证每个小文件可以载入内存然后在每个小文件内求出Top 100最后把所有小文件的Top 100汇总再做一次Top 100选择。这也是MapReduce的经典思路map阶段求局部Top Kreduce阶段求全局Top K。如果数据分布极端不平衡比如某个小文件里的数据量远远超过其他文件分治策略可能失效需要使用哈希分区加二次哈希的方式保证数据均匀分布到各个分区。我在笔试时虽然没有写大段代码但把这些方案的分层逻辑讲清楚了得分应该不低。4.4 一道现场编程题完整复盘编程题第二题是一道字符串处理题目具体题目是“给定一个只包含大小写字母和空格的字符串反转字符串中每个单词的字符顺序同时保留单词之间的空格作为分隔符”。举个例子输入Lets go to the office输出应该是steL og ot eht eciffo。我当时的思路是先用空格把字符串切分成单词数组然后对每个单词做反转最后用空格拼接。这个思路在逻辑上没问题但Python或C里处理字符串需要特别小心连续空格和首尾空格的情况。我写出了完整代码#include string #include sstream #include vector #include algorithm std::string reverseWords(std::string s) { std::istringstream iss(s); std::vectorstd::string words; std::string word; while (iss word) { std::reverse(word.begin(), word.end()); words.push_back(word); } std::string result; for (int i 0; i words.size(); i) { if (i 0) result ; result words[i]; } return result; }用istringstream处理的好处是它会自动忽略连续空格word按空格分割不用手动处理分割逻辑。但这带来一个隐患如果题目要求保留原来的空格数量istringstream这种写法会把多个连续空格压缩成一个空格导致输出与要求不符。考场上的题目描述里写了“保留单词之间的空格作为分隔符”这意味着多个连续空格需要原样保留我后来复盘时意识到这个问题。如果要严格保留空格更好的做法是原地扫描字符串识别单词边界并逐字符反转。我复盘时重新实现了一遍先把整个字符串反转然后逐单词再反转回来这样空格数量就能原样保留std::string reverseWordsPreserveSpaces(std::string s) { int n s.size(); // 反转整个字符串 std::reverse(s.begin(), s.end()); // 逐个单词反转 int start 0; while (start n) { if (s[start] ) { start; continue; } int end start; while (end n s[end] ! ) { end; } std::reverse(s.begin() start, s.begin() end); start end; } return s; }这个版本的思路是先整体反转再局部反转每个单词从而保证单词内的字符顺序反转同时空格数量完全保留。这道题给我的教训是读题一定要仔细到“空格”这种细节不要想当然地用标准库的默认行为套上去。5. C与Linux后端笔试的硬核细节5.1 智能指针与内存管理C相关的选择题考了智能指针的使用场景要求区分unique_ptr、shared_ptr和weak_ptr的区别。这道题对于不写C的人可能觉得陌生但对C后端开发来说是日常功课。unique_ptr是独占所有权的智能指针同一时间只能有一个unique_ptr指向某个对象不能复制只能移动。它的优势是零额外开销比裸指针只多一个析构函数的逻辑适合对象生命周期明确的场景。shared_ptr是共享所有权的智能指针通过引用计数机制管理对象生命周期最后一个shared_ptr析构时释放对象。它的开销比unique_ptr大因为引用计数的增减需要原子操作多线程环境下还有线程安全问题需要注意。weak_ptr是配合shared_ptr使用的弱引用智能指针它不增加对象的引用计数用来打破shared_ptr之间的循环引用。笔试中关于智能指针最常见的坑是循环引用问题。两个对象各自持有一个指向对方的shared_ptr导致引用计数永远无法降为0对象永远不会被释放造成内存泄漏。解决方案是把其中一个指针改成weak_ptr访问时临时提升为shared_ptr再使用不增加引用计数。我在笔试答案里重点强调了智能指针不是万能的它解决了忘记释放内存的问题但没有解决垂悬指针的问题。用一个已经释放的对象的weak_ptr提升为shared_ptr时如果对象确实已经被销毁提升会失败返回空的shared_ptr需要判空后再使用。这个细节体现了对智能指针底层机制的真实理解。5.2 Linux命令实操题日志分析Linux相关的简答题考了一道“给定一个Nginx访问日志文件统计每个IP地址出现的次数并降序排序”。这是后端开发基本功考察grep、awk、sort、uniq这些命令的组合使用能力。Nginx访问日志的默认格式里IP地址是每条日志的第一个字段所以提取IP用awk {print $1}。统计每个IP出现次数用sort加uniq -cuniq -c的作用是统计相邻重复行的出现次数所以必须先排序再uniq否则相同的IP没有连续排列uniq统计会出现多条记录。最后按出现次数降序排序用sort -rn。完整的命令一行搞定awk {print $1} access.log | sort | uniq -c | sort -rn | head -n 10这个命令组合几乎是后端笔试的标配考点。我见过很多人只写了uniq -c忘了sort统计结果完全错误因为uniq只合并相邻重复行。另一个常考的变形是提取某个时间段内的日志可以用grep加正则匹配时间字段或者用sed取指定行号范围再配合awk做统计。当时笔试考的是发一段命令让你说出它的作用以及如果结果不符合预期从哪里排查。我的答案是先确认awk取出的字段是不是IP再确认排序方式和uniq的使用最后确认head截取的是不是前10条。这种排查思路比直接背命令更有用因为实际操作中95%的问题都出在这些简单的环节上。5.3 并发编程线程池设计思路选择题有一道关于线程池参数设计的题给了一个线程池假设核心线程数是4最大线程数是8阻塞队列长度是100问当提交第101个任务且所有线程都忙时会发生什么。这个题考察的是线程池的工作流程和拒绝策略。线程池的工作流程是提交任务时如果当前线程数小于核心线程数创建新线程执行任务如果当前线程数大于等于核心线程数先把任务放入阻塞队列如果阻塞队列已满且线程数小于最大线程数创建新线程执行任务如果线程数已经达到最大线程数执行拒绝策略。所以第101个任务是放到队列里的因为队列长度是100前100个任务已经占满队列的容载第101个任务提交时队列已满这时线程数如果没达到最大线程数就会创建新线程执行达到最大线程数就会触发拒绝策略。题目没给当前线程数所以答案是“取决于当前线程数是否已达到最大值”。这道题我后来把它整个推理过程记了下来作为复习线程池的框架。核心思想是线程池的运作本质上是“线程优先队列其次扩容兜底拒绝收尾”。理解了这个顺序不管是Java的ThreadPoolExecutor还是C的自定义线程池都能对应上。笔试时忘了拒绝策略有哪几种可以记住四种AbortPolicy抛异常、CallerRunsPolicy调用者执行、DiscardPolicy静默丢弃、DiscardOldestPolicy丢弃最旧任务。6. 系统设计题从零搭建一个短链服务6.1 需求拆解与容量估算系统设计题占了30分题目是“设计一个短网址服务要求支持长链接转短链接、短链接访问重定向到长链接并说明整体架构、存储方案、接口设计以及如何处理过期和并发”。容量估算我用了常规的假设每天新增短链接1000万个按有效期1年计算总存储量达到约36亿条这个规模单机肯定扛不住需要分库分表。每条短链接数据包括短码、原始长链接、创建时间、过期时间、访问次数等字段平均一条记录按500字节估算一年的存储量大约180GB这个量级用MySQL加上合理的分表策略可以支持但如果数据量再上一个数量级就要考虑分布式KV存储。接口设计我定义了两个核心接口一个是创建短链接输入长链接和过期时间输出短链接另一个是访问短链接输入短码返回原始长链接通过302重定向跳转。为什么用302而不是301这是个经典的考点。301是永久重定向浏览器会缓存重定向结果后续访问直接走本地缓存不会请求短链服务导致无法统计短链接的点击次数。302是临时重定向每次访问都会请求短链服务服务端可以记录访问日志做点击量统计和来源分析。对于短链服务来说统计能力是核心价值的一部分所以选302更合理。6.2 存储方案选型自增ID与发号器短链接的核心问题是短码如何生成。最直接的设计是让短码和原始长链接通过哈希算法对应但哈希可能冲突且哈希值太长不符合“短”的要求。行业内的主流方案是使用发号器先生成全局唯一ID再将ID映射为短码。发号器的核心要求是高可用、高并发、全局唯一。最简单的方案是用数据库的自增ID但单库自增ID有性能瓶颈也会暴露业务量被猜到ID规律还可能被遍历。更通用的方案是使用分布式ID生成算法比如雪花算法Snowflake。雪花算法是美团开源的一个思路后来被业界广泛借鉴生成的ID是一个64位的长整型由时间戳、机器ID、序列号三部分组成在单机内部通过时间戳加序列号保证同一毫秒内的ID唯一在不同机器之间通过机器ID保证不冲突。短码则通过进制转换生成将10进制的ID转换为62进制大小写字母加数字共62个字符可以显著缩短位数。比如10进制的ID转换为62进制后大约只需6到7个字符就能表达很大范围的ID。具体转换方式是不断除以62取余余数映射到字符表最后反转得到短码。发号器生成的ID基数越大短码位数越少访问效率越高。6.3 重定向与过期策略重定向的实现相对简单访问短码时先查缓存从缓存拿到长链接后直接返回302重定向。如果缓存未命中查询数据库查到后回填缓存并返回重定向查不到返回404。为了加速访问可以将热点短链提前放到Redis等缓存中减少数据库压力。过期策略需要单独考虑。一种方案是定期清理过期数据后台任务扫描超过过期时间的记录并删除但删除是写操作大量删除会给数据库带来压力也容易造成碎片。另一种方案是惰性删除访问时才检查过期时间过期了返回404并异步删除记录这种方式不会造成批量删除压力但存在过期数据占据存储空间的问题。实际系统里两种方案一般是结合使用后台定期清理兜底访问时惰性删除兜底确保两者配合。我在设计题里还考虑了并发场景。同一个短链接在缓存过期瞬间被大量请求打到数据库需要加互斥锁或者做原地更新避免缓存击穿。这恰好和前面选择题里考到的缓存穿透、缓存击穿呼应上了说明整套试卷的设计是有内在逻辑的。6.4 设计题的回答技巧与踩坑记录系统设计题最容易踩的坑是直接上手写表结构忽略了容量估算和架构设计的步骤。我在笔试时给自己定的答题顺序是先写需求分析再写容量估算然后写表结构和接口定义再画架构图最后写关键细节说明。这样即使后面的细节没写完前面的框架也能拿到一部分分数。容量估算这个步骤很容易被忽略但它恰恰是体现工程能力的地方。面试官看候选人有没有做过真实系统只要看这个候选人会不会算存储量、QPS、峰值流量就能判断出来。所以做系统设计题时不管题目有没有明确要求都应该主动补上容量估算。表结构设计方面我当时设计了一张短链表字段包括id、short_code、original_url、create_time、expire_time、visit_count然后对short_code建唯一索引。随着数据量增长可以按short_code的哈希值进行分表。缓存设计用Rediskey是short_codevalue是original_url设置合理的过期时间访问时先查缓存再查数据库数据库查询回填缓存。关于架构图我不会用mermaid但笔试纸上画图并不需要直接把架构分层列出来就行。我把整体架构分成四层接入层Nginx、服务层短链服务、缓存层Redis、存储层MySQL每一层说明职责和部署方式。这种分层描述的思路比画一张花哨的图更能体现对一个系统的掌控力。7. 常见问题与避坑指南7.1 笔试中的时间管理误区做这套卷子最大的时间陷阱是选择题花太多时间。20道不定项选择里有几道题比较烧脑比如那题线程池拒绝策略的推导如果再让我做一次我会把这种题先跳过最后再回来做而不是卡在上面浪费五分钟还影响心态。考场时间管理有一个实用建议拿到卷子先花两分钟把整张卷子浏览一遍标记出哪些题是送分题、哪些题需要思考、哪些题是完全没把握的然后按照“先做送分题再做需要思考的题最后啃硬骨头”的顺序做题。这个方法尤其适合基础题和编程题混在一起的卷子。7.2 读题不仔细导致的翻车前面提到的那道字符串反转题就是因为没有仔细读“保留空格”这个要求导致我用istringstream的处理方式会压缩连续空格输出结果不符合要求。这个教训非常深刻。还有一个经典的读题陷阱是“降序排序”还是“升序排序”Top K问题是选最大的100个还是最小的100个。这些细节直接在题目描述里写明但紧张状态下容易扫一眼就当作自己熟悉的题型来处理。我的建议是每道编程题和设计题都先把题目的关键要求用笔圈出来再动手写代码宁可多花30秒消化题目也不要花5分钟写完再推翻重来。7.3 代码风格与边界条件的注意点笔试编程题的评分标准里代码风格通常占有一定比重。写代码时注意命名清晰、有缩进、有注释这些细节会给阅卷人留下好印象。更重要的是边界条件的处理链表题要关注空链表和单节点链表字符串题要关注空字符串和首尾空格数组题要关注越界访问和数组长度为1的情况。在做任何操作之前先问自己“如果输入是空我的代码会不会崩”。我在准备阶段总结过一个边界条件自查表输入为空、输入长度为1、输入为最大值或最小值、输入包含重复值、输入包含特殊字符、输入处于临界值。把这些情况在代码里逐一验证基本能把大多数隐藏的bug排掉。7.4 我对这套题的整体评价这套试卷的整体难度在当年的后端笔试里属于中上水平它的难不在于题目有多偏而在于考察维度多、组合度高、需要动手写的东西多。选择题考的知识点如果单独拎出来都是常见的八股文但组合在一起就需要考生对操作系统、网络、数据库、C有全面的掌握。编程题难度不低但题型是常见的链表和字符串没有故意出冷门算法。系统设计题是最能拉开差距的部分也是这套试卷最有区分度的题目。做了这么多年后端开发回头再看这套题我最大的感受是它考察的不是刷题量而是一个人的计算机系统基础是否扎实。框架可以速成JVM调优经验可以背但进程线程、虚拟内存、TCP状态机、B树、并发控制这些底层知识真的需要花时间系统学习和长期积累。面试官通过这套题想筛选的就是那些真正理解计算机系统原理的候选人而不是只会写业务代码的工程师。如果你正在准备类似的技术笔试我的建议是多花时间啃底层原理亲手写代码验证每一个结论并且在做完每套题后认真复盘错题背后的知识点。笔试考完不是结束把不会的题彻底弄懂才是这套题对你真正的价值所在。
返回列表