小红书后端开发岗面经合集(四):AQS 与锁升级、线程池与高并发活动设计
- 轮次
- 多轮面试合集
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
面经 01 · 小红书后端开发岗(2025-09-25)
- 有挑战的事情、如何做的、做了什么、具体实现与业务细节
- 现有 a、b、c 三个线程,需实现「a 执行完执行 b,b 执行完执行 c,c 执行完回头执行 a」的按需循环效果,该如何实现?
- 请说明 synchronized 锁升级的过程
- 轻量级锁基于 CAS 自旋实现,其默认自旋次数是多少?从轻量级锁升级到重量级锁的临界条件是什么?
- 轻量级锁(CAS)存在 ABA 问题,该如何解决?
- JUC 包中乐观锁实现(如 ReentrantLock、CountDownLatch 等)底层依赖的抽象类 AQS,其底层实现逻辑是什么?
- AQS 中的公平锁和非公平锁是如何实现的?
- 请说明线程池的核心参数,以及这些核心参数之间的关系(任务提交时的判断逻辑)
- 若线上有一个 MySQL 需要优化,请说明你的优化思路
- 使用 explain 分析 SQL 时,核心关注哪些指标?这些指标如何帮助优化?
- 若 explain 分析无问题、已加索引,但 MySQL 整体速度仍很慢,还有哪些优化思路和手段?
- InnoDB 存储引擎底层用什么数据结构存储数据和索引?
- B+ 树有什么特点?
- 一个三层的 B+ 树大概能存储多少数据?其评估思路是什么?
- 若 MySQL 页大小为 16K,以主键索引为例,一页大概能存储多少数据?
- MySQL 的 InnoDB 存储引擎支持事务,其是如何实现事务特性(ACID)的?
- 你在实习过程中用过 Redis 吗?用了 Redis 的哪些数据类型?解决了什么业务问题?
- Redis 的 string 和 list 类型,底层用的数据结构是什么?
- string 类型底层基于「简单动态字符串(SDS)」实现,这种数据结构的好处是什么?
- 若业务数据需存储在 Redis 分片集群(单个节点存不下,需多节点联合存储),Redis 是如何实现数据写入和读取的?其底层是否用哈希实现?
- 现有一个活动:奖品数量少(仅几百个)、参与人数多(几十万)、瞬间 QPS 高(数十万),需对该系统进行设计,具体该怎么做?设计过程中核心需要关注哪些点?
面经 02 · 小红书后端开发岗(2025-09-19)
- 了解个人情况,介绍自己业务
- struct 内存对齐
- 上来要求手写一个线程池
- 算法一:给你一个二维矩阵,上面写满了数字 0 和 1,要求你找出一个连通块,使得这个连通块内所有数字都是 0 且边界全部是 1(被 1 包围)
- 算法二:你是一个售票机,一开始售票机内有 0 元,有 m+n 个人来买票,每张票 50 元,其中 m 个人手上有 50 块钱,n 个人手上有 100 块钱,每个人来买票顺序不确定,求找不开钱情况的概率
- boost 库用过吗?std::future
- SIMD 等 C++ 的八股和技术栈
面经 03 · 小红书后端开发岗(2025-09-18)
- 你能简单解释一下什么是 Java 的内存模型(JMM)吗?它是如何保证多线程下的可见性和有序性的?
- 在项目中使用 SpringBoot 时,如何实现一个简单的自定义注解,并让它在某个切面中生效?
- 假设有一个包含百万级数据的表,你该如何通过 SQL 查询快速找到某个字段的最大值?
- 你了解 Java 中的 ClassLoader 吗?它是如何加载类的?你是如何避免类加载冲突的?
- 在分布式架构中,Nacos 作为配置中心的作用是什么?它如何帮助管理服务的配置?
- Redis 中的缓存穿透、缓存雪崩、缓存击穿分别是什么?你是如何在项目中处理这些问题的?
- 你如何在 Spring 项目中实现一个简易的异步任务执行?能举例说明一下如何用 @Async 注解实现吗?
面经 04 · 小红书后端开发岗(2025-09-14)
- 一张表假设有多个字段 a、b、c,建立了 abc 联合索引,where a = ? and b = ? order by c 能命中索引吗?
- 索引的底层原理
- 单例模式怎么实现?为什么要双重检测?如果使用反射机制可以破坏单例吗?
- MySQL 事务隔离级别介绍,你平时用的哪一个?
- Redis 你在什么场景使用?旁路缓存会存在脏数据吗?
- Redis 获取缓存耗时增加了,原因可能是什么?
- OOM 问题怎么处理?Java OOM 问题处理,原因可能是什么,怎么处理?
- 优惠券秒杀(方案描述),这里最核心的问题是什么?你是怎么避免超卖的?
- 线程池有哪些核心参数?为什么线程池要先使用核心线程,然后队列排队、队列满才创建非核心线程,为什么要这么设计?
面经 05 · 小红书后端开发岗(2025-09-12)
- 实习:缓存怎么设计的?库存扣减的逻辑具体是怎么样的?用 lua 脚本怎么实现的?lua 脚本怎么保证原子性的?并发情况下的状态变更会出现问题吗?
- 消息队列可靠性怎么实现的?
- 介绍一下 redis 的集群模式(主从加哨兵),集群模式下的 hash 槽是怎么分配的?扩展之后呢?
- 说一下哈希环相关的知识?
- 说一下 @Service 注解的具体原理?
- spring 事务的实现方式?失效场景?如果是标注在类的静态方法上呢?
- 缓存击穿、穿透、雪崩?对应的解决方案?
- 场景:有一个本地缓存加 Redis 加 MySQL 的三层架构,你怎么设计数据更新时的逻辑?需要注意的点有哪些?有考虑过使用本地缓存自己的更新机制吗?多实例下的本地缓存,怎么保证每个实例都能触发更新?
- 利用消息队列的广播机制,怎么让多个消费者能消费到同一条消息?详细说说 kafka 广播模式的底层原理
- 本地缓存 caffeine 的缓存更新机制和过期策略了解吗?
- 判断一个链表是否成环
面经 06 · 小红书后端开发岗(2025-09-12)
- 聊实习
- 了解 netty 的内存池算法吗?netty 和 nio 的关系
- 线程池的原理?如果不用线程池还可以怎么做?异步为什么好?
- jvm 和 os 的线程有什么关系?linux 中线程和进程的异同?
- 零拷贝的流程?有哪些系统调用?
- 场景题:分库分表
- mysql 分哪几层,各自功能是什么?mysql 的 ACID 各自由什么来保证?
- 算法:topk
- 算法:两个有序数组的中位数查找,要求 log 时间复杂度
- aqs 是什么?如果要实现一个 aqs,你觉得核心是什么?
- 有锁和无锁的区别是什么?乐观锁和悲观锁区别是什么?java 中有哪些锁?
- synchronized 有什么缺点?为什么内核态和用户态切换耗时长?你了解虚拟线程吗?
- kafka 如何保证消息不丢失?kafka 不就是暂时存储消息的吗,为什么它的消息积压也是一个问题?
- page cache 是什么?有什么作用?
- 大三的课程怎么办?最快什么时候到岗?家是哪里的?
面经 07 · 小红书后端开发岗(2025-09-01)
- 挑一个项目来介绍一下
- Java 的 GC 过程会有 Stop the World,谈谈为什么要有 STW 的机制?
- ZGC 垃圾回收器
- G1 已经很不错了,为什么还要有 ZGC 这样的垃圾回收器,为了解决什么问题?
- 比如一个订机票的场景,涉及多个外部系统,首先要去看有没有票,然后支付要调支付宝或者微信去付款,订完票可能过了半个小时才告诉我订票有没有成功,对于这种场景下的分布式事务,你认为怎么去处理和设计来保证一致性比较好?
- 基于消息传递的方案,消息可能传递失败,如何解决?
- 如果用消息队列,这种场景怎么做技术选型?
- 做题:新兵报到,指导员命令所有人按身高大小从低到高依次站好,每次从头这边开始调整,但是要求每次只能进行一次交换
《参考解析》
面经 01 / 04 / 06 · 并发与锁
-
锁升级、自旋与 ABA:升级路径是无锁 → 偏向锁 → 轻量级锁 → 重量级锁。偏向锁把线程 ID 写进对象头,同一线程再进入无需 CAS;出现第二个线程竞争就撤销偏向、升级为轻量级锁,竞争线程在栈上建立 Lock Record 并 CAS 抢 Mark Word,失败进入自旋;自旋到阈值(JDK 6 之后是自适应自旋,次数由前一次在同一锁上的成功率动态决定,不再是固定的 10 次)仍拿不到锁就膨胀为重量级锁,靠 monitor 与操作系统互斥量挂起。临界条件要答成「自适应自旋失败、或竞争线程数超过 CPU 核数的一半、或锁持有者长时间不释放」这类判据,而不是背一个数字。轻量级锁的 ABA 问题的落点是 CAS 只比较值不比较历史:解决手段是加版本号(AtomicStampedReference)或时间戳,也可以换成 AtomicMarkableReference 只关心「有没有被改过」。面试官更想听的是「ABA 在什么场景才会造成业务损失」——只有在「值变回原样但中间状态有意义」时才有害,比如栈的 pop 场景,纯粹计数场景通常无害。
-
AQS 与公平性:AQS 的骨架是一个 volatile 的 state 加一个 CLH 变体的双向等待队列。以 ReentrantLock 为例,state 为 0 表示空闲、大于 0 表示被占用的重入次数;抢锁是 CAS 把 state 从 0 改成 1 并记录持有线程,失败就包装成 Node 入队,然后通过 LockSupport.park 挂起;释放时 state 减到 0,唤醒队列里的后继节点。公平与非公平的唯一差别在「入队前是否先检查队列为空」:非公平锁直接抢,抢不到再排队,所以可能出现后来的线程插队成功,吞吐更高但理论上会饿死先来者;公平锁每次先看有没有前驱,有就乖乖排队,代价是更多的上下文切换。「队列里要存什么才能自己实现一把锁」的答案是线程引用(要唤醒谁)、等待状态(区分正常等待、取消、条件等待)、前驱后继指针(维持顺序并支持取消节点时把链补上),以及一个能被唤醒者识别的标记,否则一个节点取消就会让后面的线程永远收不到通知。
-
三个线程按序循环的实现与线程池设计取舍:a→b→c→a 这类按需循环,标准解法是一个共享的「当前轮到谁」状态加条件变量:用 ReentrantLock 配三个 Condition,每个线程在轮到自己之前 await、执行完改状态并 signal 下一个;也可以用 Semaphore 用一个许可令牌在线程间传递,或用 CountDownLatch 做一次性顺序(循环场景不适合)。要注意虚假唤醒(await 必须放在 while 循环里判断条件)、异常路径下也要把令牌交出去否则整条链卡死、以及别用忙等+sleep 这种既费 CPU 又有延迟的做法。线程池为什么「先核心线程、再入队、队列满才扩到最大」:线程创建与销毁的成本远高于任务在队列里等待,先复用已有线程能控制并发度、避免频繁切换;如果先扩到最大再排队,CPU 会在高并发下被线程切换拖垮,而队列失去了削峰作用。反过来说,如果任务延迟敏感、不允许排队,就该用有界小队列 + 较大的 max,或者直接用 SynchronousQueue 把任务立刻交给线程或触发拒绝。手写线程池要覆盖的点:Worker 继承 AQS 或直接用锁、runWorker 的取任务循环、allowCoreThreadTimeOut、拒绝策略、以及 shutdown 时中断空闲线程并等已提交任务跑完。
面经 01 / 03 / 04 · MySQL
- 执行计划、索引与 B+ 树容量:EXPLAIN 要重点看 type(ALL 全表最差、index 全索引扫描、range/ref/eq_ref/const 依次变好)、key(实际用的索引,NULL 说明没走索引)、rows(预估扫描行数,用于判断索引选择是否合理)、filtered、Extra(Using filesort、Using temporary 是排序与临时表开销,Using index 表示覆盖索引)。explain 看起来没问题但仍然很慢,要从别处找原因:数据量本身很大(需要归档或分表)、并发高导致锁等待或磁盘 IO 打满、单次返回结果集过大(网络传输与序列化成为瓶颈)、SQL 写法让排序或聚合在临时表里做、索引统计信息过期导致优化器选错执行计划(analyze table)、以及缓冲池命中率低。三层的 B+ 树能存多少数据是可以现场推的:假设页 16K、主键 bigint 8 字节加 6 字节页指针,非叶子节点一个索引项约 14 字节,一页能放约 1170 个,两层就到了约 137 万条指向叶子;叶子节点每行假设 1KB,一页放约 16 行,所以三层大约能放两千万行量级——答题时要给出推导过程而不是背一个数,并说明这个估算对行宽和主键长度非常敏感。InnoDB 靠 redo log(崩溃恢复与持久性)、undo log(回滚与 MVCC 版本链)、锁与 MVCC(隔离性)以及约束(一致性)共同实现 ACID,另外要提到两阶段提交保证 redo 与 binlog 的逻辑一致。
面经 01 / 05 · Redis 与缓存架构
-
Redis 的数据结构、分片与缓存一致性:string 底层是 SDS,好处是记录了长度所以取长度 O(1)(C 字符串要遍历)、二进制安全(能存任意字节)、预分配与惰性释放减少内存重分配次数,且不会因为
\0截断。list 在元素少时用紧凑列表(ziplist / listpack),大了转 quicklist(链表 + 紧凑列表的组合),兼顾内存与操作效率。分片集群(Cluster)把整个键空间划分为 16384 个哈希槽,键通过CRC16(key) mod 16384决定归属槽,客户端拿到 MOVED 重定向或本地缓存槽映射表;扩容时新增节点会迁移部分槽,迁移中是「ASK 重定向 + 逐个 key 搬运」,此时可能有短暂的访问异常,所以扩容要挑低峰并配合客户端重试。这就是「底层是否用哈希实现」的落点:是哈希分片,但不是把 key 全量哈希到节点,而是哈希到槽、槽再分配给节点。缓存一致性:旁路缓存(Cache-Aside)在并发下确实会脏——先更新库再删缓存仍有窗口(删除失败或读请求在删除后回填旧值),把缓存当准会有脏读;工程做法是先更新数据库再删除缓存、加延迟双删或订阅 binlog 失效、给缓存设 TTL 兜底、关键业务用「读写都加锁」或索性不用缓存保正确性。Redis 取缓存变慢的常见原因是:慢命令(大 key 的 hgetall、keys、大 zset 范围查询)阻塞单线程、网络往返增加(跨机房、连接数暴涨)、内存不足触发了淘汰或 swap、持久化 fork 导致抖动、以及热 key 打满单个分片——排查先看 slowlog 与延迟监控。 -
数十万 QPS 的抽奖活动设计:这类题按「入口、库存、去重、异步化、兜底」五段答。入口先挡:客户端限流、验证码或答题挡脚本、网关层按用户与 IP 限流,把无效流量在第一层削掉。库存必须提前预热到 Redis,用 Lua 把「校验库存 + 扣减 + 记录已参与用户」合成一次原子操作,防止超发与重复中奖;奖品只有几百个而参与几十万,最坏情况是绝大多数请求都是来抢那几百份,所以要么用「先到先得 + 库存为 0 立即快速失败」,要么改成「抽奖资格 + 异步开奖」把瞬时写压力拆掉。去重按用户维度做(活动 ID + 用户 ID 的唯一约束,Redis 用 set 或 bitmap 判断是否已参与),并在数据库层用唯一索引做最终保证。异步化是把中奖记录、发券、通知这些非关键路径放到 MQ 里慢慢消费,主链路只做「判断 + 扣减」,失败靠重试与对账补偿。兜底要有:库存回补(下单超时或发奖失败时把库存还回去,且回补必须幂等)、对账任务扫「库存为负」「同一用户多次中奖」、以及降级预案(活动太热时关闭部分入口或改为预约制)。答题时主动提「Redis 单点与主从切换会丢扣减记录」这一风险,并给出「数据库条件更新兜底 + 对账修复」的对策,才算完整。
面经 05 / 06 / 07 · 系统设计与分布式
-
三层缓存的更新与多实例失效:本地缓存 + Redis + MySQL 的架构下,难点是本地缓存不受控——多实例各自的本地缓存在数据变更时都要失效。可行方案按代价排序:一是给本地缓存设很短的 TTL(秒级)并以 Redis 为准,接受秒级不一致;二是用消息队列广播失效消息,每个实例订阅并清掉自己的本地缓存,注意消息要带版本号或时间戳,避免乱序导致用旧值覆盖新值;三是用配置中心或 Redis 的发布订阅做通知通道,实现更轻但要处理断连重连期间的丢失(重连后强制清一次本地缓存);四是把版本号放在共享存储(Redis 里维护每个 key 的版本),本地缓存命中后异步校验版本,过期则回源。整体顺序是「先写库 → 删 Redis → 广播失效本地」,任何一步失败都要靠 TTL 与下一次读回源收敛;本地缓存只适合放「允许短时间不一致、读远多于写」的数据,强一致数据不要放本地。Kafka 的广播是另一回事:同一消费者组内每个分区只被一个消费者消费,要让多个消费者都拿到同一条消息,就要让它们属于不同的消费者组(各自独立的 group.id),或者用不同的 topic/分区策略;底层原理是分区是并行与顺序的基本单位、位点(offset)按「消费者组 + 分区」维护,所以广播的本质是「多组各自维护一份位点」。
-
零拷贝与订票场景的分布式事务:零拷贝的核心是让数据不经过用户态缓冲区反复拷贝,常见实现是 sendfile(文件 → socket 直接在内核里流转)、mmap + write、splice,Kafka 与 Netty 都用这套把磁盘到网络的数据路径缩短;系统调用层面记 sendfile、splice、mmap、sendmsg 即可,答题时补一句「零拷贝减少的是 CPU 拷贝与上下文切换,不是完全没有拷贝(DMA 拷贝仍在)」。订票这类「先查票、再支付、半小时后才出结果」的场景,本质是长事务加多个外部依赖,不能用数据库事务硬扛。推荐的组合是:本地消息表或事务消息保证「订单状态变更」与「待执行动作」原子落地,然后用状态机驱动后续步骤(待支付 → 支付中 → 出票中 → 已出票 / 已退款),每一步都幂等可重试,超时未回调就走主动查询接口对账,最终由定时对账修掉不一致;支付与出票这种依赖第三方的步骤要设最大重试与人工兜底队列。消息可能传递失败,所以投递侧要有确认与重发(至少一次),消费侧靠唯一键幂等;选择 MQ 时按「是否需要事务消息、延迟消息、严格顺序、堆积能力」来定,这类业务一般需要事务消息与延迟消息,RocketMQ 更贴合,Kafka 更适合日志与流处理。