ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

美团2020校招后台开发笔试题复盘:从并发到系统设计

美团2020校招后台开发笔试题复盘:从并发到系统设计 说实话美团2020校招后台开发方向的笔试题放在今天依然很值得拿出来反复咀嚼。原因很简单这一年的题目风格非常典型它既不是那种纯粹的LeetCode刷题竞赛也不是靠背八股文就能应付的题库而是把算法基本功、Java并发、分布式场景和业务设计揉在一起考。备考过这几年的同学应该都有感觉美团笔试的调性一直很稳定就是“考你写代码之外能不能处理真实后台系统里那些难缠的问题”。这篇文章我会站在后台开发求职者的角度把2020年这套笔试完整复盘一遍。内容包括题型分布、高频考点的解题思路、现场时间分配、容易踩的坑以及一类题目的通用答题框架。无论你是准备暑期实习还是秋招冲刺这篇文章都值得你花半小时认真读一遍——因为很多公司在后台开发笔试中的出题逻辑本质上都是相通的。1. 试卷整体结构与命题逻辑1.1 题型分布与分值结构2020年美团后台开发方向笔试整体分为选择题、编程题和问答题三大部分。不同批次的题量略有浮动但整体结构非常固定我当时考的那场大概是这样题型题量每题分值建议用时考察重点单选题20题左右2分25分钟数据结构、操作系统、网络、Java基础多选题10题左右3分15分钟并发、JVM、数据库原理、分布式理论编程题3题20~30分60分钟算法与数据结构、边界处理能力问答题1~2题20~30分30分钟系统设计、场景方案、工程思维这套题最能拉开差距的地方不在选择题而在编程题和问答题。选择题基本就是基础知识的快速筛查很多知识点只要你系统刷过牛客网的面经都能眼熟。真正决定你能否进入面试的是你在编程题里能不能快速写出无Bug的代码以及问答题里能不能展现出一个后台开发工程师应有的系统设计思路。1.2 命题背后的技术栈信号我一直觉得笔试题目能反映出一家公司的技术生态。美团后台开发以Java为主所以卷子里Java并发和JVM的题目占比非常高。你如果用的是C或Go去投后台岗位选择题部分大概率会吃亏因为很多考点本身就是Java语法层面的。另外美团的业务场景决定了它的命题偏好。外卖、到店、酒旅这些业务每时每刻都在产生海量订单系统要处理高并发写入、状态流转、分布式事务、缓存穿透等一系列问题。所以2020年的问答题里基本绕不开订单状态机设计、缓存一致性、分布式ID生成这些经典场景。这给我们一个很重要的信号备考美团笔试不能只看纯算法要对“一个高并发后台系统的常见组件和它们要解决的问题”有整体认知。分布式理论、消息队列、缓存、数据库索引这些都属于后台开发的基础素养笔试会考面试会问工作后每天都在用。2. 高频重点题型拆解与解题思路2.1 Java并发编程题美团笔试的“钉子户”2020年选择题里并发相关考点大量出现。比如volatile关键字的作用、synchronized的锁升级过程、ThreadLocal的内存泄漏问题、ConcurrentHashMap在JDK 7和JDK 8中的实现差异还有线程池的核心参数和拒绝策略。其中有一道让我印象非常深刻的多选题大致意思是关于线程池ThreadPoolExecutor以下哪些说法是正确的选项里有“核心线程数会被回收吗”“任务队列满时会触发什么策略”“CallerRunsPolicy到底在哪里执行被拒绝的任务”这类细节。很多人在这道题上栽了跟头原因是他们对线程池的理解停留在背参数层面而没有真正理解线程池的工作流程。请记住一个非常朴素的判断方法线程池在执行任务时先判断核心线程是否已满——没满就创建新线程执行满了就把任务丢进阻塞队列队列也满了才判断最大线程数——没满就继续创建线程如果线程数已经达到最大值就执行拒绝策略。这个流程必须像背自己的身份证号一样烂熟于心。我建议你动手画一遍或者直接打开源码把execute()方法的逻辑读一遍。理解了顺序很多选择题不用背答案也能推出来。比如CallerRunsPolicy它是在调用者线程里同步执行被拒绝的任务而不是丢弃任务这在业务上是一种降级而非放弃所以更适合不希望丢消息的场景。再补一个高频考点线程池线程数是应该设置成CPU密集型还是IO密集型的讨论。选择正确答案不需要背公式核心逻辑是CPU密集型任务希望减少线程切换所以线程数接近CPU核心数IO密集型任务大部分时间在等待可以多一些线程来提高吞吐。美团内部很多服务是IO密集型的所以这个点反复出现并不意外。我当时做题的心得是并发学习不能停留在“能说出概念”要能在纸上把过程写清楚。你把“线程A拿到锁之后发生了什么”“volatile写屏障对这个变量的读有什么影响”写一遍才算真懂。2.2 数据结构与算法题稳、准、快是唯一标准2020年三道编程题我记得第一题是字符串处理第二题是栈和队列的变种应用第三题是动态规划。都是中等难度比LeetCode周赛的压轴题要温和但题量大、时间紧所以对熟练度的要求非常高。我拿第一题举例。题目大意是给定一个字符串要求找出所有满足特定条件的子串数量。这类题在LeetCode上能找到大量类似题目滑窗或者前缀和就能解。但笔试环境没有提交反馈你必须一气呵成写对。这种时候编码习惯就特别重要了。我建议你把常用算法模板练到肌肉记忆尤其是这几类滑动窗口模板用于子串、子数组问题单调栈模板用于下一个更大元素、柱状图最大矩形并查集模板用于连通性问题排序的变种应用比如TopK的堆解法动态规划的状态定义和转移方程写法编程题最容易翻车的不是思路而是实现的边界条件。比如字符串题里空串、单个字符、重复字符这些Case一定要在动手写代码前先想清楚。宁可前面多花两分钟想Case也不要写完再调试因为很多在线笔试系统一旦你切出去调试切回来还耽误时间。我当时写完每道题都会花30秒做一个“杠精自查”变量越界没有循环会不会死循环取模有没有必要还有没有重复代码这个小习惯帮我避免了至少一次数组越界的尴尬。2.3 LRU缓存与“变形题”的通用解法这里我想多说一个几乎年年出现的角色LRU缓存淘汰算法。2020年笔试没有直接考LRU原题但考了一道它的变种要求设计一个数据结构支持按访问频率淘汰的LFU缓存。LRU和LFU的核心都是“如何在O(1)复杂度内完成插入、查找和淘汰”。LRU的标准解法是HashMap加双向链表HashMap负责O(1)查找双向链表负责维护访问顺序。面试官如果让你手撕代码你起码要把这个写出来class LRUCache { class DLinkedNode { int key, value; DLinkedNode prev, next; DLinkedNode() {} DLinkedNode(int key, int value) { this.key key; this.value value; } } private MapInteger, DLinkedNode cache new HashMap(); private int size; private int capacity; private DLinkedNode head, tail; public LRUCache(int capacity) { this.size 0; this.capacity capacity; head new DLinkedNode(); tail new DLinkedNode(); head.next tail; tail.prev head; } public int get(int key) { DLinkedNode node cache.get(key); if (node null) return -1; moveToHead(node); return node.value; } public void put(int key, int value) { DLinkedNode node cache.get(key); if (node null) { DLinkedNode newNode new DLinkedNode(key, value); cache.put(key, newNode); addToHead(newNode); size; if (size capacity) { DLinkedNode tailNode removeTail(); cache.remove(tailNode.key); size--; } } else { node.value value; moveToHead(node); } } }LFU则麻烦一些需要“频率”维度的维护。常见解法是维护一个频率到链表桶的映射每个节点保存访问频率同时维护一个最小频率变量。代码量比LRU大不少但核心思想还是“用空间换时间用HashMap保证O(1)访问”。这类题目给我的经验是不要只会背模板要理解为什么双向链表在这里优于ArrayList。ArrayList删除中间元素是O(n)而双向链表在已知节点指针的情况下可以做到O(1)。理解了这一点即使题目再变形成“按插入顺序淘汰”或“按访问时间过期”你也知道该用哪种数据结构去打底。3. 操作系统、网络与数据库必背知识点压测3.1 操作系统高频题死锁、进程调度和IO模型操作系统在美团笔试题里占的分值不多但经常出现在多选和问答题的角落。2020年我记得有一道关于死锁的题问的是“下列哪些是产生死锁的必要条件”。答案自然是互斥、持有并等待、不可剥夺、循环等待四选。看起来简单但很多人会在“循环等待”和“持有并等待”之间产生犹豫。这里有一个记忆技巧死锁的本质是资源被不同进程互相咬住所以必要条件一定绕不开“持有并等待”和“不可剥夺”。如果没有“持有并等待”进程无法一边占着资源一边等别的资源如果资源可剥夺系统早就抢过来重新分配了。还有一道选择题考了进程和线程的区别选项里有一个非常经典的干扰项“线程拥有独立的地址空间”。这是错的进程才拥有独立的地址空间同一进程内的线程共享地址空间。这个考点出现的频率极其高不光美团几乎所有大厂校招笔试里都有它的影子。IO模型也没有缺席。选择题里出现过同步阻塞IO、同步非阻塞IO、IO多路复用和异步IO的区分。美团后台的高并发服务离不开NIO和Netty所以IO模型这个点其实是在为后续面试聊RPC框架做铺垫。你至少要能说出BIO是一个线程处理一个连接连接多了线程就爆炸NIO用一个线程配合多路复用器管理大量连接适合高并发场景AIO是操作系统帮你准备好数据再通知你但实际工程中用得不多。3.2 计网与数据库高频题TCP、索引与事务隔离级别计算机网络和数据库部分是后台开发笔试的重头戏。TCP三次握手、四次挥手几乎是必考的但美团不会直接问你“为什么是三次不是两次”而是会换一个场景客户端突然断电会发生什么、服务端能感知到吗这种题考察的是你对TCP状态和保活机制的理解。数据库这块索引相关的题最常考。比如“联合索引(a,b,c)查询条件where b? and a? 能不能走索引”这类题考察的是最左前缀原则。我的建议是遇到这种题先不要急先把联合索引的B树结构画出来然后判断查询条件的顺序是否满足最左匹配。事务隔离级别也是美团笔试的心头好。读未提交、读已提交、可重复读、串行化每个级别解决了什么问题、还存在什么问题必须能清晰说出来。MySQL默认是Repeatable Read而Oracle和PostgreSQL默认是Read Committed这个对比记忆会让你在选择题里反应更快。再补一道典型的SQL题。题目大意是有一张订单表字段包括order_id、user_id、amount、create_time要求查出每个用户下单金额最高的前两条订单记录。这道题考察的是分组TopN在MySQL 8.0里可以用窗口函数优雅解决SELECT user_id, order_id, amount FROM ( SELECT user_id, order_id, amount, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY amount DESC, order_id ASC) AS rn FROM orders ) t WHERE rn 2;如果是MySQL 5.7环境就要用变量或者自连接去模拟。笔试里如果允许优先用窗口函数代码简洁、不容易出逻辑错误。但也要注意如果你的目标公司内部还跑着5.7面试官可能会追问“窗口函数底层怎么实现”那就需要你额外了解物化排序的过程了。4. 系统设计题与场景题的答题框架4.1 订单状态机如何设计才能“无懈可击”2020年美团问答题里有一道很典型的场景题请设计一个外卖订单状态机要求考虑异常情况比如支付超时、骑手取消、用户异常退款。这道题虽然没有标准答案但非常能考察一个人的工程思维。很多人上来就列状态待支付、已支付、配送中、已完成。然后呢然后就没有然后了。这种回答拿不到高分因为它没有体现对业务复杂性的理解。我建议用下面这个框架来答题第一步穷举所有可能的状态。外卖订单至少包括待支付、已支付、备餐中、待取餐、配送中、已完成、已取消、退款中、已退款、异常单。第二步明确每个状态可以合法迁移到哪些状态画出状态流转关系。第三步重点描述异常分支待支付超时15分钟后自动取消配送中用户申请退款系统要先冻结骑手结算流程退款中如果骑手已经取餐需要先触发拦截逻辑而不是直接改状态。这个思路的核心是“状态机只允许合法迁移非法操作直接拒绝”。实现上可以用一个二维矩阵表示状态之间是否可达也可以用策略模式封装每个状态的迁移逻辑。业务系统中状态机往往配合数据库里的状态字段加乐观锁版本号一起使用防止并发请求把状态改乱。这里有个我从实际项目里学到的坑状态机一定要把“重复通知”和“乱序通知”考虑进去。比如支付回调因为网络超时被消息队列重发了两次如果你的状态机是幂等的第二次回调直接忽略就好。但如果你没有做幂等就可能把已支付的订单重新改成待支付这就是重大事故了。美团业务里有一个核心概念叫“核销”尤其在外卖和到店场景下特别重要。到店团购用户付款后需要到商家现场验证消费这个验证动作就是核销。核销系统的稳定性直接关系到资金安全所以设计核销类系统时必须考虑防重复核销、防并发核销和核销失败补偿。笔试里如果遇到类似“到店核销流程设计”的题目你往并发控制和幂等方向答基本不会偏。4.2 设计一个短链接系统从哈希到发号器的思维进阶另一类非常常见的问答题是设计一个短链接系统。这题表面上是系统设计实际上考的是哈希算法、存储选型、重定向状态码和缓存策略的综合运用。你首先要说清楚短链接的生成算法。最简单的方案是取原始URL的MD5值截取前6到8位作为短码。但MD5取前缀会有碰撞风险碰撞后怎么处理更工程化的方案是发号器思想用一个全局自增ID再将其转为62进制字符串这样每个原始URL都能唯一对应一个短码且不会碰撞。有了短码之后存储怎么选短码到原始URL的映射可以用MySQL存储也可以用Redis缓存热点数据。访问量大的短链接在重定向时如果每次都查数据库数据库扛不住所以大部分读请求应该落到Redis上只有缓存未命中才回源数据库。最后再说HTTP状态码。301和302都能实现重定向但语义不同。301是永久重定向浏览器会缓存这个跳转之后的访问直接绕过短链接服务302是临时重定向每次访问都会请求短链接服务。如果短链接服务要做访问统计就必须用302否则统计到的数据会少一大截。这道题的答题层次很清晰先给方案再说存储再说缓存再说状态码。即使你没有完整做过类似系统按照“入口—处理—存储—回源”的思路答也能让面试官看到你有工程化思考的能力。5. 笔试环境、时间分配与细节坑5.1 机考环境与ACM模式的输入输出美团笔试用的是牛客网的系统编程题要求手写完整输入输出也就是我们常说的ACM模式。这和LeetCode的核心函数模式不一样如果你平时只用LeetCode刷题第一次上考场很容易在输入解析上翻车。我记得2020年有一道字符串题输入是一行字符串但中间可能包含空格。如果你用next()去读取就会只读到空格前的一部分导致答案错误。正确做法是用nextLine()读取整行再按题目要求做处理。另一个常见问题是输出格式。题目要求输出结果用空格分隔很多人最后多打了一个空格导致格式错误被判0分。建议养成“先输出第一个后面每个前面加空格”的习惯不要拼一个长字符串再输出容易最后多一个尾巴。时间分配上我的策略是拿到卷子先花5分钟把所有题目扫一遍给每道题打一个难度标签。选择题控制在40分钟内做完不会的先标记跳过不要死磕。编程题先做最有把握的那道把基础分拿到手再处理稍微复杂的动态规划题。最后至少留10分钟检查选择题尤其是多选题少选、多选都会扣分不要凭感觉。5.2 边界Case与防御性编程像“杠精”一样审视代码这道题目虽然不算分但我想单独拿出来说用“反向思维”检查代码。这里的反向思维是指把自己当成一个专门挑毛病的测试工程师用各种极端Case去攻击自己刚写完的代码。比如写数组题时要问自己数组为空怎么办数组长度为1怎么办元素全是负数怎么办结果会溢出吗如果答案是“我还没想清楚”那就说明代码有隐患。再比如写二分查找时大部分人写的都是while (left right)但有的人更新边界时left mid 1、right mid到底怎么搭才是正确的我的习惯是先用两个元素的小数组自己走一遍循环如果走得通再往大数组推广。写代码时花30秒手推边界远比提交后反复调试划算。多写一些防御性判断对笔试也有帮助。笔试自动判题时往往同时有隐藏测试用例这些用例里大概率包括空输入、超大输入、边界值等恶心Case。你提前做了防御就是在为自己“凭空加分”。5.3 面试复盘笔试结束后的三件事笔试结束不代表什么都结束了。很多同学考完就丢掉题目这是一个非常可惜的习惯。我建议每场笔试结束后花30分钟做三件事第一把不确定的选择题查清楚。选择题的选项本身就是知识点把四个选项为什么对、为什么错都弄清楚这是效率极高的查漏补缺方式。第二把没写出来的编程题重新做一遍。笔试时可能时间不够但下来后一定要把这题写通、写熟练。第三把问答题的答题框架优化一遍整理进自己的题库。你整理过的框架越多下次遇到新题时调用知识的速度就越快。当时我把美团这场的问答题和牛客网上其他同学回忆版整合了一下总结出一套“场景题四步法”先抽象需求再拆分模块再点出关键技术点幂等、缓存、消息队列、分布式锁最后给出异常处理方案。这套方法我后来面其他大厂时也一直在用反响都还不错。6. 从笔试看后台开发的核心竞争力复盘完2020年美团后台开发笔试题我发现一个很明显的趋势头部互联网公司的笔试已经不再满足于“你会不会写这道算法题”而是想通过笔试看你有没有“作为一个后台开发工程师的基本盘”。这个基本盘包含三块能力。第一块是扎实的计算机基础数据结构、操作系统、网络、数据库这些是无论你用什么语言、在什么公司都绕不开的底层逻辑。第二块是Java生态下的工程能力尤其是并发编程、JVM调优和分布式中间件的使用这决定了你入职后能不能快速上手业务系统。第三块是系统设计意识同样一个问题初级工程师会给方案高级工程师会考虑异常链路、扩容方案和成本控制。美团这套题之所以经典就是因为它在120分钟内把这三块能力都测了一遍。算法题给你压力问答题给你场景选择题则像一面镜子专门照出你的知识盲区。我个人在备考阶段最大的体会是不要盲目追求刷题数量而要学会“以题带点”。每做一道题把它背后的知识点、可能的变化形态、同类题目的解题套路都梳理清楚。你以为你是在准备一场笔试实际上你在为后面所有的技术面试和工作实战打地基。这套方法我从校招一直用到现在带新人依然有效。
返回列表