美团后端开发岗面经合集:JVM 锁、系统调用与 Redis 淘汰策略
- 轮次
- 多轮面试合集
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
面经 01
- 为什么你这个专业要找互联网工作?
- CSS 匹配缓存怎么设计?
- 什么是 Copy-On-Write 操作?
- AI 项目高并发怎么做?
- 线程池设计需要注意什么?
- 什么时候用锁?什么时候不用?
- 原子变量是什么?
- 数据库表结构怎么设计?
- 如何保证消息顺序?
- 数据量大怎么办?
- 索引什么时候会失效?
- 为什么用 MQ?
- 如何保证消息不丢?
- MVCC 原理?
- 是否接受写 Java?
面经 02
- 实习期间你具体做了什么?数据从哪里产生,主要分析什么?
- 你们部门是开发数据分析 agent 这个数据产品,还是数据部门自己去挖掘数据?
- 这个数据产品具体有哪些功能?
- 它实际上是怎么做的?是不是做了能力适配,让业务团队直接使用大模型?
- 内置的 MCP 做了哪些工作,让数据分析 agent 表现更好?你在设计过程中参与了哪些具体事项?
- 在意图识别这块,你具体做了什么?
- 进入意图识别之后,针对深度分析等场景是怎么适配的?主要是装配 Skill 和 MCP 吗?
- 除了 Skill、MCP 和思考强度限制,还有哪些方式能让模型在特定场景下表现更好?
- 生产环境那个实例挂掉,是通过数据分析 agent 做的吗?你是怎么分析 dump 文件的?
- 这个数据产品是不是有页面,不同业务线用户可以上传文件、数据或文档做分析?底层是怎么实现的?
- 招聘助手按渠道适配、消息路由,最终产品形态是什么?是浏览器插件还是别的?
- 是本地起服务、本地起页面吗?不同渠道是否需要打通实际招聘平台?
- 你说的渠道具体指什么?
- 岗位这些数据从哪里来?
- 这个助手的作用是什么?是从招聘平台获取信息后,在本地页面按渠道分发到社交平台吗?
- 它怎么帮用户找到合适岗位?需要爬取吗?有爬虫吗?
- 具体涉及哪些外部招聘平台?是爬虫还是用户登录?登录和反爬怎么解决?
- 你平时用得比较多的 AI 编程工具或模型是什么?实际体验如何?
- 你怎么理解 Prompt Engineering 和 Context Engineering?
- 你了解现在主流的 Agent 架构吗?
- ReAct 为什么不适合大型复杂项目?
- 融合架构整体采用 Plan,如果初始 Plan 就错了,或者中间步骤确认后发现问题,怎么办?
- Java 中 List 里放自定义对象,怎么排序?
- 自定义对象如何实现排序?
- Lambda 表达式背后的接口是什么?
- synchronized 修饰静态方法和实例方法有什么区别?锁对象分别是谁?类作为锁对象时有对应实例吗?
- 加锁之后锁标记放在哪里?怎么保证互斥?
- synchronized 修饰实例方法后,标记该实例已被当前线程持有,这个标记信息存在哪里?
- Java 多态中,父类引用指向子类实例,实际执行子类行为,动态绑定具体是怎么实现的?
- Java 线程池里 execute 提交任务和 submit 提交任务有什么区别?
- volatile 的实现原理,你知道哪些层次都可以说说。
- volatile 的这些功能,在 JVM 和操作系统层面分别怎么支持?
- 策略模式和模板方法模式有什么区别?
- 如果进程申请的内存远超过物理内存,操作系统怎么处理?
- 发生系统调用时,比如读文件,操作系统会经过什么流程?
- 内核处理系统调用时,进入和返回后,和应用程序怎么交互?
- 系统调用通常带参数,比如 read,内核怎么知道读哪个文件?返回后怎么回到应用程序下一行继续执行?
- 保存上下文的表具体在哪里?存在哪?
- TCP 第四次挥手时,第四个数据包丢失会怎么样?
- 是重发第三次挥手吗?
- 如何用 UDP 实现 TCP 的可靠传输?
- MySQL/InnoDB 的日志主要有哪些?各自作用是什么?
- 一个事务提交时,redo log、undo log、binlog 的落盘顺序是什么?
- Redis 的淘汰策略你了解吗?
- LRU 和 LFU 各有什么不足?
- 怎么优化 LFU?Redis 里 LFU 是怎么实现的?会不会增加开销?
- 怎么保证 LFU 的计数准确?
- 手撕:盛最多水的容器。
面经 03
- 项目几个人开发、多少 PD、多大体量?只有一个后端吗?
- 埋点上报最终数据存到哪?埋点平台底层怎么存储?会丢吗?
- 上报这个数据合理吗?采集的都是什么数据?为什么这种数据要存到服务端?
- 有没有开发时才发现不该由你们来开发的例子?没有技术方案吗?
- 上线之后有没有问题?线上 bug 之类?
- 这个工作流是谁派发的任务,还是你自己想做的?是导师提的你来承接吧?
- 任务是先执行再检查再继续,全自动的过程,那第一个任务代码就有问题,会把错误放大,出现过吗?怎么解决的?是逻辑问题还是别的?
- 同事用后给过什么反馈?生成的代码符合团队代码规范吗?没考虑过加规则约束吗?分析过原因吗?
- 用 AI 这么久,模型生成代码质量不高有哪些原因?任务给多了注意力转移你发现过吗?
- 你们主要用 Codex,是吧?没用 Claude Code?
- 你做的东西调研过业界吗?
- AI 返回的结果业务能直接看懂吗,还是需要技术人员再转换话术?有没有考虑继续迭代?
- MySQL 事务的隔离级别有哪些?
- MySQL 优化做过吗?慢查询如何处理?
- Java 线程池的执行流程是什么?从添加任务到拒绝策略如何处理?
- 线程的创建过程是什么?
- MQ 的主要使用场景是什么?开发消费者有哪些原则?幂等如何实现?
- Redis 如何保证原子性?如何实现只消费一条?为什么用 Lua 脚本而不用 SETNX?
- Spring 事务有哪些实现方式?代码如何编写?
面经 04(原帖在此处被截断)
- 面试官开始提问(原帖截断)
《参考解析》
1. Copy-On-Write 与缓存设计。 COW 的核心是「读共享、写复制」:多个读者共享同一份数据,写者先复制一份再改,改完原子替换指针,读者要么看到旧版本要么看到新版本,不会读到半成品。Java 的 CopyOnWriteArrayList 就是每次写都复制整个数组,适合读多写少、数据量小的场景(比如监听器列表),写多时会频繁复制、内存抖动。Redis 的 BGSAVE 也是 COW:fork 出子进程共享页表,父进程写时复制页面。浏览器里的「CSS 匹配缓存」是另一个层面的缓存——把选择器匹配结果按元素/规则缓存,DOM 变化时按失效范围清理,设计要点是缓存的 key(元素 + 规则集 + 状态伪类)、失效粒度(避免一个 class 变化就全量失效)、以及内存上限。
2. 线程池、锁与原子变量。 线程池设计关注:核心/最大线程数、有界队列、拒绝策略、线程命名(便于排查)、异常处理、以及池的隔离(不同业务不共用)。用锁的判断是「有没有共享可变状态、临界区是否真的需要互斥」:无状态服务、局部变量、不可变对象都不需要锁;能用原子变量/CAS 解决的单变量计数不要用锁;读多写少可以用读写锁或 COW;跨进程才考虑分布式锁。原子变量(AtomicInteger、LongAdder)靠 CPU 的 CAS 指令保证单变量原子性,LongAdder 在高并发下用分段累加降低竞争,最后求和。
3. 消息顺序、可靠性与表设计。 保证顺序的三要素:同一业务键路由到同一分区/队列(Kafka 用 key 做 partition,RocketMQ 用顺序队列)、单分区内单消费者串行消费、消费失败不要跳过(否则顺序断裂)。不丢消息要三段都做:生产端确认(acks=all + 重试)、Broker 持久化与副本(min.insync.replicas)、消费端手动提交 offset(处理成功再提交)。表设计先定访问模式:主键选择、避免大宽表、冷热字段分离、必要的冗余(减少 join)、以及分库分表的 shard key;数据量大时先看能不能归档/分区,再考虑分片。索引失效的典型场景:对列做函数/运算、隐式类型转换、like '%x' 前导通配、联合索引不满足最左前缀、or 两边有一边没索引、以及优化器判断全表扫更便宜。
4. MVCC。 InnoDB 给每行加隐藏的 DB_TRX_ID(最后修改的事务 id)和 DB_ROLL_PTR(指向 undo log 的版本链)。快照读时,事务拿着自己的 read view(活跃事务 id 列表、最小/最大事务 id),沿版本链找「对当前事务可见」的版本:已提交且早于快照的可见,未提交或晚于快照的不可见,继续沿 undo 找。RC 每次查询生成新 read view,RR 在第一次查询时生成并复用,所以 RR 能避免不可重复读;幻读靠间隙锁(当前读)或快照读的版本可见性来规避。MVCC 只作用于快照读,select ... for update、update、delete 走当前读并加锁。
5. synchronized、动态绑定与 volatile。 synchronized 修饰实例方法锁的是 this 对象,修饰静态方法锁的是 Class 对象(类只有一个,所以是全局锁),修饰代码块锁的是括号里的对象。锁信息存在对象头的 Mark Word 里:偏向锁记录线程 id,轻量级锁记录指向栈上 Lock Record 的指针,重量级锁指向 monitor;JVM 通过 CAS 修改 Mark Word 来抢锁,失败则膨胀升级。多态的动态绑定靠虚方法表(vtable):每个类有一张方法表,对象头里的类型指针指向类元数据,调用虚方法时按运行时类型查表,找到子类重写后的入口。volatile 保证可见性和有序性:JVM 层插入内存屏障,硬件层靠缓存一致性协议(MESI)让其他核的缓存行失效,写操作直接写回主存。execute 提交 Runnable 且不返回结果、异常会直接抛给线程池的未捕获异常处理器;submit 提交 Callable 返回 Future,异常被封装在 Future 里,不 get 就看不到。
6. 内存、系统调用与 TCP。 进程申请的内存超过物理内存时,操作系统用虚拟内存 + 按需分页:先只分配虚拟地址空间,访问时缺页中断再加载;物理内存不够就把不活跃页换出到 swap(或直接 OOM kill)。系统调用流程:用户态把系统调用号放进寄存器、执行 syscall 指令陷入内核 → 内核保存用户态上下文(寄存器、栈指针、返回地址)到内核栈 → 按调用号查系统调用表执行 → 参数通过寄存器/用户态指针传入,内核用 copy_from_user 校验并拷贝(read 的 fd 用来查进程的文件描述符表)→ 返回时恢复上下文、把返回值放回寄存器、切回用户态继续执行下一条指令。上下文保存在内核栈和 task_struct 的 thread_struct 里(x86 保存 rsp/rip/rflags 等)。TCP 第四次挥手(客户端最后的 ACK)丢失时,服务端停在 LAST_ACK 并超时重传第三次挥手(FIN+ACK);客户端已经进入 TIME_WAIT,收到重传的 FIN 会再回一个 ACK,所以不会卡死,只是服务端晚一点关闭。
7. InnoDB 日志与落盘顺序。 redo log 是物理逻辑日志,记录页的修改,保证崩溃恢复(WAL);undo log 记录旧版本,用于回滚和 MVCC;binlog 是 server 层的逻辑日志,记录语句/行变更,用于主从复制和归档。提交顺序:先写 redo(prepare 状态)→ 写 binlog → redo 标记 commit,即两阶段提交(2PC),保证 crash 后 redo 与 binlog 一致;组提交(group commit)把多个事务的 fsync 合并以降低开销。
8. Redis 淘汰与 LFU。 淘汰策略八种:noeviction(写满报错)、allkeys-lru、allkeys-lfu、allkeys-random、volatile-lru、volatile-lfu、volatile-random、volatile-ttl。LRU 的不足是「偶发一次全量扫描会把冷数据顶成热数据」(缓存污染);LFU 按访问频率淘汰,能抵抗偶发访问,但老数据的计数会一直累积、新数据难以出头。Redis 的 LFU 用 8 bit 存计数(概率递增,到 255 饱和)加 16 bit 存「上次衰减时间」,按 lfu-log-factor 控制增长难度、lfu-decay-time 控制衰减,避免计数无限累积。它不增加额外内存(复用 LRU 字段),但计数是近似值(概率递增 + 定时衰减),所以「准确」只能做到趋势正确;需要精确计数就得自己维护哈希表,代价是内存和复杂度。
9. 手撕:盛最多水的容器。 双指针从两端向中间收:面积 = min(h[l], h[r]) * (r - l),每次移动较矮的一侧(因为移动高的一侧不可能让面积变大),O(n) 时间、O(1) 空间。要能解释为什么移动矮的一侧不会漏掉最优解:矮边是当前面积的瓶颈,固定它、换更高的边只会让宽度变小,所以以它为一边的最优解已经枚举过了。
10. 工程与 AI 协作。 埋点数据一般走「客户端上报 → 网关/日志服务 → Kafka → 数仓(Hive/Iceberg)」,底层是列式存储 + 分区,丢失靠客户端本地缓存重发和服务端幂等去重。AI 生成代码质量不高的常见原因:上下文不足(没给足相关文件和规范)、任务粒度过大(一次改太多处,注意力分散)、缺少验证(没有测试/编译反馈)、以及模型对项目约定不熟。改进靠「小步任务 + 明确验收 + 工具反馈闭环」:每次只让它改一处、改完跑测试和 lint、失败把错误信息回灌;规范用 lint/模板固化,而不是只写在提示词里。