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

资讯详情

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

蘑菇街Java后端一面复盘:缓存一致性、并发与JVM核心考点

蘑菇街Java后端一面复盘:缓存一致性、并发与JVM核心考点 2019年7月底我还在实验室啃《深入理解Java虚拟机》突然收到一个杭州座机打来的电话。接起来才知道是蘑菇街提前批的一面。说实话当时有点措手不及因为投完简历才三天根本没想过会这么快好在提前批本身就是临时起意的机会能走到哪儿是哪儿。后来这一面面了将近一个小时问的内容不算偏但每一块都问得很细尤其是项目细节和Java基础答得让我印象深刻。这几天陆续有学弟学妹问我当年提前批的情况干脆把这场蘑菇街一面完整复盘出来按面试问题我的答案复盘点评的结构写希望能给准备校招后端岗位的同学一些参考。1. 提前批的节奏与我的准备状态1.1 蘑菇街提前批的时间线回顾2019年的秋招比往年更早蘑菇街的提前批大概在7月中旬就开了。我当时是通过实验室学长内推投的投递的是Java后端开发岗位。内推后的第三天接到面试电话约在当天晚上七点电话面试。时间线大概是这个节奏7月中旬内推投递简历投递后第3天接到一面电话邀约一面电话面试约55分钟面试形式电话沟通没有共享屏幕手撕代码通过口述思路事后发代码链接完成这里提醒一句蘑菇街当时提前批和正式批是不冲突的提前批挂了还可以走正式批。所以不要因为准备不充分就放弃投递提前批等于多了一次面试机会拼的就是一个早。1.2 我当时的复习重心接到面试电话之前我的复习已经持续了大概一个月。因为目标是后端开发岗所以复习重心分配得很明确刷题剑指Offer全部过了一遍LeetCode按高频题刷了大概100道重点放在链表、二叉树、动态规划三类。Java基础HashMap、ArrayList源码级理解JVM内存模型和GC整理成脑图。并发编程synchronized、ReentrantLock、volatile、线程池把原理和对比都写了一遍。数据库MySQL索引底层、事务隔离级别、MVCC、常见索引失效场景。中间件Redis的数据结构、持久化、缓存穿透/击穿/雪崩。面的过程中我发现这种按主线复习临时补充的策略是对的。蘑菇街一面并没有问太偏门的内容全部落在Java后端开发的核心范围内。换句话说只要认真准备过上述内容一面基本都能应对。2. 一面全程从自我介绍到项目深挖2.1 开场定调自我介绍怎么说电话接通后面试官简单确认了身份直接让我自我介绍。我的回答大概是这样面试官您好我是XX大学软件工程专业2020届研究生研究生期间主要做Java后端开发方向。对Java基础、并发编程、JVM和MySQL底层原理有过系统的学习平时用Spring Boot和MyBatis做项目。最近主要做了两个项目一个是基于微服务的校园秒杀系统另一个是实验室的设备借用管理平台。其中秒杀系统涉及到高并发下的库存扣减和缓存一致性设计是比较能聊的一个项目。复盘时回头看这段自我介绍其实埋了两个钩子一是主动把话题引到并发和缓存二是强调秒杀项目可以深聊。面试官后续果然顺着项目问了很多这比被动等他随便问要舒服得多。我的经验是自我介绍不用太长但一定要有意识地引导面试官去问你准备最充分的部分。2.2 项目深挖秒杀系统的缓存一致性设计面试官听完自我介绍没有问实验室管理平台直接问秒杀系统你刚才说秒杀系统涉及高并发说说最核心的难点你怎么解决的我当时把项目里的技术方案完整讲了一遍。这里还原关键对话。面试官库存扣减怎么设计的我最开始是直接操作数据库每次下单都update库存表并且加for update锁。压测的时候发现连接池满得非常快QPS顶到几百就上不去了。后来改成两层设计——先把库存预热到Redis用Redis的decr命令做库存扣减扣减成功后再把下单请求丢进RabbitMQ由消费者异步创建订单。面试官为什么选Redis的decr而不是数据库悲观锁我数据库的for update本质上是把一行记录锁住并发能力受限于数据库的连接数和锁等待时间。而Redis是单线程模型decr命令天然是原子的单机QPS可以支撑到十万级别比数据库判断库存再扣减要快得多。另外这里还有一个细节用户维度我们用了Redis setnx做一人一单限制防止同一用户秒杀多件。面试官缓存和数据库的一致性怎么保证我用的Cache Aside Pattern也就是先更新数据库再删除缓存。读到的时候如果缓存miss再从数据库读出来回填缓存。面试官为什么是删除缓存而不是更新缓存我更新缓存需要算出最新的数据再写进去成本比删除高而且并发场景下两个线程交替更新缓存和数据库很容易导致缓存里终态错误。删除缓存成本低即使删早了下一次读的时候重新从数据库加载就行逻辑简单可靠。面试官那删除缓存失败怎么办我我们做了一个兜底缓存key设置了过期时间就算删除失败最终也会过期不会永久不一致。同时我们把删除失败的key写入本地消息表通过RabbitMQ延迟队列重试删除。更完善的方案是订阅MySQL的binlog用canal解析出数据变更事件再异步删除对应缓存这样完全不依赖业务代码手动删除。面试官对这个回答没有再追问点了下头就切到下一题。复盘下来项目这块他能问到的点基本都被我提前准备过。这里最大的心得是项目深挖其实不是考你做得多牛而是考你对方案的理解深度。光说用了Redis不够得说得出来为什么不用数据库锁Redis为什么合适删缓存失败会有什么问题怎么补救。把这些问题想通了项目环节基本稳了。2.3 面试官追问的意图后来想想面试官在每个追问背后其实都在考察一件事项目到底是不是你自己做的你有没有真正想过方案背后的取舍。比如缓存一致性问题如果只是背过先更新数据库再删除缓存这句话被问失败怎么办就露馅了。所以做项目的时候不能只看博客里怎么写一定要亲手把方案跑通把异常场景都模拟一遍。3. 基础题问答实录Java并发、JVM、MySQL项目聊了大概15分钟之后面试官话锋一转进入基础题环节。速度明显加快基本是我问你答答完就下一题的节奏。3.1 Java集合与并发面试官HashMap的底层结构是什么样的JDK 1.7到1.8有哪些变化我HashMap底层是数组加链表JDK 1.8之后引入了红黑树。当链表长度超过8同时数组容量达到64链表会转为红黑树主要是为了处理hash冲突严重时链表过长、查询效率从O(n)恶化的问题。扩容方面默认容量16负载因子0.75也就是元素个数超过阈值12时就扩容到原来的两倍。JDK 1.8还有一个重要的优化扩容时不用重新计算hash而是看原hash值新增的那一位是0还是10就留在原位置1就放到原位置加旧容量的位置。另外1.7是头插法并发扩容可能形成环形链表1.8改成尾插法避免了这个问题但HashMap本身线程不安全并发场景还是要用ConcurrentHashMap。面试官ConcurrentHashMap底层是怎么做线程安全的我1.7的时候是分段锁内部维护多个Segment每个Segment继承ReentrantLock不同段之间可以并行操作。1.8放弃了分段锁改用synchronized加CAS锁的粒度是桶也就是数组的每个槽位只有发生hash冲突的桶才会锁住并发度更高。扩容时支持多线程协助迁移把一个大数组拆成多个任务分给不同线程去做。面试官volatile和synchronized有什么区别我volatile只保证可见性和有序性它会强制把修改立即写回主内存同时通过内存屏障禁止指令重排序但它不保证原子性。synchronized保证原子性、可见性和有序性。所以像i这种操作光用volatile是不行的。volatile典型的应用场景是状态标志位还有单例模式里的Double Check用volatile修饰instance防止JVM指令重排导致拿到未初始化完成的对象。面试官synchronized和ReentrantLock怎么选我synchronized是JVM层面的锁ReentrantLock是JDK提供的API。ReentrantLock多了三个能力可以响应中断、可以设置超时时间、可以创建公平锁并且支持多个Condition条件队列。JDK 1.6之后synchronized做了偏向锁、轻量级锁的优化性能差距已经很小。如果只是简单的同步需求synchronized就够了代码更简洁如果需要超时等待、可中断、公平性控制就选ReentrantLock。这里有个插曲面试官对公平锁这个点追问了一句公平锁的底层怎么实现的我当时只说了一个大概ReentrantLock内部维护了一个等待队列公平锁会检查队列里有没有排在前面的线程有就先让前面的人获取锁。面试官没再追问。后来我仔细研究了源码AQS里是通过hasQueuedPredecessors()方法判断当前线程是不是队列头部只有真正排在头部的线程才有资格抢锁。3.2 JVM内存与GC面试官JVM运行时数据区域有哪些哪些线程共享哪些线程私有我整体分五大块。程序计数器、虚拟机栈、本地方法栈是线程私有的堆和方法区是线程共享的。JDK 8之后方法区被元空间取代元空间使用本地内存不再受JVM堆内存上限限制。对象实例和数组主要分配在堆上栈上分配是JIT在逃逸分析之后做的优化不是常规路径。面试官垃圾回收怎么判断对象可以回收我主流用的是可达性分析算法。从一组称为GC Roots的根对象出发沿着引用链往下找没有被引用链连接的对象就判定为可回收。GC Roots包括虚拟机栈中栈帧里的局部变量引用的对象、静态变量引用的对象、常量池引用的对象、JNI引用的对象、被synchronized持有的对象。引用计数法因为无法解决循环引用的问题现在基本不会单独用。面试官CMS和G1有什么区别我CMS是老年代垃圾收集器目标是低停顿用的是标记-清除算法整个过程分初始标记、并发标记、重新标记、并发清除四个阶段其中只有初始标记和重新标记需要STW。缺点也很明显标记-清除会产生内存碎片并发阶段会占用CPU资源而且它无法处理浮动垃圾。G1则是把整个堆划分成多个大小相等的Region既可以回收新生代又可以回收老年代通过维护每个Region的回收价值和回收成本做到可预测的停顿时间。G1在Java 9之后成为默认垃圾收集器它最大的特点是可以在回收过程中把Region里的存活对象复制到空闲Region里本质上是标记-复制不会产生碎片。JVM这块我明显感觉面试官比较满意因为他在我答完之后说了一句JVM底子还可以。这可能是整场面试中我最舒展的一段。3.3 MySQL索引与事务面试官InnoDB的索引为什么用B树我B树有几个特点比较适合数据库场景。第一非叶子节点只存索引键值不存数据所以每个节点能存放更多的索引项树的高度低一般三层就能存上千万条数据也就是最多三次磁盘IO就能定位到叶子节点。第二叶子节点之间有双向指针串联天然支持范围查询和排序B树就需要回溯父节点才能做范围查询。第三哈希索引虽然单点查询快但不支持范围红黑树和二叉树在数据量大的时候树太高磁盘IO次数太多了。面试官联合索引遵循什么原则哪些情况会导致索引失效我联合索引遵循最左前缀原则。比如建了(a, b, c)的联合索引查询条件里有a或者a、b或者a、b、c才能命中。失效场景常见的有对索引列做了计算、函数操作、隐式类型转换使用like时前面带百分号比如like %xx使用or连接非索引列联合索引中不满足最左前缀条件。面试官MySQL默认的隔离级别是什么MVCC是怎么实现的我默认是可重复读RR。MVCC是InnoDB实现一致性读的关键机制核心由三部分组成undo log版本链、read view、隐藏的trx_id字段。每行记录上都有最近修改它的事务IDundo log记录了历史版本形成一个版本链。查询时生成read viewread view里保存了活跃事务列表通过比较事务ID判断当前查询能看到哪个版本。区别在于读已提交RC是每条语句生成一个新的read view可重复读RR是第一次快照读的时候生成后续复用同一个read view所以同一个事务里两次查询结果一致。面试官RR级别下怎么防止幻读我主要通过两个机制。一个是MVCC的快照读第一次读的时候生成read view之后复用即使别的事务插入了新数据当前事务看不到天然避免了幻读。另一个是当前读比如select ... for update需要通过间隙锁gap lock和临键锁next-key lock来实现。间隙锁锁的是索引记录之间的间隙让其他事务无法在间隙内插入新的记录从而防止幻读。基础题环节到这里大概持续了25分钟。面试官把Java、JVM、MySQL各自挑了最核心的几个点来问没有一上来就压八股。给我的感觉是蘑菇街一面更看重能不能把原理讲清楚而不是背了多少面试题。4. 计算机网络与中间件快问快答基础题之后面试官开始快问快答节奏明显加快问题更零散像在扫知识点。4.1 TCP与HTTP细节面试官TCP三次握手为什么不是两次我如果只需要两次握手可能出现这种情况客户端发送的SYN报文在网络中滞留了很久客户端认为它超时了没有收到确认所以重发了SYN这一次正常完成了连接。但滞留在网络中的那个旧SYN报文过了很久又到达了服务端服务端以为是一个新连接于是返回SYNACK给客户端。如果只有两次握手服务端这时就认为连接建立成功了会一直等待客户端发送数据白白浪费服务端的资源。而三次握手中客户端收到服务端的SYNACK之后并不会立即认为连接建立而是要再回一个ACK。如果服务端收到的是旧SYN的响应客户端会发现这个连接不是自己期望的就不会回ACK服务端自然也不会建立连接。面试官四次挥手里TIME_WAIT为什么要等2MSL我两个原因。第一确保最后一个ACK报文能够到达对端如果ACK丢了对端会超时重传FIN如果此时连接已经关闭就没有办法重发ACK了等一个2MSL可以保证ACK重传的窗口足够。第二经过2MSL的时间能让本次连接产生的所有旧报文都在网络中消失避免它们出现在未来某个相同的四元组连接里造成数据混乱。面试官HTTPS的握手过程了解吗我HTTPS本质上是HTTP over TLS握手过程大致分几步客户端发起ClientHello携带支持的TLS版本、加密套件列表和随机数服务端返回ServerHello选定加密套件和服务端随机数同时下发证书客户端验证证书合法性然后生成预主密钥用服务端证书里的公钥加密发过去服务端用自己的私钥解密出预主密钥双方通过三个随机数协商出会话密钥。之后双方发送Finished消息确认握手成功后续应用层数据全部走对称加密。TLS 1.3进一步简化了握手把以往的两个往返优化成一个往返。面试官HTTP常见的502和504有什么区别我502 Bad Gateway表示网关或代理服务器从上游服务器收到了无效响应简单说就是上游服务器挂了或者返回了非法内容。504 Gateway Timeout表示网关在指定时间内没有等到上游服务器返回响应也就是上游处理超时了。实际排查中502更多是后端服务进程崩溃或者重启504更多是后端处理得太慢或线程池被打满。4.2 Redis三大经典问题面试官Redis缓存穿透、击穿、雪崩分别是什么怎么解决我穿透是查询一个不存在的key缓存里没有数据库里也没有请求直接打到数据库恶意攻击时能把数据库打垮。解决方式是布隆过滤器先用bitmap把所有可能存在的主键存进去查不到的直接拦截或者对空结果也做缓存设置一个较短的过期时间。击穿是某一个热点key在过期瞬间大量请求同时打到数据库。解决方式是热点key不设置过期时间或者过期时间加一个随机值再或者用互斥锁让同一个key只有一个请求去数据库回源。雪崩是大量key在同一时间段集体过期导致流量瞬间打到数据库。解决方式有过期时间增加随机因子避免集中在同一时刻热点数据不设置过期时间由后台任务定时更新还可以做熔断降级数据库压力大的时候直接返回默认值。面试官RDB和AOF怎么选我RDB是定时的全量快照文件紧凑恢复速度快适合做备份和主从同步但故障时可能丢失最后一次快照之后的数据。AOF记录的是每一个写命令数据安全性更高默认everysec配置最多丢一秒数据但AOF文件体积更大恢复速度慢。生产环境通常两个都开AOF保证数据安全RDB用于快速恢复和备份。面试官Redis实现分布式锁要注意什么我最基础的方式是SETNX加过期时间set key value NX PX 30000保证原子性。但要注意value必须是一个唯一标识释放锁的时候要先get判断是不是自己的锁再delget和del要保证原子性通常用Lua脚本执行。更完整的做法是用Redisson它有一个看门狗机制会对锁自动续期防止业务还没执行完锁就过期被其他线程拿走了。这套方案里Redis主从切换时可能会丢锁所以严格要求时要用RedLock但实际业务里用得不多。快问快答阶段明显是在扫盲区问题之间没有太多关联覆盖范围广但深度不大。我的体感是这部分的目的是快速判断候选人的知识面够不够宽而不是在某一个点上死磕。所以平时积累很重要至少每个常见知识点都要能说出个一二三来。5. 手撕算法链表中环的入口节点基础题问完面试官说最后写一道题吧。当时电话面试不方便共享屏幕就让我口述思路然后发一段代码到指定的链接里。5.1 题目分析与快慢指针思路题目是经典题给定一个链表如果它包含环找出环的入口节点没有环就返回null。我听到题目第一反应是这题有套路分两步第一步判断是否有环。用快慢指针slow每次走一步fast每次走两步两个指针都从头节点出发。如果链表中存在环那么快指针最终会追上慢指针在环内相遇如果快指针达到了链表尾部说明没有环。第二步找到环的入口。相遇之后让slow回到头节点fast留在相遇点然后两个指针都保持每次走一步的速度继续走下一次相遇的节点就是环的入口。但光记住结论不够面试官多半会追问为什么。所以当时我把推导也讲了一遍假设从头节点到环入口的距离是a环入口到第一次相遇点的距离是b相遇点到环入口的距离是c那么第一次相遇时慢指针走了ab快指针走了abn(bc)。因为快指针速度是慢指针的两倍所以2(ab)abn(bc)整理一下得到a (n-1)(bc)c。也就是说从头节点重新出发的slow指针走距离a的同时从相遇点重新出发的fast指针会绕环走n-1圈再走c两者恰好都在环入口位置碰头。5.2 代码实现与边界条件我用Java快速写出了实现public class ListNode { int val; ListNode next; ListNode(int x) { val x; } } public class Solution { public ListNode detectCycle(ListNode head) { if (head null || head.next null) { return null; } ListNode slow head; ListNode fast head; while (fast ! null fast.next ! null) { slow slow.next; fast fast.next.next; if (slow fast) { break; } } if (fast null || fast.next null) { return null; } slow head; while (slow ! fast) { slow slow.next; fast fast.next; } return slow; } }写完之后我主动把边界条件说了一遍空链表和只有一个节点的情况直接返回null。链表没有环fast会先到尾部循环自然结束。链表整个就是一个环也就是尾节点指向头节点slow回到头节点后第一次判断就与fast相等直接返回头节点逻辑是成立的。环在链表中间慢指针回到头节点后走a步到入口另一个从相遇点出发经过c步到达入口两者同步到达。5.3 面试官加试为什么快指针每次走两步果然面试官接着问为什么快指针每次走两步走三步行不行我心里庆幸之前看过这个问题的分析。回答思路是快指针走两步的目的是保证它在慢指针进入环之后一定能追上慢指针。考虑慢指针刚进入环的时候快指针已经在环里了两个指针之间的相对距离最多是环长减1。每次快指针比慢指针多走一步这个相对距离就会缩小1所以最多走环长减1步就一定能追上时间复杂度是O(n)是最优步长。如果快指针走三步相对距离每次缩小2那么当初始相对距离是偶数时能追上是奇数时就可能错过。极端情况下快指针会一直跳过慢指针所在的位置造成永不碰面的情况。当然实际中可能会因为环的形状不同而碰巧追上但无法保证一定有解。所以两步是既能保证追上又不会浪费额外时间的稳妥选择。算法题到这里结束。面试官简单评价了一句思路清晰然后问我有没有想问他的问题。这里我也说一个小经验算法题不光要把代码写出来最好能主动说明复杂度和边界条件。很多面试官不会明确要求你说但你说了他会下意识把你归类到基础扎实的那一批。这道题的时间复杂度是O(n)空间复杂度是O(1)我也一并说了。6. 面后复盘与经验总结6.1 我回答得不够好的地方这一面整体感觉不差但复盘时我给自己挑出了几个明显的问题第一线程池参数当时答得不全。面试官问线程池的核心参数有哪些我答了corePoolSize和maxPoolSize但把keepAliveTime、workQueue、ThreadFactory这几项漏了还是面试官引导了一下才补齐。这个属于基础中的基础答不全挺不应该的。后来我把ThreadPoolExecutor的七个参数核心线程数、最大线程数、空闲存活时间、时间单位、工作队列、线程工厂、拒绝策略整理成了一张表每天默写一遍之后再没有出现过卡顿。第二反问环节问得太浅。我只问了一句团队目前主要用什么技术栈面试官简单回答完就结束了。后来跟已经拿到offer的学长聊才知道好的反问是能加分的。比如可以问团队目前更多在攻坚哪块业务候选人入职后一般从什么模块入手您觉得这个岗位更看重候选人的哪方面能力这能体现你对岗位的思考深度。当然这一面本身已经过去了这个经验主要是在后面的面试里用上了。第三项目里有一个细节我没讲透。面试官问过为什么订单创建不直接同步执行而是走MQ异步我当时只说为了削峰填谷但没有说清楚异步之后怎么保证订单和库存的一致性。实际上我们当时的方案是MQ消费者里做库存二次校验如果库存扣减成功但订单创建失败会发一条消息到死信队列由定时任务做状态对账。如果当时把这个对账机制讲出来项目这块会更加完整。6.2 蘑菇街一面的考察特点如果把蘑菇街一面和同期面过的其他公司对比我的感受是技术范围中规中矩不偏门。核心还是Java基础、JVM、MySQL、Redis、算法这些后端通用知识。项目深挖比想象中要细。会在缓存一致性、超卖怎么解决这类问题上连续追问直到确定你是真的理解而不是背了个八股。算法题难度适中。没有出hard题剑指Offer和LeetCode hot 100覆盖到的程度就够用了。面试官整体比较温和会有一点引导性。你卡住的时候他会换个角度问而不是冷场让你尴尬。这里也列一个表格方便大家对照我当时整理的考察侧重点考察模块涉及知识点准备优先级项目深挖缓存一致性、超卖、异步解耦、消息可靠性最高Java基础HashMap、ConcurrentHashMap、volatile、锁高JVM内存区域、GC Roots、CMS/G1高MySQLB树、索引失效、MVCC、间隙锁高计算机网络TCP握手挥手、HTTPS、HTTP状态码中中间件Redis穿透/击穿/雪崩、分布式锁中算法链表、二叉树、双指针高6.3 写在最后的心得这场面试最终的结果是过了后续进入了二面。但说实话一面给我留下的最深印象不在结果而在于它让我第一次真正体会到准备充分的面试是很有掌控感的。项目、基础题、算法三个环节节奏分明面试官问的每个深度点我都恰好提前踩过这种正反馈是会滚雪球的最直接的影响就是让我对后续字节、快手几家大厂的面试都更有底气。如果你现在也在准备秋招我的建议就是提前批一定要投尤其像蘑菇街这种有提前批的厂子试一试没有任何损失。复习时间不够的时候优先把项目里每一个技术选型的前因后果想明白把HashMap、JVM、MySQL索引、Redis三大问题这几座大山啃透再拿剑指Offer里的高频题练手。面经不是背的是拿来对照检查自己哪里有漏洞的用这个思路去看你会少走很多弯路。
返回列表