拓冰建站拓冰建站
首页 / 资讯中心 / 正文

字节跳动后端面试复盘:Java并发、MySQL与系统设计核心考点

接到字节跳动后端岗位的面试邀请时我正在一家做电商SaaS的公司做Java后端日常写接口、调优SQL、偶尔跟产品经理battle需求项目经验谈不上多惊艳但好在一直在真刀真枪地跑线上流量。我投的是基础架构方向之所以选这个方向是因为平时看源码看得多对并发、中间件这块有点底气。整个面试流程走完大概三周三轮技术面加一轮HR面记忆最深的是面试官几乎不问你“背过什么”而是不断抛出一个场景让你现场拆解、推导、写代码。这篇文章就按我实际遇到的流程来写把每轮的核心问题、我的解答思路、后来复盘时补充的细节都整理出来希望能给准备后端面试的同学一些参照。1. 面试整体流程与考察重点拆解先说整体感受字节的后端面试不像有些公司那样上来就让你手撕红黑树而是先从一道中等偏上的算法题切入然后围绕你的项目经验逐步展开。每个面试官手里好像都有一张“能力地图”会从基础语言、操作系统、网络、中间件、分布式几个维度交叉提问。如果你在某一个点上深入讲了面试官大概率会顺着追问到底直到你露出破绽或者确实讲清楚了为止。1.1 四轮面试的节奏安排整个流程大概是这样的第一轮是基础技术面持续大概六十分钟前半段写算法题后半段问Java并发、JVM和一些MySQL索引相关的问题。第二轮是项目深挖加场景设计面试官会提前让你准备一个最有代表性的项目从项目背景一路问到技术选型、性能瓶颈、故障处理然后会出一个系统设计题。第三轮是综合面面试官通常是部门的技术负责人问的问题更宏观比如“你怎么看待服务治理”“怎么评估技术债务”同样也会安排一道算法题。第四轮是HR面主要聊职业规划、离职原因、薪资期望但也会穿插一些业务理解相关的问题。从考察侧重点来看字节非常看重“计算机基础 工程落地”的结合。比如一问到HashMap不会只让你说“数组加链表”而是会继续问“红黑树为什么是8的时候转换”“头插法和尾插法在并发环境下分别有什么问题”。这种问答方式很考验平时是否真的读过源码而不是死记结论。1.2 每一轮的核心考察能力分析第一轮核心考察的是候选人的“下限”也就是语言基础是否扎实、代码能不能写利索、对常见中间件是否有正确认知。这一轮不建议过度发散面试官问什么就精准回答什么把原理讲透就好。第二轮核心考察的是“深度和边界”面试官会刻意往你不太熟悉的领域试探重点是看你在不知道答案时的反应是坦然说不太了解还是硬着头皮瞎编。这一轮如果能在项目细节中讲出自己做的性能对比、踩坑记录会很加分。第三轮核心考察“系统观”比如给你一个“用户下单后30分钟未支付自动关单”的需求你会怎么设计。这种题目没有标准答案面试官想看的其实是你的思考框架有没有考虑延迟消息、定时扫描、消息队列、分布式锁、数据一致性这些要素以及你如何取舍。2. Java基础与并发编程核心考点字节一面最喜欢从Java基础切入因为它能快速判断一个后端工程师的真实水平。如果你简历上写着熟悉Java并发那ConcurrentHashMap、volatile、synchronized这些几乎必问。我把自己被问到的几个核心问题记录下来并补上了后来查漏补缺的内容。2.1 HashMap和ConcurrentHashMap的底层演进面试官问的第一个问题是“HashMap在多线程环境下会出什么问题JDK 8之后为什么改用尾插法”我当时从三个方面回答第一多线程put可能导致数据覆盖两个线程同时计算到同一个桶位一个key被另一个key覆盖第二JDK 7的头插法在扩容时会产生环形链表导致get操作死循环JDK 8改成尾插法解决这个问题第三虽然尾插法避免了死循环但数据覆盖问题依然存在所以并发场景必须用ConcurrentHashMap。接着面试官追问“ConcurrentHashMap在JDK 8里是怎么保证线程安全的”这里要精准回答放弃了JDK 7的分段锁改为CAS加synchronized。put操作时如果对应桶位为空通过CAS尝试插入如果不为空则对链表头节点或红黑树根节点加synchronized锁。注意synchronized锁住的是单个桶位而不是整个数组所以在不同桶位上并发操作时互不干扰竞争粒度比Segment更细。还有一个加分点JDK 8的ConcurrentHashMap在扩容时引入了协助扩容机制helpTransfer多个线程可以一起帮助迁移数据而不是只让执行put的线程单独干活。面试官如果追问到这里你可以说“扩容时会根据CPU核心数设置一个步长每个线程负责一段连续桶位的迁移”这就是读过源码的体现。2.2 volatile和synchronized的内存语义差异“volatile能不能保证原子性”这是我被问到的另一个高频题。答案是不能它只保证可见性和有序性。可见性是因为volatile变量在读的时候强制从主内存加载写的时候强制刷回主内存并失效其他线程的缓存行有序性是通过内存屏障禁止指令重排序。面试官会更关注你能否举出实际例子。我用的是单例模式双重检查锁的经典案例instance变量不加volatile时多线程可能拿到一个“半初始化”的对象。原因是new Singleton()在JVM层面有三个步骤分配内存、初始化对象、把引用赋值给变量。如果没有volatile步骤2和3可能被重排序线程A先执行了步骤3但还没执行步骤2线程B在步骤3之后读到引用拿到的就是没初始化完的对象。内存屏障这块我多说了一句在x86架构下volatile写会触发StoreStore屏障和StoreLoad屏障读会触发LoadLoad屏障和LoadStore屏障。虽然这套理论在现场有点进阶但面试官听完明显会对你有更好的印象因为这是《Java并发编程的艺术》里面级别的细节。2.3 ThreadLocal的内存泄漏风险与正确用法一面最后几分钟面试官抛了个偏门问题“ThreadLocal在ThreadLocalMap里是怎么存数据的为什么会内存泄漏”这个问题我刚好在项目里踩过坑。ThreadLocalMap是Thread类里的一个成员变量Entry的key是ThreadLocal对象而且是弱引用value是强引用。当ThreadLocal对象不再被外部强引用后key会被GC回收变成null但value还留在Map里如果这条线程是线程池里的长期存活线程Value就永远无法释放形成内存泄漏。正确的使用方式每次用完ThreadLocal都显式调用remove()尤其是在事务型工具类、请求上下文拦截器这种场景里finally块中必须remove。面试官可能接着问为什么不用强引用答案是弱引用已经能在绝大多数场景下保证ThreadLocal本身的回收漏掉的只是value这是空间换时间的取舍。如果在这里能说出“ThreadLocalMap的set方法会主动清理过期Entry但只有下一次set或get时才会触发”就说明你确实看过源码。3. MySQL索引与事务的底层逻辑后端面试离不开MySQL字节问MySQL的方式不是让你默写索引结构而是给一条慢SQL让你现场分析为什么会慢、怎么优化。这一轮内容量最大我把它拆成索引、事务隔离、锁三个小节。3.1 索引失效的典型场景与底层原因面试官给了一条SQL“select * from user where status 1 and create_time 2024-01-01其中status是普通索引create_time是索引还是普通字段这条SQL索引会失效吗”这条题的关键点在数据分布。如果status只有两个值0和1而且大部分用户status都是1那么走status索引反而会扫描大量行。优化器基于成本测算大概率会选择全表扫描而不是索引扫描。这就是我们常说的“回表代价比全表扫描还高”的情况。接着面试官问我“你遇到过哪些索引失效的场景”我总结了四个高频场景对索引列使用函数where DATE(create_time) 2024-01-01索引就会失效改成create_time 2024-01-01 and create_time 2024-01-02隐式类型转换字符串字段不写引号带头模糊查询like %张OR条件中有一个字段没有索引。这里要补充一个底层原因B树的索引列是按值的排序存储的一旦对列做函数运算原来的B树顺序就不复存在所以优化器只能放弃索引。面试中如果你能把“存储顺序”这个底层概念讲出来会比其他候选人的“死记硬背听上去老练很多”。3.2 事务隔离级别与MVCC的可见性判断“MySQL默认的事务隔离级别是什么RR级别能解决幻读吗”这是字节二面的问题答案比想象中要细。MySQL默认隔离级别是Repeatable Read它通过MVCC加间隙锁解决了一部分幻读问题。MVCC里事务在第一次执行select时生成ReadView之后的普通读都复用这个ReadView保证能看到的事务版本是一致的。但如果你在同一事务里先查询再插入一笔新数据再查询新数据仍然能被看到——因为插入操作会生成一个新的版本而ReadView只在部分场景下才会重新生成。进一步追问“那RR下幻读到底解决了没有”严谨说法是普通快照读的场景下幻读被MVCC解决了但当前读select ... for update场景下需要通过间隙锁配合Next-Key Lock来阻止其他事务插入数据才能避免幻读。如果只用默认的MVCC而不加锁在“先快照读、后当前读”的混合场景下依然可能出现幻读。回答到这里面试官会认可你的理解深度。3.3 慢SQL排查的具体实操方法聊完原理面试官把问题拉回现实“你们线上有慢SQL你怎么排查”我给出的路径是先用慢查询日志定位具体SQLslow_query_log开启后看long_query_time阈值通常是1秒或2秒。拿到SQL后先explain关注typeALL表示全表扫描、key实际用的索引、rows预估扫描行数、ExtraUsing filesort、Using temporary都是警告信号。如果是order by create_time导致的Using filesort就需要检查排序字段是否有索引如果是join产生的Using temporary考虑是否可以把数据量小的表作为驱动表。排完上面的还不慢就去看是不是数据量大导致的扫描行数太多这时候考虑分页优化把limit offset, size改成基于上一页最大id的写法比如where id 上一页最大id order by id limit 20这条路子对大数据量深分页非常有效。经验之谈85%的慢SQL都是索引没建对导致的所以优先解决索引而不是急着改SQL写法。我在实际优化中遇到过一条跑了3秒的订单查询加了联合索引后降到30毫秒改动只有一行SQL加一个索引收益非常直观。4. Redis、消息队列与分布式设计字节的面试到了二面后半段和三面就会切入分布式相关的内容。这个板块考察的是你在真实业务里有没有处理过并发、一致性、高可用问题。我这边遇到的问题是缓存一致性、分布式锁和消息队列积压。4.1 缓存与数据库一致性怎么保证面试官出了一个业务场景“用户修改了个人信息你需要更新MySQL同时更新Redis中的缓存你会怎么做怎么保证两边最终一致”最常见但错误的做法是先更新数据库再删除缓存或者先删缓存再更新数据库。前者的问题是如果删除缓存失败缓存里保留的就是旧数据后者的问题是如果更新数据库失败缓存已经被删了下一次查询会重查DB短暂把压力打到数据库上但数据最终是一致的。比较稳妥的做法是延迟双删先删除缓存更新数据库休眠几百毫秒再删除一次缓存。但休眠时间不好确定如果并发极高第二次删除也可能把别人刚写入的新缓存删掉。另一个方案是订阅MySQL的binlog通过CDC组件异步删除对应key的缓存或者直接刷新缓存。真实线上我更推荐“先更新数据库然后把删缓存操作放到MQ里消费者确保删除成功”这样即使第一次删失败异步重试也能兜底。还有一个细节为什么我不选“先更新缓存再更新数据库”因为缓存和数据库是两个存储在写并发场景下很容易出现一个写库成功写缓存失败另一个写缓存成功写库失败两边错乱排查起来特别头疼。所以主流方案里“Cache Aside”还是最稳妥的。4.2 Redis分布式锁的坑从setnx到Redisson“让你设计一个分布式锁你会怎么做”这个问题几乎每场后端面试都会出现。第一版思路是set nx ex也就是SET lock_key value NX PX 3000只有一个线程能成功其他线程拿不到锁。但这里有个致命问题如果持有锁的线程执行时间超过锁过期时间另一个线程拿到锁执行两个线程同时操作共享资源锁就失效了。解决方向是引入看门狗机制Redisson里默认每10秒会重置锁的过期时间为30秒只要业务没执行完锁就不会过期。但这里也要注意如果业务有耗时IO比如远程调用超过30秒看门狗也可能失效所以分布式锁比较适合保护本地资源访问不适合包住一大段远程调用。还有一个是锁的可重入问题。Redisson用了Hash结构来表示可重入锁内部维护一个计数器和线程标识。面试官如果问“为什么用Hash”你可以答因为需要区分不同的线程并且要支持同一个线程多次加锁、减锁。4.3 Kafka在项目里的具体落地细节面试官从我的项目里挑出一个用Kafka的场景问了三个递进的问题如何保证消息不丢失、如何保证消息不重复消费、消息积压怎么办。保证不丢失要从三个方面解释生产者端开启acksall并且重试Broker端设置min.insync.replicas和replication.factor消费者端关闭自动提交offset改为在业务执行成功后再手动提交。光说这三点还不够要补充你们项目实际用的参数值我说的是acksall、min.insync.replicas2、retries3、enable.auto.commitfalse。保证不重复消费核心是幂等消费消费端在处理逻辑里带上业务唯一ID先查记录是否存在存在就直接返回或者利用数据库的唯一索引防止重复插入。我在订单消息里就是用订单号做唯一索引重复消息插入时会报DuplicateKey捕获异常后跳过就好了。消息积压的处理思路是先提升消费者并发度比如增加分区数和消费者线程或者临时把消息转发到另一个队列先用空队列快速消费再慢慢处理。如果积压严重也可以临时写脚本把积压消息导出等系统恢复后再导入重新消费。面试官比较欣赏这种“先止血再排查”的思路。5. 系统设计题与算法题的实战复盘第三轮面试基本就是系统设计加算法题。系统设计题通常不会太难属于“给一个业务场景你给个高可用架构”的类型但算法题是必须过的硬门槛。这板块我多写一点自己的做题心得和踩坑经验。5.1 设计一个“30分钟未支付自动关单”系统这是一个很经典的场景题主要考察延迟消息和分布式事务思维。我在回答时分了三层第一层是普通方案启动一个定时任务每分钟扫一次订单表把超时未支付订单关掉实现简单但数据量大时对数据库压力很大实时性也差。第二层是用RabbitMQ或Kafka的延迟消息订单创建后发一条延迟消息30分钟后消费者收到消息检查订单是否支付未支付就关单实时性好但消息队列里的消息可能丢失需要配合定时任务兜底。第三层是两者结合核心链路用延迟消息定时任务作为兜底巡检双保险。这个分层的回答结构比较加分因为它展示了“先实现、再优化、再保证可靠性”的工程思路。过程中面试官追问“如果订单量非常大定时扫描怎么设计”我说可以按订单ID取模开启多个定时任务分片扫描配合索引下推能显著降低数据库压力。5.2 字节算法题的常见题型与手感字节算法题的风格是LeetCode中等偏上难度大概率是动态规划、二叉树的遍历变形、双指针滑动窗口、贪心、回溯这几类。我遇到的是一道“给你一个字符串找出最长的不含重复字符的子串长度”属于滑动窗口的经典题。我的解题思路用HashMap记录字符和它最近出现的位置维护left和right两个指针。right每走一步如果当前字符在map里已经出现过就把left移动到map中记录的旧位置加1。这里有个坑每次更新left时不能直接取旧位置加1得取max(旧left, 旧位置1)否则left可能倒退。我在白板上写的时候第一版就漏了这个max判断好在测试用例提醒了我。算法题备考的经验我总结了几条每天至少刷两道保持手感不要只刷会的题每种题型都要总结模板比如二叉树递归的三段式终止条件、分治、合并做过的题要写题解和复杂度分析方便二刷三刷。字节面试的算法题不要求你想出最优解但要求你“边写边讲”把思路讲清楚比一上来就写对更重要。5.3 系统设计题里如何表现自己的架构思维系统设计题目的考察点很综合。比如“设计一个短链接系统”我会先明确需求QPS大概多少数据量多大需要哪些功能如果是十万级QPS那么生成短链的主流程是接收长链接生成短码写入MySQL再用Redis做缓存加速查询。短码生成要避免分布式ID冲突一般用雪花算法或发号器。然后要考虑查询链路用户访问短链时先查Redis缓存命中直接重定向缓存未命中再查MySQL并回填缓存。要设计过期策略来防止缓存无限膨胀。最后还要补充高可用多副本部署、限流降级、并发冲突处理。面试官并不期待你设计方案面面俱到而是看你能不能抓住主干需求分析、架构选型、数据存储、缓存、高可用、容灾。每个环节都能说出“为什么这样做”在面试官看来就算合格。这个能力是靠平时多做项目复盘并且多问自己“如果流量再大十倍系统会先挂在哪”来练习的。6. 面经之外简历、面试心态与避坑经验最后一个部分写点面经相关的软技巧也是我在多次面试中真切体会到的“隐形加分项”和“隐形扣分项”。技术能力确实决定能不能过但很多细节会影响面试官的主观评价。6.1 简历项目经验怎么描述更容易被追问简历上的项目经验千万不要只写“负责XX系统的开发”而是要写成“项目背景 我的职责 核心难点 最终成果”。比如“负责订单中台的重构通过引入Redis缓存和异步消息将订单查询性能提升了80%支撑日均十万级订单量。”这样写面试官很容易找到追问点比如“你怎么做的性能分析”“Redis缓存的一致性怎么保证”“为什么用Kafka而不是RocketMQ”。你在写简历时就要对每一个细节提前准备好两三个层次的故事。我不太建议在简历上堆砌技术名词比如“精通高并发、分布式、微服务”这些空话。面试官随机挑一个名词深问你你说精通高并发那你说说你们项目的QPS是多少峰值瓶颈在哪里如果你的回答支支吾吾反而会被扣分。宁可少些形容词多写可验证的数据。6.2 谈项目时的两个核心原则原则一不要把自己包装成项目里所有模块的负责人。面试官经验丰富往往能从一个细节里判断出你是真的做过还是背过别人的方案。回答项目问题时多讲“我遇到的一个具体问题”和“我是怎么定位和解决的”这种第一视角的真实感比“我们用了Redis做缓存极大提升了性能”这种套话有说服力。原则二主动说出项目的不足和后续规划。面试官问你“有什么可以改进的地方”如果你回答“没有了”会显得缺乏深度思考。我通常会指出项目里一个已知问题并能给出可行的优化方向比如“我们目前的缓存更新策略是延迟双删但极端情况下还是可能短暂不一致后续打算引入binlog监听方案替代它”。6.3 面对不会的问题怎么回答没人能保证每个问题都会。我在第三面时遇到一个“SSE(Server-Sent Events)在Nginx层怎么配置超时时间”的问题我当时对这块不熟直接说“这个我没有在生产环境实际配过只了解基本原理”然后把自己知道的部分比如SSE和WebSocket的区别讲了一遍。这样做的效果比硬编要好得多。字节面试官一般是想看你有没有快速学习能力而不是你有没有背完所有知识点。你可以在回答完“不会”的问题后顺手展示你的理解思路“如果是我的话我可能会查官方文档确认参数然后在测试环境压测验证。”这种答法既诚实又展示了你解决未知问题的方法论。6.4 面试节奏和时间分配的技巧字节每轮面试时间固定在60分钟左右算法题通常会占用20到30分钟剩下的时间聊项目和基础。所以算法题不能耗时太久如果15分钟还没思路就直接跟面试官说“我想到一个暴力的解法先写出来再优化”好过一直卡住。基础问答环节要控制回答节奏既不要只报结论也不要长篇大论。常见的做法是先给出结论然后补一个简单的原理解释最后加一句实际使用场景。比如面试官问“HashMap为什么用红黑树”你可以说“因为链表长度过长时查询退化为O(n)红黑树能让查询保持O(logn)”然后补一句“实际上JDK 8是在链表长度超过8时转树”这样既清晰又全面。面试结束前通常会有反问环节建议准备一到两个有深度的问题比如“你们团队目前最大的技术挑战是什么”“新人在这个团队熟悉业务大概要多久”。这样可以体现你的主动性也帮你判断团队与自己的匹配度。7. 面后复盘与经验沉淀面试结束后我第一时间把遇到的每个问题整理成文档并标注了当时答得好的地方和卡壳的地方这些记录后来成了我准备下一轮面试的重要素材。这里也分享一个个人习惯我会在每次面试后把核心问题用“费曼学习法”重新讲一遍——假装对面坐着一位同事给他讲清楚一个技术点。如果讲得磕磕绊绊就说明这个点还没真正掌握需要重新翻资料。字节的一面面试官当时说过一句话我印象很深“面试不是让你表演知识点而是想看你怎么拆解和解决一个陌生问题。”所以很多题目没有标准答案关键在于你的分析思路是否清晰、是否具备全局视野。这也是我后来在做项目复盘时最在意的一点技术选型永远不只看两边的功能对比更要考虑团队维护成本、业务演进方向、故障恢复手段这些实际因素。我最后想分享的一点是面经只是参考它的价值不在于让你背假答案而在于帮你梳理出高频考点和考察方式。真正的底气还是来自平时写过的每一行代码、处理过的每一个线上事故和踩过的每一个坑。这些积累一旦足够深面试时自然会形成肌肉记忆顺着问题层层展开把你最真实的工程能力呈现给面试官。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门