十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

后台开发笔试全攻略:从HashMap到分布式锁的考点解析

后台开发笔试全攻略:从HashMap到分布式锁的考点解析 1. 后台岗笔试考察什么一份真题背后的隐藏考点2020年那会儿我刚开始投后台开发岗的校招简历。互联网大厂还没像现在这样统一用赛码、牛客的在线测评系统不少公司都是自己出题、自己安排笔试时间。乐信的笔试就是这种情况——HR发来一封邮件约定某个周末晚上七点登录一个在线答题平台两个小时内完成八道题五道选择题一道简答题两道编程题。当时我一度以为笔试就是刷题把LeetCode上热题刷得滚瓜烂熟就够了结果真正坐在电脑前才发现后台岗位的笔试题和纯算法题完全是两回事。先说一个比较容易踩的认知误区很多人把后台开发笔试等同于算法笔试把所有精力都押在动态规划和图论上结果拿到卷子发现选择题考的是HashMap在JDK 8之后为什么引入红黑树、MySQL默认隔离级别是什么、TCP四次挥手的TIME_WAIT到底解决了什么问题。这些题目的共性是它们全部指向后台开发日常工作中真正会用到的基础原理而不是刷题网站上的偏题怪题。你背过的LRU缓存手撕快排当然也重要但本质上只是考核的一部分甚至可以说是一小部分。后台岗位的工作内容决定了笔试的出题逻辑。你入职之后要面对的是什么是千万级用户的高并发请求是分布式系统的数据一致性是数据库慢查询的优化是服务崩溃后的快速恢复。所以笔试题目必然围绕这几个核心领域展开Java基础与集合框架HashMap、ConcurrentHashMap、ArrayList的底层实现线程安全集合的演进并发编程synchronized和ReentrantLock的区别volatile的语义线程池参数的含义与调整JVM内存与垃圾回收堆内存的分代模型GC算法的演进OOM的排查思路计算机网络TCP/UDP的区别三次握手与四次挥手HTTP/1.1与HTTP/2的差异操作系统进程与线程的区别死锁的四个必要条件虚拟内存与页面置换MySQL数据库索引的数据结构事务的隔离级别MVCC机制SQL优化的思路数据结构和算法数组、链表、堆、树的增删改查复杂度以及常见的算法模板熟悉这套考点的分布你就能理解为什么我在前言说后台笔试题最难的往往不是算法——它难在知识面广难在与实际工程结合的深度难在你不仅要会做还要能在有限的笔试时间内准确写出让阅卷人满意的答案。下面我按笔试的实际答题顺序把这些科目的考察方式和应对思路逐一道来。文中涉及的具体题目是我结合2020年乐信后台岗位笔试的常见出题风格和同届同学的口述回忆整理的并非原卷逐字照录但考点和价值是一致的参考性不打折。2. 五道选择题的覆盖面从HashMap到TCP状态机2.1 JDK 8的HashMap改造为什么引入红黑树选择题第一道通常比较温和比如JDK 8中HashMap在什么条件下会将链表转换为红黑树答案是链表长度达到8且数组长度达到64。这道题听起来简单但它背后其实是一个绝佳的考察点因为你需要理解三个层面的东西第一层为什么需要树化当多个key经过hash计算落到同一个数组槽位时冲突的键会以链表形式串联。在数据量小的场景下链表查找的时间复杂度是O(n)n是链表的长度几十个节点的遍历完全能接受。但如果某个槽位的链表膨胀到上万甚至几十万个节点一次get操作就得线性遍历性能会严重劣化。第二层为什么阈值偏偏取8这就涉及统计学知识了。HashMap的hash算法希望让元素均匀散布在数组的各个位置按照泊松分布的计算在负载因子0.75、单槽位链表长度达到8的概率约为千万分之六。也就是说正常业务场景下几乎不可能出现链表长度超过8的情况一旦出现说明hash函数出了问题或者数据分布本身极其不均匀此时用红黑树来挽救比继续依赖链表更划算。红黑树的查找复杂度是O(log n)即便这个位置的元素继续膨胀性能也还能守住。第三层为什么有两个条件而不是只看链表长度数组长度如果小于64说明桶的数量还太少更合理的做法是先扩容让元素重新分布而不是直接树化。如果跳过扩容直接树化一个只有16个桶的HashMap里塞满了hash碰撞的key树化后的树会变得很深而且扩容之后又要拆树性能反而更差。这道选择题的完整设计其实在暗示一门后台开发的基本功不要只背结论要把结论放在源码设计的上下文里理解。我在笔试复习时专门花了一个下午把HashMap的源码从头到尾读了一遍包括resize过程、红黑树的拆分逻辑、以及JDK 7和JDK 8在并发场景下的不同表现后面做选择题和简答题都轻松了很多。2.2 线程池的核心参数笔试的标准答案与真实工程的区别选择题第二题大概率是线程池相关考察形式通常有两种要么问ThreadPoolExecutor构造方法中各个参数的含义要么给一段代码问核心线程数和最大线程数的变化。这里我要多说两句因为校招生在准备这道题时常犯两个错误第一个错误是背错参数顺序。ThreadPoolExecutor的构造参数是核心线程数corePoolSize、最大线程数maximumPoolSize、空闲存活时间keepAliveTime、时间单位unit、任务队列workQueue、线程工厂threadFactory和拒绝策略handler。面试官想看的不是你背得熟不熟而是你是否理解在什么情况下线程数会从核心线程数向最大线程数扩张。第二个错误是不理解队列在线程池中的作用。核心线程数满了之后新任务先进入阻塞队列而不是立刻创建新线程。只有当队列也满了才会继续创建线程直到达到最大线程数此时再有新任务进来才会触发拒绝策略。如果你的答案是核心线程数满了就创建新线程到最大线程数那这道选择题就做错了。线程池的设计思想是先用队列缓冲再用增员来应对持续高峰最后才是丢弃或降级。举一个日常的例子帮助理解把线程池比作一家银行柜台核心线程是固定开放的3个窗口队列是等待区最大线程数是银行有别的办法临时加开的窗口。客户来了3个窗口忙不过来客户去等待区排队等待区也排满了银行才临时加开窗口加到最多6个窗口6个窗口也处理不过来才开始劝退客户拒绝策略。这个类比把线程池的整个工作流程讲得非常直白笔试答简答题也可以用这套逻辑说明。2.3 秒杀场景下的锁选择synchronized还是ReentrantLock选择题里出现频率很高的一道是问在超高并发秒杀场景下对于库存扣减操作如何选择锁。这道题考察的知识点非常密集你需要串联起JMMJava内存模型、锁的底层实现、以及业务场景对吞吐量的要求。先说结论单纯从用Java API实现的角度看ReentrantLock可重入锁在公平性和可中断性上确实提供了synchronized没有的能力很多同学因此认为高并发场景就应该选ReentrantLock。但在真实的互联网后台开发中库存扣减这种热点数据的超高并发写入正常的解决方案有两条路一条路是放弃JVM层面的锁直接使用数据库的行级锁用UPDATE inventory SET stock stock - 1 WHERE id ? AND stock 0这样的原子操作通过影响行数判断是否扣减成功。这条路依赖数据库自身的并发控制能力代码简单性能受数据库限制。另一条路是使用分布式锁比如Redis的Redisson框架提供的可重入锁或者基于ZooKeeper实现的分布式锁为的是在多个服务实例之间协调资源。毕竟单体应用加锁只能保证一个进程内的线程安全一旦服务做水平扩展部署了多台机器进程内的锁就失效了。正确答案的判断标准是看题目是否明确多实例部署。如果题目给的是高并发场景下单机部署那么用JVM层加锁也可以如果是高并发秒杀、多实例部署那就必须上分布式锁。选择题里还有很多类似的情况其实考查的不是你会不会背某个API而是你能不能根据场景判断该用哪个方案——这恰恰是后台开发日常工作里最核心的能力。2.4 get和post的语义边界一道总有人翻车的送分题还有一道很典型的选择题问的是HTTP中GET和POST有什么区别。你可能会觉得这太简单了——GET用来获取资源POST用来提交数据GET的URL有长度限制POST没有对吧但标准答案和大多数人理解的其实有出入。严格从RFC规范来看HTTP协议本身并没有规定GET的URL长度上限长度限制是浏览器和服务器的实现策略不是协议的一部分。同样地协议也没有规定GET只能取数据、POST只能提交数据你完全可以用POST去获取数据用GET去触发删除操作只是这不符合语义化和业界规范而已。真正本质上的区别在于GET是幂等的同一个GET请求发多少次服务端资源状态都不变POST不是幂等的多次提交同一个POST请求可能会产生多个订单、多扣几次款项。很多后台技术群里的老哥喜欢调侃用GET还是POST取决于后端心情这话有玩笑成分但也反映出实际开发中不少人确实没有严格遵守语义化。笔试里遇到这类题目要按RFC规范去辨析而不是停留在民间经验层面才能保证不丢分。2.5 TIME_WAIT与TCP四次挥手选择题里经常出现的状态机关于TCP的题目也是后台笔试的座上宾尤其是问主动关闭连接的一方进入TIME_WAIT状态是为了什么。这道题的完整答案是两个目的第一个目的是保证最后一个ACK能被对端收到。主动关闭方发出最终的ACK之后这个ACK可能丢失被动关闭方会超时重发FIN报文主动关闭方需要停留在TIME_WAIT状态以便重新发送ACK确保对端能正常关闭连接。第二个目的是让旧连接的报文在网络中自然消失。如果主动关闭方立即关闭端口而网络上还有之前这个TCP连接里迟到的数据报文这些报文到达后可能被新建立的、端口号相同的连接接收到导致数据错乱。TIME_WAIT状态一般持续2倍的最大报文生存时间2MSL足以保证旧报文在网络中消散。后台开发中你要处理的一个实际问题就是高并发短连接服务往往会出现大量TIME_WAIT连接耗尽本地端口号资源。所以这道选择题不是只停留在理论层面的它直接指向你在优化服务端网络参数时遇到的现象。我在实习期间就遇到过一台服务器上TIME_WAIT连接数超过十万的情况后来通过开启SO_REUSEADDR、调整keepalive参数、以及让客户端改用长连接才把问题压下去。考场里你把这层关系写清楚阅卷人一眼就能看出你有真实运维经验而不只是背了书。3. 简答题的答题逻辑从JVM内存模型到MySQL索引失效3.1 JVM运行时数据区用哪个区域会OOM串联记忆简答题的第一题通常会让你谈一谈JVM的内存模型或者直接问哪些区域会抛出OutOfMemoryError分别怎么排查。先明确各个区域的作用程序计数器当前线程执行的字节码行号指示器是JVM中唯一不会OOM的区域虚拟机栈每个方法执行时会创建栈帧栈帧中存放局部变量表、操作数栈、动态链接、方法出口等信息栈深度不够会抛StackOverflowError栈内存不足申请不到空间时抛OOM本地方法栈为虚拟机使用的Native方法服务规范也允许实现方把它和虚拟机栈合并Java堆存放对象实例和数组是GC的主要战场也是OOM的高发区域方法区存放已被加载的类信息、常量、静态变量、即时编译后的代码JDK 8之后被元空间Metaspace取代直接占用本地内存元空间OOM通常是因为加载的类过多或CGLib生成的动态类太多。答题的时候不要只罗列这些区域的名称更漂亮的做法是用一个实际案例来串联。我当时的答案是如果线上服务出现java.lang.OutOfMemoryError: Java heap space优先用jmap -dump导出堆快照再用Eclipse MAT分析大对象和内存泄漏链如果是java.lang.OutOfMemoryError: Metaspace多半是动态生成类太多需要检查是否有反射或CGLib使用不当如果是创建线程时报unable to create new native thread那大概率是系统线程数或进程虚拟内存受限要和操作系统的ulimit参数联系起来。用这种区域现象排查手段的结构答题比干巴巴的背诵要高出好几个档次。3.2 索引失效的场景一条SQL背后的综合考察MySQL的索引知识在后台笔试题里的占比非常高简答题常见的一种问法是请列举导致索引失效的常见场景并解释原因。要答好这道题你先得从B树索引的数据结构说起。B树的叶子节点按索引列的值有序排列叶子节点之间通过双向链表连接。索引能加速查询本质上是利用了数据的有序性。一旦查询条件破坏了有序性索引就失效了。常见的失效场景包括对索引列使用函数比如WHERE YEAR(create_time) 2020函数会使B树无法利用原有的有序结构必须扫描全部叶子节点隐式类型转换索引列是varchar类型条件却传入了整数MySQL会隐式地做类型转换导致索引失效左模糊查询WHERE name LIKE %张因为字符串的排序规则决定了无法从以张结尾的角度走索引使用OR连接多个条件如果OR的旁边存在非索引列索引会失效违反最左前缀原则联合索引(a, b, c)在查询时直接以b作为筛选条件无法使用该索引索引列参与运算WHERE age 1 21也一样是破坏了有序性这里我建议你在复习时多想想优化器这个角色。MySQL优化器采用基于成本的查询优化策略它会计算全表扫描和走索引扫描的成本估算然后选择其中成本更低的一种。如果你的条件能命中索引但是命中率太高比如性别字段只有男女两种取值优化器反而会放弃索引选择全表扫描这就是低基数列建索引没意义的原理。考试时答出这个层面可以体现出你不仅知道索引会失效还知道为什么失效、优化器做了什么判断。3.3 Redis的数据类型与缓存穿透简答题常考的组合拳Redis相关的简答题几乎从未缺席后台笔试。最常见的组合问法Redis支持哪些数据类型各自的使用场景是什么Redis缓存常见的三大问题是什么数据类型方面你至少要熟练区分五种基础类型String缓存对象、计数器、共享session、Hash对象的部分字段更新、List消息队列、最新列表、Set去重、交集并集运算、ZSet排行榜、延迟队列。笔试时有些同学会把ZSet的底层实现也答出来提到跳跃表skip list和压缩列表ziplist这属于加分项能体现源码阅读深度。缓存穿透、缓存击穿、缓存雪崩这三个问题的对比是常规考点你不用背得太痛苦我给你一个理解框架缓存穿透请求的key在缓存和数据库里都不存在每次请求都打穿到数据库数据库压力巨大。对策是布隆过滤器拦截、缓存空值并设置短TTL、参数校验兜底缓存击穿某个热点key过期的瞬间大量并发请求同时打到数据库。对策是互斥锁重建缓存、逻辑过期时间不给物理TTL而是存一个过期时间戳、热点数据不过期缓存雪崩大量key在同一时间集中过期或者Redis节点宕机请求全部落到数据库。对策是过期时间加随机偏移、Redis集群高可用、多级缓存2020年乐信后台笔试的简答题里我记得有一道场景题假设首页有一个爆款商品的详情页QPS很高请设计它的缓存方案。这种题的通用解法是先给缓存结构商品基本信息用String缓存JSON库存用Hash或者String存储再处理缓存击穿用Redisson的分布式锁让单线程回源数据库最后给出降级方案缓存挂掉后直接返回托底数据。这种结构化答题的思路一旦掌握大部分场景题都能覆盖。4. 两道编程题的两道解法考的不是算法模板是工程意识4.1 LRU缓存淘汰策略无模板可背但必须理解数据结构的组合乐信2020年笔试的编程题之一是实现一个LRULeast Recently Used最近最少使用缓存。给定容量capacity实现get和put两个方法。这道题在LeetCode上是146题很多校招生刷过但笔试时能一遍写对的人并不占多数。要高效实现LRU核心在于选择合适的数据结构哈希表负责O(1)地定位key是否存在双向链表负责维护访问顺序。get时如果key存在就把该节点移动到链表头部put时如果key已存在更新值并移动到头部如果key不存在且容量已满删除链表尾部的节点同时删除哈希表中的对应项。下面是Java的实现思路import java.util.HashMap; import java.util.Map; public class LRUCache { // 双向链表节点 static class Node { int key; int value; Node prev; Node next; Node(int key, int value) { this.key key; this.value value; } } private final int capacity; private final MapInteger, Node map new HashMap(); // 虚拟头尾节点简化边界处理 private final Node head new Node(-1, -1); private final Node tail new Node(-1, -1); public LRUCache(int capacity) { this.capacity capacity; head.next tail; tail.prev head; } public int get(int key) { Node node map.get(key); if (node null) { return -1; } moveToHead(node); return node.value; } public void put(int key, int value) { Node node map.get(key); if (node ! null) { node.value value; moveToHead(node); return; } if (map.size() capacity) { Node removed removeTail(); map.remove(removed.key); } Node newNode new Node(key, value); addToHead(newNode); map.put(key, newNode); } private void addToHead(Node node) { node.next head.next; node.prev head; head.next.prev node; head.next node; } private void removeNode(Node node) { node.prev.next node.next; node.next.prev node.prev; } private void moveToHead(Node node) { removeNode(node); addToHead(node); } private Node removeTail() { Node node tail.prev; removeNode(node); return node; } }这段代码的关键点在于虚拟头尾节点的使用它能把插入头部删除尾部的边界条件全部统一化省去了判空逻辑出错概率大大降低。笔试时如果你用的是这个写法代码行数虽然多一点但逻辑清晰阅卷人一眼就能看懂。不过我想提醒的是这道题在笔试中更看重的是代码的完整性和对异常情况的考虑比如capacity为0时的put操作比如key不存在时get返回什么。很多人刷题时用的是Python的collections.OrderedDict一行代码就实现了笔试时如果用Java就必须自己构造双向链表对基本功的考察就在这里体现出来了。4.2 多线程交替打印考察并发基本功的经典题另一道笔试编程题通常是并发题。常见问法用两个线程交替打印奇数和偶数分别是1、3、5…和2、4、6…直到100。这道题看似简单但它考察的是你对线程通信机制的理解。用synchronized加wait/notify可以实现用ReentrantLock加Condition也可以实现用AtomicInteger加上自旋也可以实现。下面给出一种比较稳妥的synchronized写法public class PrintOddEven { private static final Object lock new Object(); private static int count 1; private static final int MAX 100; static class Printer implements Runnable { private final int targetMod; // 当前线程关注余数 Printer(int targetMod) { this.targetMod targetMod; } Override public void run() { while (true) { synchronized (lock) { while (count MAX count % 2 ! targetMod) { try { lock.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; } } if (count MAX) { lock.notifyAll(); break; } System.out.println(Thread.currentThread().getName() : count); count; lock.notifyAll(); } } } } public static void main(String[] args) { Thread odd new Thread(new Printer(1), Odd-Thread); Thread even new Thread(new Printer(0), Even-Thread); odd.start(); even.start(); } }注意这里的细节while (count MAX count % 2 ! targetMod)必须用while而不是if因为线程被唤醒后要重新检查条件这就是防止虚假唤醒的经典教训。另外打印完数字并且count超过MAX之后要调用notifyAll()唤醒对方线程让对方也能退出否则可能出现一个线程已经退出另一个线程还在wait导致程序无法结束的尴尬情况。笔试阅卷时代码能跑通是基本要求能使用while循环而不是if来判断条件、能正确设置中断状态、能避免死锁这些都是加分项。很多同学写完代码就交卷了但如果你在注释里说明用while防止虚假唤醒容易给阅卷人留下一个读过并发编程经典书籍的印象。5. 时间分配和答题顺序两个小时的笔试如何不慌笔试时间只有两个小时五道选择题、一道简答题、两道编程题看起来数量不多但如果你在选择题上反复纠结在简答题上洋洋洒洒写小作文最后编程题大概率时间不够。我根据自己的经验和同届同学的反馈整理了一套比较稳的时间分配方案。选择题建议控制在三十分钟以内。后台岗位的选择题难点不是计算量大而是概念辨析。比如前面提到的GET和POST的区别线程池核心线程数满了接下来怎么做这些题如果你会几乎秒答不会的话思考十分钟也可能还是选错。与其纠结不如先标记跳过做完后面的题再回头处理有时候灵光一闪反而能想明白。我在笔试时就跳过了那道关于红黑树转换条件的题把时间留给了编程题等编程题写完再回头想突然记起链表长度8和数组长度64这个条件正确率反而更高。简答题控制在四十分钟左右。答题的时候不要用大白话流水账要有结构。我的习惯是先写结论再分点展开最后补一句在实际生产中我倾向于……。比如问JVM的OOM排查答题框架是指出OOM发生的区域说明该区域OOM的典型原因列出排查步骤jstat查看GC情况、jmap导堆、MAT分析给出常见解决方案用这种结论-原因-手段的结构就算你不是知识点全覆盖阅卷人也能看出你有清晰的排查思路得分率比长篇大论的碎片化描述高得多。两道编程题建议各留二十五分钟。先读清楚题目确认输入输出的边界再动手写下核心数据结构。如果第一道题五分钟没有思路立刻放弃去做第二道不要死磕。笔试的过关线通常是做对一道半而不是一定要全对。我见过太多人为了一个测试点反复调试最后第二道题的送分部分都来不及写。如果你的代码写完了还有剩余时间不要急着交卷做两件事第一检查边界条件比如空数组、极端值、负数第二在代码里补充必要的注释尤其是关键逻辑那一行比如LRU里移动节点到头部的注释这会让阅卷人更快地理解你的思路。程序的正确性固然重要但笔试阅卷很多时候是人工审核代码的可读性直接影响评分。6. 笔试题目背后的后台岗能力模型复盘比分数重要笔试结束之后无论感觉好坏我强烈建议你花一个晚上做一次完整的复盘。不要只看正确答案而是分析每道题对应的是后台开发中的哪项能力。乐信的这套题目看起来零零散散其实构画了一个相当完整的后台岗位能力模型我用一张表来对照说明题目方向考察能力后台日常工作的对应场景HashMap/ConcurrentHashMap底层原理集合框架与并发安全理解缓存设计、线程安全集合选型线程池参数与拒绝策略并发编程基础异步任务处理、消息消费限流锁机制与分布式锁高并发一致性理解秒杀库存、分布式事务协调TCP状态与TIME_WAIT网络协议理解连接池配置、网络故障排查JVM内存模型与OOM运行时机制和调优能力线上内存溢出排查、GC日志分析MySQL索引失效数据库优化能力慢SQL治理、表结构设计Redis缓存穿透/击穿/雪崩缓存体系设计能力缓存方案设计、系统高可用保障LRU缓存实现数据结构组合能力本地缓存、热点数据管理多线程交替打印线程通信基本功多线程任务编排、生产者消费者模型你会发现这些能力不是孤立的而是围绕如何构建一个高并发、高可用、易维护的后台服务展开的。笔试出题人不是想为难你而是想用最短的时间判断这个人入职之后能不能快速接手一个线上模块能不能在出现问题的时候不慌张能不能写出别人敢维护的代码。2020年那次笔试给我的一个体会是题目本身并不偏几乎都是经典知识点但每道题都有再深一步的空间。比如选择题问你HashMap的树化条件如果你知道泊松分布和负载因子的关系你就能理解为什么要选8如果你只知道是8那遇到换个数字的变体题就很容易被绕进去。所以在复习准备阶段我不建议靠题海战术而是建议采用由点到面的方法每做一道题就把它涉及的底层知识点展开读一遍源码或者看一篇原理分析用自己的话讲清楚为什么。过程可能比较慢但坚持一个月之后你会发现选择题的正确率明显提高简答题也自然能写出有深度的内容。刷题量只是底线真正拉开差距的是理解深度。如果你是准备投后台开发岗的校招生用这套笔试题来检验自己的知识体系至少要在考前达到这样的状态拿到任何一道经典题不需要回忆能条件反射地说出答案并且能往外扩展三层原因。达到这个状态之后你对笔试的恐惧感会自然消失因为题目对你来说已经变成在熟悉的领域里做一次快速展示了。
返回列表