ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

掌阅后端笔试复盘:Java基础、缓存设计与场景题全解析

掌阅后端笔试复盘:Java基础、缓存设计与场景题全解析 去年秋招那阵子我投了掌阅科技的后端开发岗。简历过了之后收到的第一封笔试通知比想象中来得快要求在牛客网系统里完成一场限时笔试。当时我一边刷着面经一边心里打鼓等真正打开试卷的时候才发现这套题和我预想的“纯刷八股”不太一样——它既有大量Java基础和数据库选择题也有不少贴近阅读App业务场景的开放问题算法题反而没那么刁钻。考完之后我把整套题复盘了一遍发现很多考点其实都有很明确的考察意图。这篇文章就把我对这套考题的整体印象、题目背后的知识点拆解、以及我当时踩过的坑和复习思路完整写出来给后面准备掌阅或者其他阅读类App后端岗位的同学做一个参考。1. 笔试的整体印象题型构成与答题节奏1.1 掌阅这套笔试题的基本盘先说整体结构。掌阅2023年秋招后端岗的笔试是纯在线笔试总时长我记得是90分钟题量不算小。整套试卷大概分成三块第一块是技术选择题大约25到30道覆盖Java基础、集合源码、JVM、并发编程、计算机网络、MySQL和Redis第二块是2到3道简答题或者场景设计题这一块比较考验工程思维不是单纯背概念就能答好的第三块是2道算法题难度集中在LeetCode中等偏下时间充裕的话完全做得完。整体来看这套题比很多大厂动辄五六道算法题的笔试要温和不少它更看重你作为后端工程师的“基本功场景落地能力”而不是纯粹刷题机器。换句话说如果你Java基础和数据库这块复习得扎实同时又能结合业务思考问题掌阅这套笔试过线并不难。1.2 时间分配的策略90分钟做这么多题时间分配直接决定胜负。我当时的策略是选择题限时40分钟不纠结任何一道题超过2分钟简答题和场景题留30分钟最后20分钟集中做算法题。这样做的好处是保证前面那些“稳拿分”的选择题不会被拖垮因为选择题分值往往很平均一道题就一分两分但错多了很伤。有个细节必须提醒牛客网这种在线考试系统题序是固定的但你可以任意跳题。所以遇到读一遍没思路的选择题先标记等做完所有有把握的题目再回头猜一个答案。我这次就遇到一道关于JVM垃圾回收器的选择题选项里有两个很接近的描述果断先跳过最后回来对比了一下才选出来。这种“先易后难再捡漏”的顺序在任何在线笔试里都成立。1.3 选择题里的行测陷阱对了这套题里居然还混了几道类似行测的逻辑推理题比如数字规律、图形推理之类。说实话那几道题我完全没有准备浪费了好几分钟。后来复盘的时候发现这种题通常在整张卷子的靠前位置有点像是考察你在压力环境下能不能快速切换思维模式。不过对技术岗来说这种题分值占比不高一旦感觉卡住建议立刻跳过不用恋战。毕竟后端笔试的核心筛选标准还是技术能力逻辑题只是辅助观察项。2. 计算机基础选择题数据结构、网络和操作系统的考察重点2.1 数据结构题HashMap和B树是高频中的高频掌阅这套选择题里数据结构相关的题目大概出了5到6道考察范围比较常规数组和链表的区别、栈和队列的应用、二叉树遍历、哈希表冲突解决和B树的特性。其中最值得说的是HashMap和B树。HashMap相关的那道题我记得很清楚题目大概是问“JDK 1.8中HashMap在链表长度达到多少时会转成红黑树”。答案是8这个不难但选项里有个干扰项是“HashMap允许null键和null值”很多人容易记混Hashtable和HashMap。实际上HashMap允许一个null键和多个null值而Hashtable不允许。如果复习的时候只看“八股”不细究这种题真的容易翻车。B树的题则结合了InnoDB索引问的是“B树相比B树的优势在哪”。核心答案是“B树所有数据都存在叶子节点并且叶子节点之间用链表连接适合范围查询”。这个知识点我建议往深里准备因为掌阅这种场景题经常会问到分页查询优化底层原理就是B树。2.2 计算机网络TCP握手和HTTP状态码的常规考法网络这块掌阅考了两道比较经典的选择题TCP三次握手过程中客户端第二次发送的报文段是什么以及HTTP 301和302的区别。三次握手那道题考察的是标志位第一次握手客户端发送SYN1的报文第二次服务器回复SYN1、ACK1的报文第三次客户端发送ACK1的报文。这个如果理解不到位很容易和断开连接的“四次挥手”混淆。我复习的时候是把整个握手挥手过程画了一张时间序列表把每次交互的发送方、接收方、标志位和携带信息都列出来这样考试就不容易乱了。HTTP 301和302的考点也很有意思301是永久重定向浏览器会缓存重定向结果302是临时重定向每次请求都可能重新询问服务器。掌阅作为一个阅读类App后端接口经常涉及资源迁移和临时跳转所以这个点在真实业务中也很有用。选择题的考法一般是给你一个场景比如“用户访问旧书城域名需要永久跳转到新域名应该返回什么状态码”答案就是301。2.3 操作系统进程线程和死锁操作系统的考点比较基础只考了进程和线程的区别、死锁产生的四个必要条件。这几个概念属于送分题但有一个很容易忽略的点上下文切换开销。题目问“线程比进程切换开销小的根本原因是什么”很多人会答“线程共享内存”但这并不准确。更本质的原因是同一进程内的线程切换不需要切换地址空间而进程切换必须切换页表、刷新TLB开销自然更大。这道题提醒我们复习基础知识不能只记结论要追到“为什么”的层面。死锁那道题的四个必要条件是互斥、占有并等待、不可剥夺、循环等待。选项里通常会有一个“资源分配公平”的干扰项看清楚就行。3. Java与数据库选择题里隐藏的深度陷阱3.1 Java基础题集合、线程池和JVM的易错点Java这块是掌阅笔试的重头戏至少占了选择题的三分之一。我印象最深的有三道题ArrayList和LinkedList的区别、线程池的核心参数执行流程、以及JVM堆内存的划分。ArrayList和LinkedList的区别几乎是送分题但掌阅的选项设置很刁钻它没有直接问“谁查找快谁插入快”而是给了四个描述组合。比如“ArrayList在指定位置插入元素的时间复杂度是O(n)”这个是对的因为要移动元素又比如“LinkedList实现了RandomAccess接口”这个是错的LinkedList没有实现RandomAccess。如果只记得“ArrayList基于数组、LinkedList基于链表”这种粗粒度结论这道题就会卡住。线程池那道题考察的是ThreadPoolExecutor的核心参数执行顺序。题目场景是核心线程满了接着来任务怎么办答案是“先放入阻塞队列队列满了才创建非核心线程非核心线程也满了才执行拒绝策略”。这个顺序很多人会记反以为是“先创建非核心线程再入队”。实际上JDK的设计是优先利用队列缓冲避免频繁创建销毁线程。我复习的时候把这个执行流程画成了一个判断流程图核心线程是否满 - 队列是否满 - 非核心线程是否满 - 拒接策略然后对着源码逐行过了一遍这样理解的记忆才牢固。JVM堆内存的题也不难问的是Eden区和Survivor区的比例。标准的答案是默认Eden:Survivor0:Survivor1 8:1:1。但这里有个隐藏考点这个比例是可以通过参数调整的而且Minor GC之后存活对象会从Eden区复制到Survivor区如果Survivor区放不下会通过担保机制直接进入老年代。掌阅的选项里就有这样一个干扰项它说“Minor GC之后所有存活对象都会进入老年代”显然是把特殊情况误当成了通用规则。3.2 MySQL索引和事务隔离级别是拿分关键数据库题考了大概5道集中在MySQL上。索引相关的有两道题一道问的是InnoDB的聚簇索引和数据行存储的关系另一道是“SQL查询中哪些写法会导致索引失效”。聚簇索引这道题的正确答案是“InnoDB表的数据行实际存储在聚簇索引的叶子节点上二级索引的叶子节点存储的是主键值”。这就意味着通过二级索引查询时如果查询字段不满足覆盖索引条件需要回表到聚簇索引里再查一次。掌阅的选项里有个干扰项说“MyISAM也支持聚簇索引”这个明显是错的。复习的时候要把两种存储引擎的索引结构对比着记MyISAM索引文件和数据文件分离索引叶子节点存的是数据行的物理地址InnoDB聚簇索引叶子节点直接存数据行。索引失效的题更贴近实战题目问“下列哪个查询不会使用name索引”选项包括对name字段做了函数计算、使用了前导模糊查询、列类型是varchar但传了数值型参数等。正确答案是“对字段做函数计算”。但这里有个细微之处在MySQL 8.0中某些情况下函数索引是可以使用的但默认情况下对索引字段使用函数确实会导致索引失效。这种题考察的是日常工作里最常见的SQL优化问题掌阅作为阅读平台书籍信息表、用户表的数据量非常大SQL效率直接影响接口响应时间所以考察这个点很合理。事务隔离级别那题考察的是MySQL默认隔离级别以及各种级别下可能出现的并发问题。答案很明确MySQL默认是可重复读REPEATABLE READ这个级别下理论上避免了脏读和不可重复读但依然存在幻读问题。不过需要知道的是InnoDB通过间隙锁在可重复读级别下很大程度上解决了幻读这也是为什么很多大厂面试官会说“InnoDB的可重复读基本实现了串行化下才有的一致性”。掌阅这题就考到了间隙锁选项里说“可重复读级别下完全不存在幻读”其实就是没考虑间隙锁的实现机制。3.3 Redis缓存穿透、击穿和雪崩的经典场景题Redis在选择题里出现了两道而且都和阅读App的业务高度相关。一道是缓存穿透的解决方案另一道是缓存和数据库一致性的更新策略。缓存穿透的题干是“大量请求查询一个不存在的书籍ID导致请求直接打到数据库如何解决”。标准答案有两个布隆过滤器拦截和缓存空值。布隆过滤器适用于数据量极大且不允许误判的场景而缓存空值实现简单但可能造成短暂的不一致。选项里有个坑是“使用分布式锁”这其实是解决缓存击穿的手段不是穿透。二者名字只差一个字但应对场景完全不同穿透是查不存在的数据击穿是热点key过期瞬间大量请求打到数据库。笔试这种题其实是在考察你有没有真正理解概念之间的边界而不只是背名词。缓存一致性那道题问的是“更新数据库后应该删缓存还是更新缓存”。大部分技术方案都会选择“删除缓存”因为更新缓存会产生并发写覆盖的问题而且如果缓存数据需要复杂的计算逻辑每次更新都计算一遍很浪费。正确答案是“先更新数据库再删除缓存”或者“延迟双删”。但延迟双删中的“延迟”时间怎么确定这里有一个容易被忽略的点它取决于从数据库读数据到写回缓存之间所需的时间一般经验值是几百毫秒到一秒具体要根据业务响应量级来调整。掌阅作为阅读App书籍信息、章节内容都是典型的读多写少场景缓存一致性方案必须兼顾性能和最终一致性这道题考得很对口。4. 场景设计题数字阅读平台的高并发读写问题4.1 热门书籍的缓存设计从缓存穿透到多级缓存简答题第一道我印象很深刻题目大致是“掌阅是一个数字阅读平台热门书籍会被海量用户同时访问请设计一个缓存方案说明如何应对缓存穿透、击穿、雪崩并保证缓存和数据库的一致性。”这种题一看就是开放式的没有唯一标准答案关键是看你的回答有没有结构性。我当时的回答分成了三层第一层是缓存架构选型第二层是三种异常的应对策略第三层是一致性保障。缓存架构选型我直接说用Redis作为热点数据缓存层在Redis前面再加一层本地缓存比如Caffeine做二级缓存。原因很简单本地缓存访问速度最快可以挡住相当比例的热点请求Redis作为分布式缓存层承担多实例共享和缓存失效后的回源压力。这种“本地缓存分布式缓存”的二级架构在阅读App这种高并发读场景下是非常常见的做法。缓存穿透方面我给出了两个方案一是对不存在的书籍ID设置短时间空值缓存比如60秒二是用布隆过滤器预判书籍ID是否存在。如果把两者结合会更好布隆过滤器挡住大部分非法请求空值缓存兜底。缓存击穿方面我是这么回答的对热点书籍key不设置过期时间而是在value里保存一个逻辑过期时间由后台异步线程更新缓存。这样可以彻底避免热点key过期瞬间的并发回源问题。另一种方案是互斥锁也就是当缓存失效时只有一个线程去查库并回填缓存其他线程等待。但因为互斥锁在极端高并发下会造成短暂阻塞体验下降所以在阅读场景下我更倾向于逻辑过期方案。缓存雪崩方面的回答是给过期时间加随机值避免大量key在同一时刻失效。比如基础过期时间设成24小时实际过期时间在24小时基础上加一个0到300秒的随机数。这样即使用户同时请求大量书籍也不会让所有缓存同时触发回源。至于一致性保障我给出的方案是先更新数据库再删除缓存再对关键数据采用延迟双删。延迟双删的大致逻辑是更新数据库、删除缓存、等待几百毫秒、再次删除缓存。之所以要第二次删除是为了避免并发场景下某个线程在第一次删除缓存之后、更新完成之前把旧数据重新写回缓存。这个简短的回答包含了完整的技术链条正好涵盖了选择题里出现的那些考点笔试的时候就是要把它们串起来用。4.2 用户书架与阅读进度海量数据下的存储设计第二道场景题更贴近掌阅的核心业务用户书架和阅读进度存储方案。题目给了一个前提——掌阅用户量接近亿级每个用户的书架最多有几百本书需要保存每本书的最新阅读进度。我的回答分两步走先做存储选型再考虑读写优化。存储上书架信息和阅读进度其实读写特点不一样。书架列表是“读多写少”适合用MySQL保存结构化数据按用户ID分库分表阅读进度是“读多写多”每一次翻页都可能上报如果全部写MySQL压力会非常大所以一定要引入Redis作为进度上报的一层缓冲。比如用户在阅读时客户端每翻一页就上报一次进度后端先写入Redis然后异步批量落库到MySQL。这样既保证了用户阅读进度的实时性又不会给数据库带来太大压力。分库分表的设计我也展开了书架表和阅读进度表都通过用户ID的哈希值进行水平拆分比如拆成128张表分布在多个实例上。这样单表数据量可控同时按用户维度路由也天然支持了读操作的隔离。对于“用户在一个书架上还有几百本书”这种场景分页查询是必须的要尽量避免深度分页。实现上可以用延迟关联或者游标分页效果都比传统offset分页好得多。这道题考察的核心不是你会不会写SQL而是你有没有考虑过数据规模增长之后系统的走向。掌阅这种体量的平台后端设计必须有面向海量数据的意识所以回答时要突出“分层存储、读写分离、异步化”的思维。4.3 阅读排行榜与搜索Redis和ES的落地第三道场景题我记得是关于排行榜的需要实现一个“书籍热度排行榜”按小时内阅读人数实时更新。这个题直接指向Redis的有序集合ZSET。我的回答是用ZSET的key表示榜单时间段value是书籍IDscore是阅读人数。每次有用户阅读时ZINCRBY增加对应书籍的score。排行榜查询直接用ZREVRANGE取前N名时间复杂度是O(log(N)M)性能完全扛得住。如果要计算“实时热度”而不是累计热度就需要用滑动窗口。具体做法是用多个时间段的小ZSET比如每5分钟一个key查询时聚合最近12个小窗口的数据。这种滑动窗口方案在博主后台和短视频平台的热榜实现里非常常见。搜索这块虽然没有单独出题但结合掌阅的业务不难想到它很可能在后续面试中问到。书籍搜索一般靠Elasticsearch核心是倒排索引和中文分词。如果笔试时间允许把ES的写入链路和查询链路简要提一下能体现你的知识面比较完整书籍信息变更后通过MQ异步同步到ES搜索请求直接查ES而不是查MySQL。5. Spring生态与项目题如何在笔试里展现工程能力5.1 Spring Boot自动配置原理掌阅这套笔试题对SpringBoot框架的考察比我想象中少只在简答题里出现了一道简述Spring Boot自动配置的原理。但就是这道题很多人答不到点子上。标准回答分三步走第一Spring Boot在启动时会读取META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件这个文件里列出了所有自动配置类第二每个配置类上都有Conditional系列注解比如ConditionalOnClass、ConditionalOnMissingBean只有当满足条件时才生效第三配置类通过Bean方法创建实例并注册到容器中。如果你把这三步都答上基本就能拿到大部分分数。我当时还额外加了一句自动配置的本质是“约定大于配置”Spring Boot通过大量的默认值减少了开发者的配置负担但这个默认值的背后是通过条件注解实现的动态装配。这样答能够体现出你读过源码而不是只背过面经。5.2 前后端分离项目怎么描述笔试中有一道项目题要求描述一个你做过的后端项目并说明其中遇到的难点和解决方案。从掌阅的技术栈来看它明显倾向Java后端所以项目描述最好围绕Spring Boot MyBatis/MyBatis-Plus或者JPA展开。我当时写的是一个仿阅读App的后端项目用Spring Boot实现用户注册登录、书籍管理、书架管理、阅读进度上报等功能模块。前端用Vue做了一套管理后台后端通过RESTful API提供接口用JWT做登录态校验数据库用MySQL缓存用Redis。项目部署用了Docker Compose把MySQL、Redis、Spring Boot应用各跑一个容器。在描述难点时我特意提到了两个点第一个是登录态的安全设计我把JWT过期时间拆成了短期access token和长期refresh tokenaccess token有效期2小时refresh token有效期7天刷新接口可以对令牌进行轮换同时用Redis记录了refresh token的版本号防止同一个token被重复使用。这一点和掌阅作为移动端App对登录态的持续性和安全性要求是匹配的。第二个是性能优化书架列表分页慢的场景我通过分析SQL执行计划发现是深分页导致的后来改成了基于游标的分页方案接口响应时间从800毫秒降到了100毫秒以内。这种项目描述方式不炫技但每一步都有明确的业务背景和收益量化笔试官最想看到的就是这种“能用数据说话”的工程思维。5.3 部署和构建工具的隐性考察另外有一道简答题问的是“你们项目的后端是怎么构建和部署的”。这题看似简单但我看到不少人会答错。最标准的说法是使用Maven的mvn clean package打包成可执行的JAR包然后通过Docker构建镜像推送到镜像仓库最后在服务器上用docker run或者Kubernetes启动。如果项目里用了Jenkins还可以补充一句“通过Jenkins配置构建任务监听Git仓库代码变动自动执行编译、测试和部署流水线”。这道题考察的是你对软件交付流程的完整认知。很多应届生在实验室里开发就是本地跑通但真实后端岗位要求的是“能上线”的代码所以熟悉构建、部署、发布流程是加分项。掌阅的笔试里虽然没有深入考K8s但主动把CI/CD的链路写出来能明显拉高考官对你的评价。6. 算法题复盘难度不高但要注意边界6.1 两道算法题的实际考点掌阅的算法题整体不算难第一道是“反转链表”第二道是“最长连续递增子序列”。难度比较常规但越是这样越容易大意。反转链表我一分钟就写完了但在检查的时候发现一个边界问题如果链表只有一个节点需要额外处理一下next指针否则可能陷入死循环。这种边界条件恰恰是扣分点。最长连续递增子序列那题我一开始直接用动态规划的O(n^2)解法写完提交后才意识到其实用O(n)的一次遍历就能解决维护两个变量一个是当前连续递增长度另一个是历史最大值不满足递增条件时重置当前长度。虽然LeetCode上这道题本身不难但如果你习惯了动规思维可能反而会绕远路。不要为了用高级算法而用高级算法笔试里最朴素的解法如果效率达标就是好解法。6.2 写代码时容易忽略的输入输出处理牛客网和LeetCode最大的一个区别是牛客网的编程题需要自己处理输入输出而LeetCode只写函数。掌阅的在线笔试就是牛客网风格所以第一道反转链表题给的输入是一个以逗号分隔的字符串比如1,2,3,4,5你需要自己解析并构建链表。我在实战中发现有几个同学因为Scanner没有处理完多组输入导致后边的测试用例全部读不上来。处理牛客输入的一个小技巧是如果题目没有说多组用例直接用一行读入整串然后split如果可能有多组用例就用while(scanner.hasNextLine())循环读取。笔试前最好去牛客或者赛码网上刷几道“输入输出练习”的题把Scanner的标准用法练熟。这个准备工作看起来很小但成本很低收益很高至少能帮你避免考场上浪费10分钟调试输入解析。6.3 算法题的准备建议从掌阅这套题来看后端岗算法题的定位是“筛掉完全不会写代码的人”而不是“筛掉不擅长算法的人”。所以复习重点应该放在链表操作、二叉树遍历、哈希表应用、基础动态规划、字符串处理。尤其是链表和字符串在在线笔试里出现频率最高因为这两类题最容易考察代码的严谨性。我建议在笔试前两周每天固定刷3到5道LeetCode简单题和2道中等题优先刷高频题号和热题100里的基础题。不要贪多关键是每道题都要自己写成完整代码并跑通不能只看思路。很多人在LeetCode上习惯了看题解到了牛客上反而写不出来就是因为缺少从“想法”到“可运行代码”的转换训练。7. 考后复盘这次笔试暴露出的知识盲区7.1 最痛的失误对Redis新特性的知识盲区考完我对了一遍答案发现有一道Redis的选择题错了。题目问的是Redis 6.0之后默认使用的多线程模型是用来处理什么操作的。我第一反应是“多线程处理命令执行”但正确答案是“多线程处理网络IO的读写和协议解析”而命令执行仍然是单线程。这个点其实在Redis官方发布6.0的博客里写得很清楚它引入多线程是为了提高网络IO吞吐量而不是让命令并行执行。这提醒我复习的时候不能只抱着“Redis命令大全”和“三大缓存问题”这种传统面经Redis的新特性也很重要。比如Redis 7.0引入的Function、Redis 6.2的客户端缓存这些在新趋势下都可能成为笔试的拉分项。掌阅这种还在快速迭代的互联网公司会格外关注候选人有没有持续追踪技术演进的习惯。7.2 笔试和面试的衔接笔试之后大概一周就收到了面试通知复盘下来最大的感受是掌阅的笔试题不是孤立的一关它在为面试做铺垫。你在笔试卷里答过的场景设计题面试官很可能拿来作为追问的起点。比如我笔试里写了“用ZSET实现排行榜”面试的时候面试官就直接问“如果某个书的score出现并发递增怎么保证不丢更新”这就是典型的从笔试到面试的延续性问题。所以我的建议是笔试结束之后不要马上把题抛到脑后而要把自己写的答案复盘一遍把其中涉及的技术点往深里再挖两层。比如缓存一致性方案不仅要会写“先更新数据库再删缓存”还要准备好回答“为什么不用更新缓存”“删除缓存失败怎么办”“延迟双删的延迟时间怎么定”。这些都是面试官最爱追问的细节提前准备好能让你在面试里占据主动。7.3 给下一届同学的建议回首整套流程想给准备掌阅或者其他阅读类互联网公司后端岗的同学几个实际建议。第一基础知识的复习要成体系不能散装。Java集合、并发、JVM、Spring、MySQL、Redis、网络、操作系统每一块都要有一条清晰的复习主线把知识点串成树状结构而不是一个个孤立的名词。第二场景题要多结合具体业务。阅读App的业务核心是书籍、书架、阅读进度、排行榜、搜索、推荐这些场景的后端设计问题值得考前自己模拟设计一遍。当你对业务足够熟悉笔试里遇到“缓存设计”“存储设计”这类题思路会自然涌现出来。第三一定要至少做一次“定时模拟笔试”。找一个完整的90分钟设定好闹钟用牛客网的真实笔试环境做一套往年真题或模拟题。模拟的意义不只是练习知识点更是练习时间分配和考试心态。我身边至少有两位同学是在真实笔试里因为第一道算法题卡住导致后面所有题都没做完这就是缺少模拟训练的典型结果。最后想说秋招笔试只是起点它考察的是你过去几年的知识积累和工程思维的雏形。掌阅这套题不算难但覆盖面很广能帮你快速看清自己的薄弱环节。如果你在复习中把上述知识点吃透不仅笔试这关问题不大后续的面试也会轻松不少。
返回列表