小红书后端开发岗面经合集(三):JVM 调优、Spring 循环依赖与 Redis 大 Key 治理
- 轮次
- 多轮面试合集
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
面经 01 · 小红书后端开发岗(2025-12-25)
- 你实习中实现的 RPC 接口的限流过程是怎样的?怎么在问题出现前避免?
- 消息队列信息一致性怎么保证?
- 消息队列都有什么,各自都有什么区别?
- 实习中 MySQL 涉及到的锁?说说 next-key lock?SQL 语句的 left join、right join、inner join 区别?
- 讲讲消息队列加 zookeeper 的实现过程?zookeeper 注册过程?zk 的树状节点?Redis 优点?Redis 分布式锁、分布式 ID?
- 说说死锁?如何避免死锁?
- Redis 有几种数据结构?每种数据结构能有什么应用?底层实现?
- MySQL 分库分表?
- Redis 缓存雪崩、击穿、穿透?
- 在你的实习或项目中有什么有挑战的工作?相关工程代码有什么印象深刻的实现方法?
- 为什么选择从原公司转到小红书实习?
- 做两道题:左连接 SQL 语句 + 最长有效括号
面经 02 · 小红书后端开发岗(2025-12-22)
- 手撕:字符串相乘
- Redis 常用命令
- MySQL MVCC
- Spring AOP
- JVM 垃圾回收机制
- Redis 实现分布式锁
- synchronized 锁升级机制
面经 03 · 小红书后端开发岗(2025-12-12)
- 线程池的核心参数
- synchronized 和 volatile 的区别
- 手写单例模式,要求双重检查锁
- 索引是什么结构,为什么用 B+ 树
- 手写 SQL:查出所有学生的学号、姓名,还有总分,然后按总分从高到低排序
- 小 B 希望你能够帮到她,你是否愿意?
面经 04 · 小红书后端开发岗(2025-12-10)
- 介绍一下你最熟悉的后端项目,你在其中负责了什么?遇到了什么技术挑战,怎么解决的?
- 为了保证审核系统的「高可用」,你会从哪些方面设计和考虑?
- 如果线上一个审核服务 CPU 占用率突然达到 100%,你的排查思路是什么?
- 在审核系统中,如何用 Redis 缓存审核规则?如果规则有更新,如何保证缓存和数据库的一致性?
- 谈谈你对 Kafka、Flink 在内容安全中应用的理解
- 什么是数据库事务?在审核系统中审核通过后需要更新内容状态并记录审核日志,如何保证这两个操作的事务性?
- 假设让你设计一个接口,防止恶意用户高频提交内容来绕过审核,你会怎么做?
- 为什么使用微服务架构?它带来了什么挑战?在审核系统中,服务间调用超时了怎么办?
- 你如何理解这个岗位中提到的「数据 sense」?能举例说明吗?
- 写一个 SQL 查询:找出最近 7 天内被审核员驳回次数最多的前 10 个用户
面经 05 · 小红书后端开发岗(2025-12-08)
- ThreadLocal 在小红书的用户会话管理应用中如何避免内存泄漏?结合弱引用和线程池场景分析
- 手写无锁化点赞计数器,对比 AtomicLong、LongAdder、Redis INCR 的性能差异
- ConcurrentHashMap 扩容原理:多线程环境下如何保证迁移线程安全?画图说明 transfer() 逻辑
- 小红书评论列表实现:如何用 CopyOnWriteArrayList 保证读写并发安全?分析其适用性和缺陷
- 虚拟线程(Loom)实战:如何改造小红书 IO 密集型服务?对比线程池方案
- JVM 内存泄漏排查:小红书 App 后台频繁 Full GC,如何用 MAT 定位 byte 泄漏?
- G1 调优实战:给定一个高并发的推荐服务 JVM 参数,MaxGCPauseMillis、InitiatingHeapOccupancyPercent 如何设置?
- StringTable 性能问题
- 小红书动态生成大量短链接导致 YGC 变慢,如何优化?
- Native 内存泄漏:DirectByteBuffer 未释放导致容器 OOM,如何用 NMT 加 pmap 追踪?
- JIT 编译优化
- 小红书某个热点方法被频繁调用,如何通过 -XX:CompileThreshold 调整触发阈值?
- Spring 循环依赖解决:小红书商品服务与库存服务相互注入,三级缓存如何破环?画图说明 Bean 创建过程
- 动态数据源切换:小红书多租户架构中,如何基于 AbstractRoutingDataSource 实现分库路由?
- Spring 事务失效场景:小红书优惠券服务中 @Transactional 为何不回滚?
- Redis 大 Key 治理:小红书热帖评论列表超过 10MB,如何拆分?
面经 06 · 小红书后端开发岗(2025-12-06)
- 你写的两个线程交替打印 1-100 的代码,先打印一下结果我看一眼
- 你为什么会这么对 Java 的锁进行分类?有这样分类的理由吗?
- 你理解的悲观锁就是 synchronized 加 monitor,乐观锁就是 CAS 相关的东西,是这样吗?
- 你刚刚说的 synchronized 的底层流程,能再具体说一下吗?
- 你刚刚说 synchronized 性能比较差,是这样吗?它的性能一定比较差吗?
- 我们聊一下 Lock 的底层数据结构是怎样的
- Lock 怎么实现公平与非公平锁呢?
- 你说 Lock 的底层结构是「state 变量加队列」,它是怎么通过这个结构实现锁的?
- 既然你不清楚节点存储的内容,那如果让你自己实现一把并发锁,队列里需要保存什么信息才能实现锁的功能?多线程下怎么通过这个队列控制线程竞争锁?
- 你了解 Java 的集合吗?从底层介绍一下 Java 集合
- 你说集合分为 Collection 和 Map 两大类,那 Collection 的结构、分层是怎样的?Collection 里除了 List,还有什么其他类型?
- 你了解 Map 吗?
- HashMap 中当链表长度达到阈值后会转红黑树,为什么一定要用红黑树?为什么不用其他二叉树?
- HashMap 是线程安全的吗?多线程环境下会有什么问题?
- 你提到多线程下 HashMap 的操作无法保证原子性,能具体说一下吗?比如 HashMap 扩容时多线程下会出现什么问题?
- 你有想过 HashMap 扩容时多线程下出现问题的具体场景吗?两个线程分别处于什么状态、在做什么操作,才会导致问题?
- 你说的「分段锁」(JDK 8 之前)和「局部锁」(JDK 8 之后)具体指的是什么?在 HashMap 的数据结构里,这个代码块锁的是什么对象或资源?
- 你觉得实现一个高性能的 HashMap,最重要的是什么?
- 你了解 HashMap 的哈希函数吗?它是怎样的?怎么保证哈希值分布均匀?
- 我们聊一下 Redis,Redis 支持的数据结构有哪些?
- 你了解过 Redis 中 HashMap 的扩容机制吗?
- Redis 的部署模式你了解吗?哨兵模式怎么部署?哨兵节点是单个还是多个?作用是什么?
- 你觉得 Redis 哨兵这种分布式部署模式会有什么问题吗?
- Redis 主从节点的数据同步是怎样的?每个节点的数据如何保持一致?
- 那有什么办法能做到 Redis 数据不丢失吗?
- 你用 Redis 实现过分布式锁吗?是怎么保证原子性的?设置值和设置有效期这两步怎么保证原子性?
- 在 Redis 集群模式下,用 Redis 实现分布式锁会有什么问题?
- 你用 Redis 读数据时,读的是主节点还是从节点?如果读从节点,可能会出现什么问题?
面经 07 · 小红书后端开发岗(2025-11-21)
- 介绍一个你熟悉的项目
- 当时为什么做这个项目?上线了吗?
- 你认为什么是 RAG?它跟微调有什么区别?效果上会有什么区别?为什么有两种方式?
- 有实际做过微调吗?
- RAG 去做判卷,你的检索内容是什么?
- 用的是什么向量数据库?为什么最后选了它?
- Redis 的缓存策略:为什么要去设计一个热点题目缓存这样的东西?
- RocketMQ 在正常的发送和消费时,怎么保证消息不丢失?
- 消息发送写到 broker 的时候,在你的发送里面要做什么样的设置才可以保证一定会写入?
- 你在做哪些开源的事情?
- 你对哪一个技术中间件是最熟悉的?
- Redis 为什么很快?为什么单线程还会比多线程快,感觉这有点反直觉?多线程比单线程执行会多一些成本吗?
- 上下文切换有哪些具体的开销?
- 什么是程序计数器?它是跟线程绑定的吗?
- Redis 多路复用
- 你觉得你对 Redis 的掌握程度是怎么样的?评价一下
- 你现在学习是通过什么方式去学习新知识的?有什么感兴趣的方向吗?
- 假设你跟你的 mentor 在一件事情上意见完全相反,你会如何处理?
- 算法题:不含重复字符的最长子串的长度
- 你实际做项目里面,碰到过最难的问题是什么?如何解决?
面经 08 · 小红书后端开发岗(2025-11-19)
- 接口和抽象类的区别
- 说一下线程池的核心参数
- 动态线程池你说的是个什么概念?
- 不是说你做了一个动态线程池吗?那 K8s 或者阿里云的服务器其实都有自动扩容功能,比如根据 QPS 自动多开几个 Pod 或者自动加机器,那不就相当于线程池的线程数也变多了吗?
- 刚才你说的那个动态线程池,先说默认线程池的工作原理:我有一个任务加到线程池里边,它是怎么一个升级,核心线程数和最大线程数怎么升级的?
《参考解析》
面经 05 / 06 · Java 并发与容器
-
锁的升级、AQS 与「分类的理由」:synchronized 的锁升级路径是无锁 → 偏向锁 → 轻量级锁 → 重量级锁:偏向锁把线程 ID 记在对象头 Mark Word 里,同一个线程再进来不用 CAS;出现竞争时升级为轻量级锁,线程在栈上建 Lock Record 并 CAS 抢 Mark Word,失败就自旋;自旋失败或竞争激烈时膨胀为重量级锁,靠 monitor(ObjectMonitor)与操作系统互斥量挂起线程。要能解释为什么「性能一定差」是错的:JDK 6 之后的优化让无竞争场景几乎零成本,差的是高竞争下陷入内核态的挂起与唤醒。ReentrantLock 这一支的底座是 AQS:一个 volatile 的 state 表示同步状态、一个 CLH 变体的双向等待队列存等待线程,抢锁就是 CAS 改 state 失败后把当前线程包成节点入队并挂起,释放时唤醒后继。公平与非公平的差别只在「入队前是否先看一眼队列有没有人排队」——非公平允许插队,吞吐更高但可能饿死;公平按队列顺序,吞吐低但不会有线程长期得不到锁。被追问「自己实现一把锁,队列里要存什么」时,答案是要存线程引用(用于唤醒)、前驱/后继指针(维护顺序与取消)、等待状态(区分取消与正常等待),并且要能在节点取消时把前驱的 next 指向后继,避免唤醒链断裂。
-
HashMap、红黑树与并发问题:为什么用红黑树——链表长度到 8 且容量到 64 就转树,把最坏查询从 O(n) 降到 O(log n),防的是哈希碰撞攻击(人为构造大量同桶 key)。为什么不选普通二叉搜索树:退化后仍是 O(n);不选 AVL:平衡更严格、插入删除的旋转更多,而 HashMap 的读写更在意插入与删除的均摊成本,红黑树用近似平衡换取更少的旋转。HashMap 的哈希函数是
h = key.hashCode() ^ (h >>> 16),把高 16 位异或到低位,因为桶下标只取低位((n-1) & hash),这样能让高位也参与运算,分布更均匀——「均匀」靠的是扰动函数加容量为 2 的幂这两件事配合。线程不安全的具体表现:并发 put 可能丢更新(两个线程同时判断桶为空,后者覆盖前者);JDK 7 的头插法在并发扩容时可能形成环形链表导致 get 死循环;JDK 8 改成尾插修掉了环,但 resize 时仍可能丢数据、size 计数不准、并发写同一个桶覆盖。所以并发场景要用 ConcurrentHashMap。它 JDK 7 是分段锁(Segment 继承 ReentrantLock,锁的是段),JDK 8 改成 CAS + synchronized 锁单个桶的头节点,锁粒度更细、并发度更高,扩容时多线程可以协同迁移(用 ForwardingNode 标记已迁移的桶并让后续线程帮忙搬)。「实现高性能 HashMap 最重要的是什么」这类开放题,答到「哈希分布均匀 + 冲突处理策略 + 扩容代价 + 内存布局友好」就够了。 -
线程池的原理与动态调整:任务提交后的判断顺序是——核心线程未满就新建核心线程;核心线程满了就尝试入队;队列满了才创建非核心线程直到 maximumPoolSize;再满就走拒绝策略。为什么先入队再扩线程:线程创建与销毁成本高,而队列能起到削峰与复用作用,直接扩线程会让 CPU 在高并发下被线程切换拖垮。用无界队列(默认的 LinkedBlockingQueue 不传容量就是近似无界)会让 maximumPoolSize 形同虚设、任务无限堆积直到 OOM,所以生产上一定要有界并配明确拒绝策略(AbortPolicy 快速失败、CallerRunsPolicy 反向施压、或自定义落库重试)。「动态线程池」的意义是在不重启的前提下按监控指标调 core/max/队列容量,并让队列里已有的任务也能被感知(有版本号或直接重建队列并迁移任务);被追问「K8s 自动扩容不是也能加线程吗」时要答出区别:扩 Pod 是横向加实例,能解决总吞吐与故障隔离,但单实例内的线程数、队列长度、拒绝策略仍然是静态的,请求在单机内排队过长时扩 Pod 并不能改善单个请求的排队延迟,且扩容有冷启动延迟;动态线程池管的是单机内的并发度适配,两者互补而非替代。
面经 05 · JVM 与线上问题排查
-
内存泄漏、GC 调优与 Native 内存:MAT 定位 byte 数组泄漏的标准流程是拿 heap dump(OOM 时自动 dump 或 jmap)、看 Dominator Tree 找占用最大的对象、用 Path to GC Roots(排除弱引用)确认是哪条引用链把它留住的——常见来源是缓存没有上限、ThreadLocal 未 remove、监听器未注销、以及大数组被静态集合持有。G1 调优要先明确目标:MaxGCPauseMillis 设的是停顿目标(不是保证),设太小会让 G1 选更小的回收集、Young GC 更频繁、吞吐下降,通常从 200ms 量级起按 SLO 调;InitiatingHeapOccupancyPercent 决定何时启动并发标记周期,设太高会等堆快满才开始、容易触发 Full GC,设太低会让并发标记跑得太勤吃 CPU,一般按堆使用率的观测曲线调而不是照搬默认值。DirectByteBuffer 的泄漏要靠 NMT(
-XX:NativeMemoryTracking=detail+ jcmd VM.native_memory)看内部与映射内存的增长,再用 pmap 对照进程实际 RSS,确认是直接内存而不是堆内存;DirectByteBuffer 由 Cleaner 在 GC 时释放,堆很小但直接内存很大时 GC 不频繁,就会出现「堆正常、容器 OOM」——解法是显式限制-XX:MaxDirectMemorySize、及时释放或用池化。StringTable 与 YGC 的关系在「大量短链接生成」这类场景里很典型:intern 或大量字符串拼接会让 StringTable 膨胀、Young GC 时要扫描并清理,表现为 YGC 时间随字符串数量增长;优化方向是别滥用 intern、用 StringBuilder 拼接、必要时调-XX:StringTableSize。 -
Spring 循环依赖与事务失效:三级缓存破环的关键是提前暴露引用:一级 singletonObjects 放成品,二级 earlySingletonObjects 放未填充属性的早期引用,三级 singletonFactories 放能产出早期引用的工厂(用于在需要代理时提前生成 AOP 代理)。A 创建时把工厂放进三级缓存,注入 B 时 B 又回头要 A,就从三级缓存拿到早期引用(并升级到二级),从而打破环。要主动说明边界:构造器注入的循环依赖破不了(对象还没实例化就需要对方),prototype 作用域的循环依赖也破不了(没有缓存),Spring Boot 2.6 之后默认禁止循环依赖,鼓励用重构或 @Lazy 解决。@Transactional 失效的典型场景要能列举:方法不是 public、同类内部直接调用(this 调用绕过代理)、类没有被 Spring 管理、异常被 catch 掉没抛出、抛出的是受检异常而默认只回滚 RuntimeException 与 Error、事务传播行为设置成 NOT_SUPPORTED 或 REQUIRES_NEW 用错、以及多数据源没配对应的事务管理器——更根本的一句话是「事务是基于代理的,没走代理就没有事务」。
面经 01 / 04 / 07 · 中间件、系统设计与 AI
-
MQ 一致性、幂等与不丢失:消息不丢要三段都保证:生产者侧用同步发送或事务消息,确认 broker 落盘成功再算发送成功;broker 侧开同步刷盘加重多副本(主从同步复制)避免主挂丢数据;消费侧处理完业务再提交位点,失败则重试并配死信队列。一致性的通用解法是「本地事务 + 消息」:把业务写入与消息记录放在同一个本地事务里(本地消息表)或直接用事务消息,再由投递侧保证至少一次投递,消费侧靠唯一键(业务 ID + 消息 ID)或状态机做幂等,最终一致靠对账补偿。RocketMQ 与 Kafka 的差异答在语义层面:Kafka 强在吞吐与流处理生态、分区顺序、但延迟消息要自己实现;RocketMQ 有事务消息、定时/延迟消息、消息回溯与更完善的死信重试,业务消息场景更顺手。消息队列选型判据是「要不要顺序、要不要事务、延迟级别、堆积能力、运维成本」。死锁部分要能说清四个必要条件(互斥、占有并等待、不可抢占、循环等待)与破坏方式:统一加锁顺序破坏循环等待、一次性申请全部资源或超时释放破坏占有等待、用可中断的 tryLock 与带超时的锁、以及把大锁拆小。排查死锁看线程栈(jstack 会直接标出 Found one Java-level deadlock)与锁等待监控。审核系统那道设计题的核心是「先定位瓶颈再谈优化」:CPU 100% 的排查顺序是看线程栈找热点方法、看 GC 频率(可能是 GC 线程吃满)、看是否有正则回溯/序列化/加密这类 CPU 密集操作、看是不是流量突增或死循环。
-
Redis 与 RAG:Redis 快的原因要答全:纯内存操作、单线程避免锁竞争与上下文切换、IO 多路复用(epoll)让一个线程管理大量连接、精简高效的数据结构与协议。为什么单线程反而比多线程快:多线程的收益在于并行计算与利用多核,但 Redis 的瓶颈通常在内存与网络 IO,多线程反而带来锁竞争、上下文切换与缓存失效的成本;Redis 6 引入的多线程也只用于网络读写,命令执行仍是单线程,正是这个道理。上下文切换的开销包括寄存器与程序计数器保存恢复、栈与地址空间切换导致 TLB 与 CPU 缓存失效、内核调度开销。程序计数器是线程私有的、记录当前线程执行的字节码行号,所以它与线程绑定。RAG 与微调的区别要说在「改不改模型参数」:RAG 把知识放在外部检索、成本低、易更新、可溯源,但受限于检索质量与上下文长度;微调改参数、更擅长风格与格式适配、知识注入的效果不如检索且更新要重训。做判卷这类任务,检索内容应该是评分标准、参考答案与历史判例,而不是整本书——这决定了切块粒度与元数据设计。向量数据库选型看维度、数据量、过滤能力(前置过滤 vs 后置过滤)、是否要混合检索与 rerank、以及运维复杂度。RocketMQ 不丢失与上面的三段一致,尤其要强调「同步发送 + 同步刷盘 + 主从同步复制」这三个开关对可靠性的影响,以及消费端幂等是终局保障。