ARTICLE DETAIL

资讯详情

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

大厂Java后端面试复盘:从Spring Boot到Redis层层深挖

大厂Java后端面试复盘:从Spring Boot到Redis层层深挖 1. 面试前的梳理为什么我把项目准备成了技术故事1.1 简历上的每个亮点都要经得起三轮追问上周刚面完某大厂的Java后端岗位趁热把整轮面试复盘一遍。这轮面试从Spring Boot自动配置一路追问到Redis缓存穿透、击穿、雪崩中间还夹了不少Java并发和JVM的陷阱题整体节奏可以用四个字概括步步紧逼。我先说面试前的准备思路。面试官几乎必然会从简历上挑一个项目开刀所以我的第一件事不是刷题而是把简历里那个基于Spring Boot的上门烹饪预约服务系统重新梳理了一遍。表面上看这就是一个典型的业务CRUD项目预约下单、师傅派单、评价结算但我在里面埋了几个技术亮点Redis缓存了热门的菜品和师傅信息JWT做了登录态管理支付回调做了幂等处理另外线上环境还出现过一次典型的OOM。面试官问我项目时我就按照30秒讲清楚业务2分钟讲技术难点的节奏去组织先说这个系统是做什么的再说我负责的模块最后重点抛出两个能拉深度的点——缓存方案设计和我亲手排查的线上内存溢出问题。很多人有个误区觉得大厂面试重点考察的是算法和底层原理项目只是走个过场。实际上项目问题追问起来比八股文狠多了面试官会一直往下问问到你说不出来为止。所以准备项目的核心原则是项目里的每个技术决策都要能回答为什么这么做和当时有其他方案吗。1.2 别小看Lombok警告和编译环境问题我在整理面试资料时特意关注了几个网上讨论得很热的Java环境问题因为它们特别容易在聊项目时被面试官顺带抛出来比如Java: You arent using a compiler supported by Lombok, so Lombok will not work这个警告以及源发行版17需要目标发行版17。很多人看到这些报错就上网搜答案但很少想背后的原因。Lombok那个警告本质上是javac的注解处理器版本和JDK版本不匹配。新版JDK对注解处理器的模块化隔离和访问控制更严格老版本的Lombok跑在比较新的JDK上就会提示编译器不被支持。解决办法很直接要么升级Lombok版本要么把JDK降到Lombok支持的范围。源发行版17需要目标发行版17则是Maven的compiler插件里source和target不一致导致的Java 9之后更推荐直接用release17/release它会同时约束source、target和默认的API级别避免一些隐蔽的兼容性问题。这些细节看起来小但面试官很吃这一套因为他们考的就是你有没有真正在一线环境里踩过坑。1.3 大厂Java面试的整体流程这轮面试一共四轮技术面加一轮HR面技术面的递进逻辑非常清晰轮次考察重点主要方向一面Java基础、并发、JVM、项目细节技术广度确认基本功扎实二面Spring Boot原理、工程化实践框架深度确认源码理解三面分布式缓存、Redis、场景设计分布式系统能力四面综合方案设计、开放性问题系统架构思维和沟通能力这个流程基本覆盖了Java工程师面试的常见路线基础 - 框架 - 分布式 - 综合。后面每一轮我都会还原实际的问题、我的回答思路和复盘时的反思。2. 一面实录Java基础题里全是绊马索2.1 开场就是OOM项目上线后内存撑不住一面面试官上来没有让我做自我介绍直接指着简历说聊聊你那个上门烹饪预约系统吧听说线上出过OOM这明显是提前看了简历的。我讲了大致背景预约服务在高峰期会有大量用户同时查询师傅的档期和菜品信息起初没做缓存数据库压力大后来把热门数据放进了Redis。结果有一次发布之后线上频繁出现java.lang.OutOfMemoryError: Insufficient memory应用响应变得特别慢最后直接假死。面试官追问你是怎么排查的我把实际操作讲了一遍先用jmap -heap看堆内存使用情况发现老年代占用异常高再用jmap -dump:formatb,fileheap.hprof导了堆快照用MAT打开分析发现有一个缓存的本地Map对象占了几百兆里面的key是用户IDvalue是整个订单对象这个Map被当成临时缓存放在了一个静态字段里但忘记清理了。本质上是把本该放到Redis里的数据放到了本地内存而且没有过期策略用户量一涨就爆了。面试官紧接着问内存溢出和内存泄漏是一回事吗这里我特意区分了一下内存溢出是分配不出内存了可能是内存泄漏导致的也可能是某个瞬时峰值太大导致的内存泄漏是对象明明不再使用了但GC根路径上还引用着它垃圾回收无法回收。当时那个静态Map就是典型的泄漏。排查OOM的顺序我总结是先看监控指标再用jstat观察GC频率最后用堆转储确认对象分布而不是直接去猜代码哪里写错了。2.2 手写快速排序排序算法现场的行活项目聊完二十多分钟面试官说来手写一个快排吧。这种题就是行活必须写得又快又稳。我写的是经典的挖坑法实现public void quickSort(int[] nums, int left, int right) { if (left right) { return; } int pivot nums[left]; int i left; int j right; while (i j) { while (i j nums[j] pivot) { j--; } nums[i] nums[j]; while (i j nums[i] pivot) { i; } nums[j] nums[i]; } nums[i] pivot; quickSort(nums, left, i - 1); quickSort(nums, i 1, right); }写完后面试官开始连环追问时间复杂度多少我说平均O(n log n)最坏O(n²)。什么情况下最坏每次选的基准值都是当前区间最大或最小值比如对已经有序的数组固定取第一个元素做基准就会退化成O(n²)。随后他又问了JDK的Arrays.sort底层用的什么排序这个问题我提前准备过答得比较顺对基本类型数组Arrays.sort用的是双轴快排Dual-Pivot Quicksort对对象数组用的是TimSort一种结合了归并排序和插入排序的稳定排序算法。基本类型不需要稳定性可以用更快的快排对象类型需要保证相等元素的相对顺序不改变所以选择稳定排序。最后面试官让我对比冒泡排序和快排我顺势说了一句冒泡排序主要用于教学场景实际工程里基本不会用因为交换次数太多——面试官点了点头算是认可。2.3 synchronized与ReentrantLock到底怎么选一面后半场是Java并发面试官问了一道很常规的题synchronized和ReentrantLock有什么区别这种题不能只背点我按三个层次答的第一个层次是使用方式。synchronized是JVM层面的关键字可以修饰方法或者代码块加锁和释放锁都是自动的ReentrantLock是JDK提供的类需要手动lock和unlock一般配合try-finally使用。第二个层次是功能差异。ReentrantLock支持尝试非阻塞获取锁tryLock、支持超时获取锁、支持中断响应、可以创建公平锁。非公平锁的性能通常更好因为减少了线程上下文切换但公平锁能避免线程饥饿。ReentrantLock还提供了Condition可以更精细地控制线程的等待和唤醒一个锁能创建多个等待队列而synchronized的wait-notify只能配合一个等待队列。第三个层次是底层实现。synchronized在JDK 1.6之后引入了偏向锁、轻量级锁、重量级锁的升级过程JDK 15之后偏向锁被废弃ReentrantLock基于AQSAbstractQueuedSynchronizer实现AQS内部维护了一个volatile的state变量和一个等待队列通过CAS操作state实现加锁和释放锁。面试官点头后又追了一题volatile能替代synchronized吗我明确说不能volatile保证可见性和有序性但不保证原子性synchronized保证原子性、可见性和有序性。举了个例子多个线程同时对一个volatile变量执行i因为i是读-改-写三步操作volatile不管这块最终结果还是会丢更新。如果是多个线程同时写不同的volatile变量或者只有一个线程读一个线程写那volatile才够用。2.4 一面复盘哪些地方差点接不住面完一面我给自己做了一次很诚实的复盘。答得比较稳的OOM排查过程、快排手写、ConcurrentHashMap相关的基础问题这个虽然上面没写但面试官在并发部分顺带问了我答了JDK 1.7是分段锁、1.8改成了CASsynchronized锁头节点。答得不太好的面试官问快速排序在工程里还有哪些优化手段时我只答了随机选基准或三数取中其实还可以在递归到小区间时切换插入排序以及双轴快排本身就是一种优化。这说明我对排序算法的工程实践理解还不够深。后来我查了一下JDK的双轴快排里当数组长度小于某个阈值时确实会走插入排序因为小数组上快速排序的递归开销反而更大。另一个不足是并发部分没有充分举例。面试官问ReentrantLock的Condition有什么实际应用场景时我举了阻塞队列的例子但讲得不够具体。复盘后我重新理了一遍ArrayBlockingQueue用两个Condition分别管理队列满和队列空生产者put时如果队列满了就await消费者take后signal消费者take时如果队列空了就await生产者put后signal。这样比用一个锁的wait-notify要清晰得多。一面整体感觉是广度考察偏基础但会深挖细节很多题都是在背出来的答案上继续追问直到你能用自己的话讲清楚原理为止。3. 二面实录Spring Boot自动配置在剥洋葱3.1 SpringBootApplication背后藏了三件事二面面试官风格偏严谨上来就问你每天写Spring Boot那你说说SpringBootApplication这个注解到底做了什么这个问题网上答案很多但我没有直接背而是用自己的理解串了一遍。SpringBootApplication是一个组合注解它把三个核心注解组合到了一起SpringBootConfiguration表明这个类是一个配置类其实底层是ConfigurationComponentScan让Spring扫描当前包和子包下的组件EnableAutoConfiguration开启自动配置这是Spring Boot最核心的能力。面试官打断了一下ComponentScan默认扫描哪些路径我说默认扫描主启动类所在的包及其子包所以Spring Boot官方强烈建议主类放在root package下。如果你把主类放到某个子包里面很多组件就会扫描不到这个问题在真实项目里出现过很多次。3.2 自动配置的加载过程和条件装配然后面试官问了重头戏自动配置是怎么加载的我按Spring Boot 2.4版本之后的机制讲EnableAutoConfiguration通过Import引入了AutoConfigurationImportSelector这个类会去读取配置文件中列出的自动配置类。不同版本文件路径不一样2.7之前的版本是META-INF/spring.factories2.7及之后改成了META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports。每个自动配置类上基本都有Conditional相关注解只有满足条件才会生效。我举了Redis自动配置的例子RedisAutoConfiguration上有ConditionalOnClass(RedisOperations.class)也就是说classpath下有了Redis的客户端依赖这个配置类才会被加载然后它通过EnableConfigurationProperties(RedisProperties.class)把spring.redis开头的配置项绑定到属性类上类内部通过ConditionalOnMissingBean判断如果用户自己定义了RedisTemplate或StringRedisTemplate就用用户自定义的否则创建默认的。面试官问你既然清楚自动配置那你有没有写过自定义starter我说写过总结下来四步建一个xxx-spring-boot-starter工程里面放一个XxxAutoConfiguration配置类用ConditionalOnClass和ConditionalOnProperty控制启用条件再编写META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件把配置类注册进去最后在工程里引入它Spring Boot启动时就会自动装配。这个思路对理解整个Spring Boot生态特别重要用的时候会发现很多第三方的starter都是同样的套路。3.3 Spring Boot 2.6与Springfox 3.0.0的相爱相杀说完了正课面试官笑着问了一句你项目里面用到Swagger了吧有没有遇到过Spring Boot版本和Springfox版本不兼容的问题这个问题我太熟了因为真的在项目里踩过。Spring Boot 2.6开始Spring MVC默认的路径匹配策略从AntPathMatcher切换成了PathPatternParser而Springfox 3.0.0的底层实现还依赖Ant风格路径匹配于是启动时会报NPE或者Failed to start bean documentationPluginsBootstrapper。我们当时的处理是把Spring MVC的匹配策略改回了Ant风格在配置文件里加一行spring: mvc: pathmatch: matching-strategy: ant_path_matcher现在回看这个方案确实能用但并不优雅因为PathPatternParser本身是更好的实现。更推荐的解法是直接用springdoc-openapi替代Springfox这个库从Spring Boot 1.x到3.x兼容性都做得很好而且支持OpenAPI 3规范。但如果项目里已经深度用了一堆Springfox的注解迁移成本高那改matching-strategy是最快的方案。面试官追了一句PathPattern和AntPath区别在哪我说AntPath是基于字符串通配符匹配*匹配一层路径**匹配多层路径PathPattern是Spring 5.3引入的新匹配机制解析时把路径拆成段性能更好匹配规则也更严格所以默认策略切换之后很多老配置会失效。3.4 从WebSocket集成聊到Spring Boot的嵌入式容器二面后半段面试官指着我的简历说你上面写了用WebSocket做订单状态推送那你说说Spring Boot 2.1是怎么集成WebSocket的。这个问题比较细。我讲了当时的做法引入spring-boot-starter-websocket写一个继承TextWebSocketHandler的处理器再实现WebSocketConfigurer注册handler设置允许跨域和握手拦截器。但真正让面试官感兴趣的不是流程而是我踩过的一个坑在WebSocket处理器里想要注入Spring的Service结果发现是null。原因在于Spring Boot的WebSocket处理器默认是每次连接都会创建一个新实例没有交给Spring容器管理。解决办法是把它声明成一个Spring的Component然后在注册handler的时候使用这个Bean。如果自己new出来再去注册那里面自然没法注入依赖。这个坑很典型也侧面说明Spring Boot的自动配置不是万能的有时候它自动帮你做了一些决定你需要知道这些决定的边界。面试官顺势问Spring Boot默认内置的Tomcat能换吗我说可以把spring-boot-starter-tomcat排除掉换成spring-boot-starter-undertow或者spring-boot-starter-jetty即可。Undertow的内存占用更低Jetty支持更多的异步特性很多公司的大流量场景会倾向用Undertow但Tomcat仍然是默认选择因为它是Servlet规范最标准的实现社区资料也多。3.5 Lombok警告与编译环境的版本管理面试结束前十分钟面试官问了一类贴近真实工程的题你有没有见过这个警告You arent using a compiler supported by Lombok, so Lombok will not work我说见过是在一个老项目里团队把JDK从8升到17之后Lombok版本还停留在很老的大版本上一编译就报这个警告而且IDE里代码的getter、setter全部标红。这个问题本质是Lombok作为注解处理器运行在javac内部而新版JDK对注解处理器做了更强的封装和权限控制导致老版本Lombok无法正常工作。解决方案一般是升级Lombok到1.18.30及以上或者调整JDK版本保持兼容。面试官又补了一问那源发行版17需要目标发行版17这种错误呢我说这是Maven编译参数问题。很多人会在maven-compiler-plugin里只配source和target这俩不一致就会报这个错更稳妥的做法是直接配release它能同时约束编译源码版本、目标字节码版本和JDK API版本避免比如target配了8但代码里用了JDK 11的API这种隐性问题。面试官最后评价了一句看来你确实是做过工程的人这也是让我比较受用的肯定。二面的重点其实不在于你背了多少源码注解而在于你有没有真的在业务之外想过框架为什么这么设计。4. 三面实录分布式缓存是一场攻防战4.1 缓存引入你凭什么说查询慢是数据库的锅三面面试官是技术专家组的人开场就没废话很直接地问了一个架构题你项目里引入了Redis那我问你你怎么判断一个接口该不该加缓存这个问题让我稍微意外但答起来有方向。我先说结论加缓存不是因为Redis快这个简单原因而是要看数据访问的时间局部性和频率。如果一个接口的QPS高、数据读取远多于写入、数据在短时间内的变化不大那它就是适合加缓存的反过来如果数据每秒都在变或者写入量极大缓存会导致一致性问题反而得不偿失。具体到我那个预约服务系统师傅的档期和菜品的介绍、图片这些数据访问频率很高而且变更不频繁所以非常适合缓存。下单接口我坚决没加缓存因为订单数据必须严格落库一旦缓存和数据库不一致用户会看到我下单成功了但师傅端没有订单这种严重事故。面试官点头说那面试正式开始我们聊五个场景——我心里紧张了一下但节奏在后面反而越来越顺。4.2 缓存穿透布隆过滤器与空值缓存第一个场景是缓存穿透。面试官说假设有一个查询师傅详情的接口缓存里没有数据库里也没有但黑客一直用不存在的ID来刷你怎么防我的回答分两层。第一层是空值缓存如果查数据库发现没有这个ID就往缓存里写一个空值并设置一个较短的过期时间比如3到5分钟这样后续相同的请求会被缓存挡住。这个方案的缺陷是如果有大量不同的无效ID空值缓存会占内存而且每次都要等过期时间到了才能重新写。第二层是布隆过滤器在Redis缓存之前加一层布隆过滤器把所有合法师傅ID放进去。布隆过滤器是一种基于位数组和多个哈希函数的数据结构判断一个元素一定不存在非常快但可能存在有误判率。请求到达时先查过滤器如果判断ID不存在直接返回无此师傅根本不会打到缓存和数据库。面试官追问布隆过滤器有没有缺点我说有三个一是误判率误判会导致极少数不存在的ID继续向下访问通常控制在1%以内可以接受二是删除困难普通的布隆过滤器不支持删除操作因为一个位可能被多个元素共享如果要删除就得用Counting Bloom Filter用额外的计数器来记录引用数三是快照重建麻烦数据库里数据量大的时候重建过滤器需要遍历全量数据所以一般会选择在数据初始化阶段批量构建。4.3 缓存击穿互斥锁与逻辑过期的取舍第二个场景是缓存击穿。面试官说要是某个热点师傅的ID缓存刚好过期了一瞬间几万个请求同时打到数据库怎么办我说这要分两种情况物理过期和逻辑过期。物理过期场景下我用的方案是互斥锁。流程是查缓存没有尝试加锁拿到锁的线程去查数据库、重建缓存、释放锁没拿到锁的线程短暂自旋等待后重新查缓存。这个方案能保证同一时间只有一个请求重建缓存但缺点是锁等待期间有一小部分请求会被阻塞如果重建缓存非常慢等待线程可能堆积。逻辑过期是另一种思路缓存里不存真正的TTL而是存一个逻辑过期时间戳。比如把完整的师傅信息放到value里value附加一个expireTime请求来了如果发现逻辑过期不直接删缓存而是返回旧数据同时后台异步起一个线程去数据库加载新数据并替换缓存。这个方案的优点是读请求永远不会阻塞用户体验好缺点是逻辑上有一小段时间内用户会看到过期数据所以适合对数据一致性要求没那么极端的读多写少场景。理清这两个方案后面试官问了一个对比问题两个方案怎么选我说核心看业务容忍度如果业务上绝对不能出现旧数据选互斥锁如果更介意延迟和可用性选逻辑过期。在预约系统里我选的是互斥锁因为师傅的排班信息如果展示错了会导致用户约错时间代价远高于等待那几十毫秒。4.4 缓存雪崩过期时间打散与多级缓存第三个场景是缓存雪崩。面试官问如果大量缓存key在同一时间集体失效或者Redis集群突然不可用你怎么办我提出的方案分三个层面。第一个层面是过期时间设计。批量写入缓存时在基础过期时间上增加一个随机偏移量比如统一设10分钟但实际每个key的TTL在8到12分钟之间随机这样就避免了同一批key同一秒集体过期的尴尬。第二个层面是缓存集群高可用。Redis部署不能是单点至少要主从加哨兵大流量场景直接上Cluster。这样即便一台机器挂了缓存服务还能继续对外提供。第三个层面是兜底策略。即使缓存全挂了数据库也可能会被流量打爆所以需要做多层保护接口做限流、熔断、降级。比如查询师傅详情时如果Redis不可用本地可以有一个短期的进程内缓存兜底虽然数据一致性差一点但至少服务不挂。这就是多级缓存的思想从本地缓存到分布式缓存再到数据库每一级都承担一部分流量越靠前越快越靠后越可靠。面试官明显对兜底降级这个点很关注因为很多候选人都只会在前面加缓存而不考虑最后一道防线。4.5 缓存与数据库的一致性延迟双删能不能用第四个场景是原图级别的难题。面试官盯着我缓存更新你用的什么方案是Cache Aside吗如果写数据库成功、更新缓存失败你会怎么处理我承认当时有点紧张因为这个问题确实没有标准答案只有取舍。我答的是最常用的Cache Aside Pattern读的时候先读缓存没读到就读数据库再写缓存写的时候先更新数据库再删除缓存。也就是说缓存不是更新而是删除让下次读的时候重新从数据库拉取这比直接更新缓存更安全因为更新缓存需要考虑并发写问题。面试官追问先更新数据库再删除缓存如果删除失败了怎么办我介绍了两种工程上常用的补救手段。第一种是延迟双删也就是更新数据库后延迟几百毫秒再删一次缓存目的是把那段时间内可能读到旧数据的线程写入的脏缓存清理掉。但我也主动说这个方案不完美延迟时间很难定设置太短可能达不到预期效果设置太长又会影响性能另外如果第二次删除也失败了还是会有脏数据。第二种是更可靠的做法不把缓存删除操作放在业务代码里同步执行而是通过消息队列异步执行。更新数据库成功后发一条删除缓存的消息到MQ消费者拿到消息再去删缓存如果删除失败就重试直到成功为止。更进一步可以参考Canal这类工具基于MySQL主从同步的binlog来实时监听数据变更监听到变更后自动触发缓存删除这样业务代码完全不用感知缓存逻辑。面试官问延迟双删到底靠不靠谱我的回答是它是一个解决特定问题的过渡方案如果团队没有MQ也没有Canal用它是合理的但对一致性要求很高的系统延迟双删的延迟是它天然的硬伤最终还是要靠重试机制和binlog监听兜底。5. 终面与综合题Redis不是只有GET/SET5.1 持久化RDB、AOF和混合持久化怎么选终面面试官是一个看起来像架构师级别的人上来直接说前面聊了很多Redis的应用那我问你Redis挂了数据会丢吗我回答Redis的数据持久化有两种主流方式。RDB是定期把内存里的数据快照保存到磁盘它的优点是恢复快、文件紧凑适合做备份和灾备缺点是两次快照之间如果Redis宕机这段时间内新增的数据会丢。AOF是记录每次写操作的日志恢复时重放日志它可以根据appendfsync配置来平衡性能和安全性always每条写命令都刷盘最安全但最慢everysec每秒刷一次最多丢一秒数据是性能和安全的经典折中no由操作系统决定何时刷盘性能最好但最不可靠。面试官又问4.0之后的混合持久化你了解吗我说了解它是指在AOF重写时把当前内存的数据生成RDB格式的快照放在AOF文件头部之后的增量写操作再以AOF格式追加到后面。这样重启时加载速度快同时丢数据量也小。生产环境我一般推荐AOF开启everysec再配合定期RDB备份做兜底。5.2 内存淘汰策略与Redis的自我防御如果Redis内存满了写入怎么办面试官的问题转向了内存管理。我先是讲了Redis 4.0之后的8种淘汰策略重点说了几个常用的noeviction默认策略内存满了直接拒绝写入allkeys-lru从所有key中按最近最少使用淘汰volatile-lru只从设置了过期时间的key里按LRU淘汰allkeys-lfu和volatile-lfu则是按访问频率淘汰LFU能更好地处理曾经热度很高但现在已经没人看的数据。面试官问了一个实际场景你会选哪种我说大部分业务场景我会选allkeys-lfu或allkeys-lru而不是noeviction。因为缓存里天然就该容忍某些key被淘汰如果设成noeviction一旦缓存满了所有写操作直接报错这会让业务方感到缓存不可用反而不如静默淘汰不重要的数据。但要注意如果Redis里还存了少量不能丢的数据比如分布式锁的key那就必须单独规划空间或者确保这些key经常被访问不会被LRU/LFU策略误伤。5.3 Redis高可用架构主从、Sentinel、Cluster终面的第三个Redis问题是高可用。单机Redis扛不住你怎么扩展我按三个架构层次讲。第一层是主从复制一个主节点负责写多个从节点负责读主节点把写操作异步同步给从节点。这个方案解决了读的扩展但主节点挂了需要人工介入切换。第二层是Sentinel哨兵它在主从复制之上加了一套监控和自动故障转移机制当主节点不可用时Sentinel会自动选举一个从节点晋升为新的主节点对应用层基本透明。部署上Sentinel至少要三个实例避免出现假故障误判。第三层是Redis Cluster集群数据分片存储一共16384个哈希槽每个节点负责一部分槽位。客户端请求时先通过CRC16算法算出key属于哪个槽再找对应节点。Cluster解决了单节点容量和写扩展问题但引入了跨节点操作的复杂性比如多key的操作需要所有key在同一个槽里否则要使用Hash Tag这在设计key时要特别小心。面试官追问Cluster为什么是16384个槽而不是65536个这个问题其实考的是对设计取舍的理解。我答了几点16384个槽在节点数较少时已经足够心跳消息在节点间传播时用到的是位图来表示槽位信息16384个槽对应2KB的位图如果扩大到65536个槽位图就是8KB心跳包变大会白白消耗带宽而且Redis的作者也说过扩展到超过1000个主节点对集群会有明显压力16384相对来说是权衡了规模上限和消息开销的一个合理选择。5.4 Redisson分布式锁的看门狗到底在续什么分布式锁你们用的是哪个前面聊完高可用面试官突然问了这个问题。我说项目里用的是Redisson的RLock因为它实现锁的方式对业务侵入很小。lock()方法加锁时Redis中会创建一个keyvalue是一个Hash结构里面记录了持有锁的线程ID和重入次数。Redisson内部有个定时任务每隔锁过期时间的三分之一默认锁有效期30秒所以大约每10秒就会自动给锁续期这就是看门狗机制。它的作用是如果拿到锁的线程业务执行时间超过了锁的默认有效期锁不会在业务还没结束时被Redis自动释放避免其他线程在这期间拿到锁导致并发问题。面试官问了一个很经典的问题如果Redis主节点加锁成功但还没同步到从节点主节点突然挂了锁会怎么样我回答这会导致锁丢失另一个客户端连接新的主节点发现这个锁的key不存在于是也可以成功加锁两个客户端同时持有锁互斥性就失效了。理论上Redlock算法可以通过向多个独立的Redis节点依次加锁来缓解这个问题要求超过半数节点加锁成功才算加锁成功但Redlock本身也有争议因为它在极端场景下依然不能做到绝对的线性安全而且实现复杂。对绝大多数业务场景主从加哨兵配合Redisson的watch dog已经足够如果是资金类、对互斥性要求极其严格的场景需要重新审视锁方案的合理性。5.5 场景设计题一个秒杀系统你怎么用缓存扛住终面的最后一道题是开放式的场景设计假设我们要做一个厨艺课程的秒杀活动课程库存就100份但开抢瞬间可能有10万用户进来你用Redis设计一个秒杀系统。我先理清目标守住数据库、保住库存准确性、避免超卖。然后按请求链路从前往后说。第一层前端和后端接口层做拦截。秒杀按钮要限流比如同一个用户在一秒内只能请求一次还可以加验证码或者滑块把机器请求挡在外面网关层对秒杀接口做独立的限流配置秒杀接口的QPS上限甚至可以直接压到一个比较低的阈值超出部分直接返回抢购火爆。第二层Redis预减库存。请求进入后端后先用Lua脚本执行扣减库存的原子操作脚本内容大致是先判断库存key是否存在不存在则返回错误再判断库存值是否大于0如果大于0就减1并返回成功否则返回已售罄。用Lua是为了保证查库存-减库存两个操作在Redis单线程模型下不会被打断。只有这一步扣减成功的用户才有资格继续走下单流程。第三层把下单请求扔进MQ异步处理。用户的订单状态先标记为处理中后端消费者从MQ里取出订单消息真正写数据库订单表扣减数据库库存再异步通知用户下单结果。这样数据库同一时间接收的写请求不会超过消费者线程数从根本上避免了打爆数据库的情况。第四层思考超卖问题。超卖的本质是多个请求同时读到库存为1然后都认为自己可以扣库存。Redis预减库存配合Lua脚本保证同一时刻只有一个请求能把库存从1减到0其余请求全部返回失败所以数据库层的库存实际上不会出现负数。数据库里再加一个乐观锁兜底update course set stock stock - 1 where stock 0作为最后一道防线。面试官最后问秒杀热点key集中在同一个Redis节点上会不会有问题我说完全会这就是热key问题。秒杀课程的库存key是全局热点所有请求都打到同一个节点上节点CPU可能被打满。实际方案是热点key探测和多级缓存比如在秒杀开始前把库存key复制多个副本每个副本放在不同节点上请求随机访问副本但用Lua时要注意数据一致性同时可以把已售罄状态在本地缓存和CDN层做一层缓存一旦售罄后续请求直接返回不再穿透到Redis。6. 面试复盘比背八股更重要的是讲经历6.1 面试官真正在考察什么四轮技术面面完我最大的感触是大厂面试里所谓Java八股文只是入场券不是决胜点。网上热门的面试题、题库、背题手册这些内容确实有用它能帮你在短时间内快速建立知识体系的骨架比如JVM内存模型、Spring Bean生命周期、Redis持久化方式、MQ消息可靠性等等。但真实面试中面试官几乎不会只满足于你对或者你背得熟他们会在你答完之后继续追问I why和那你怎么做的。你会发现真正让你显得有竞争力的不是把某个机制背下来而是能不能快速把它和具体场景结合讲出这个问题我在哪个项目里遇到过、我当时是怎么处理的、最后效果怎么样、还有没有更好的方案。一面到终面给我的感觉是筛选逻辑层层递进一面看基础是否扎实二面看框架理解是否深入三面看有没有分布式系统的实战认知终面看能不能独立设计一个完整的方案。每一个环节淘汰的都是只会背结论的人留下的是能讲清楚决策过程的人。6.2 我的准备方法与踩坑建议最后分享几条实际操作层面的经验。第一复习时用面试官视角整理知识树。我习惯把每个知识点自问三个问题它是什么它解决了什么问题不用它会怎样比如Spring Boot自动配置如果只是死记AutoConfigurationImportSelector读取配置文件很难扛住追问但把它理解成为了消除大量xml配置Spring Boot在启动时根据classpath下的依赖和配置按条件自动装配需要的Bean再结合自己写的starter经验整条线就串起来了。第二项目经验一定要整理成场景-问题-方案-结果的四段式。不要写我负责订单模块而是写订单高峰期接口RT上升我通过Redis缓存热点菜品和师傅信息把接口平均耗时从180ms降到20ms。面试官最喜欢从这种描述里挖细节为什么不加本地缓存为什么用Redis不用Memcached缓存和DB一致性怎么保证这些问题如果你在简历上埋过点面试时就能主动引导话题。第三面试后及时复盘。我每面完一轮会立刻把没答好的问题记录下来查资料、写代码验证下次面试前重新过一遍。这套方法看着笨但效果出奇好因为面试官问的问题往往高度相似你上一次卡壳的题很可能在下一次面试以变形姿态出现。这次三轮技术面加一轮终面整体给我的体感是分布式缓存部分难度最高因为它不只看你会不会用Redis更看你在极端场景下懂不懂取舍Spring Boot部分则最看源码功底因为它考的不是用过没有而是拆过没有。如果你也准备面大厂Java岗我建议你把你项目里的缓存方案、框架用法和故障案例当成复习主线每一条都尽量往深了挖挖到你能跟一个没见过你项目的人讲明白为止。这样的面试准备过程即使最后没有拿到offer你也一定比备战之前强了一大截。
返回列表