面灵AI→

字节后端一面:布隆过滤器、幂等设计、Redis 持久化与 RAG 评估

轮次
一面
时间
2026-10
来源
牛客网

《面试题目》

  1. 介绍自己、项目和承担的角色,为什么选择这些技术组件?能不能只用一种数据库?为什么不只用 Redis,选择 MySQL 的依据是什么?
  2. 注册流程中的布隆过滤器、分布式锁和唯一索引,分别解决什么问题?
  3. 布隆过滤器判断用户名存在时后续怎么处理?误判导致用户无法注册怎么办?如果更新失败、重试一直失败,后续如何处理?
  4. 有人疯狂请求注册接口,如何限流?具体按什么维度识别和拒绝请求?
  5. 如何使用 requestId 实现幂等?既然 requestId 可以防重,为什么还需要加锁和二次校验?
  6. Redis 有哪些持久化方式?AOF 的具体流程是什么?AOF 与 RDB 应该如何选择?
  7. 如何保证数据库与缓存的一致性?并发读写时出现旧数据怎么办?
  8. MongoDB 字段级 Patch 和 CAS 版本控制分别解决什么问题?
  9. 如果 Redis 数据全部丢失,如何恢复面试进度?Redis Stream 的消费者处理到一半宕机会发生什么?如果 Redis 自身宕机,如何优化系统可用性?
  10. 会话级 FIFO 能否保证顺序?多个 Worker 同时执行时如何协调?
  11. 请解释 RAG 从文档上传、处理到检索和回答的完整流程。如何评估 RAG 或重排的效果?自己出的几个问题能否代表真实用户的各种提问?
  12. 多个知识库的检索结果如何融合?不同知识库的分数一定不能直接比较吗?为什么?
  13. HashMap 为什么线程不安全?线程安全的替代方案如何实现?
  14. MySQL 为什么需要两阶段提交?redo log 和 binlog 如何协调,故障后如何恢复?
  15. 事务有哪些特性?死锁有哪些必要条件?事务有哪些隔离级别?读未提交可能出现哪些读异常?脏读、不可重复读和幻读分别是什么情况?
  16. Redis 的 key 过期之后,内存会立即释放吗?
  17. AI 调用很慢,应该如何设计线程池?
  18. 同一个类中的普通方法调用另一个带事务注解的方法,事务一定会生效吗?
  19. 流式输出的断线补发如何实现?
  20. 每段生成内容都写数据库,请求量增大后如何处理?如果改用 Redis,又如何应对其宕机风险?
  21. 前端断线或者用户主动停止时,整个后台任务是否应该取消?已经调用的工具能否停止?
  22. Java GC 有哪些基本算法?它们分别如何工作?

《参考解析》

布隆过滤器、分布式锁与唯一索引的分工:三者不是替代关系而是成本与正确性的梯度。布隆过滤器把「大概率不存在」的查询挡在数据库外,省掉大部分 IO,但它有假阳性(说不存在一定不存在,说存在可能不存在),所以只能做前置过滤;唯一索引是最终的正确性保证,在数据库层保证不重;分布式锁是控制并发窗口,避免大量请求同时打到数据库。误判导致无法注册的处理:布隆说存在但库里没有时,最终以数据库为准放行(先查库,查不到就插入,唯一索引兜底);不能直接拒绝用户。布隆更新失败要有重建机制(全量扫描重建或双写队列补偿),重建时先用旧过滤器继续服务,避免空窗。

限流的维度:至少三层——IP/设备(挡批量脚本)、账号/手机号(挡同一账号刷)、全局出口(保护后端)。还要加行为特征(同 IP 短时间大量不同手机号、验证码请求频率、滑块验证)。拒绝策略要区分:超限返回明确的错误码与重试时间,而不是静默失败;对疑似机器人的流量走验证码而不是直接封禁,降低误伤。

requestId 幂等与二次校验:requestId 只解决「同一个请求重复提交」,做法是服务端以 requestId 为唯一键做去重(Redis setnx 或唯一索引)。它挡不住的是:不同 requestId 但业务上等价的请求(用户刷新页面重新生成 id)、以及并发窗口内的竞态(两个请求同时通过去重检查)。所以还需要分布式锁把并发串行化、以及数据库唯一索引做最后一道闸;二次校验(查一次库里是否已存在)是在锁内做的,用来处理「前一个请求刚提交成功但缓存还没更新」的窗口。

Redis 持久化:RDB 是某一时刻的全量快照(fork 子进程写,恢复快、可能丢最后一次快照后的数据);AOF 记录写命令(appendfsync 有 always/everysec/no 三档,everysec 最多丢 1 秒),文件会膨胀,靠 AOF 重写(BGREWRITEAOF,用当前数据生成最小命令集)压缩。选择上:能容忍丢几秒数据、要快恢复用 RDB;要求接近不丢用 AOF everysec;生产常态是两者都开(RDB 做备份与快速恢复,AOF 做低丢失),Redis 4.0 之后还有混合持久化(AOF 文件前半是 RDB 格式)。

缓存与数据库一致性:常规做法是「先更新数据库,再删除缓存」(Cache Aside),比先删缓存再写库的窗口更小。仍然存在的并发问题是「读请求把旧值回填」——解法是延迟双删、给缓存加版本号(写入时带版本,读到更旧版本就丢弃)、或用 binlog 订阅(Canal)异步失效。要讲清接受最终一致还是要求强一致:绝大多数业务是前者,强一致就别用缓存或在读路径上做校验。

Stream 消费与宕机:Redis Stream 用消费者组(consumer group)+ pending entries list(PEL)实现「至少一次」。消费者读完但未 ACK 就宕机,消息留在 PEL 里,可以通过 XPENDING/XAUTOCLAIM 把超时未确认的消息转给其他消费者重新处理——所以消费侧必须幂等。Redis 自身宕机则靠主从 + 哨兵/Cluster 做故障转移,配合 AOF 减少数据丢失;但故障转移期间会短暂不可用,业务侧要有降级路径(比如直接读数据库或返回稍后重试)。

RAG 全流程与评估:见前文(解析切块 → 向量化 → 混合召回 → 融合 → 重排 → 上下文拼装 → 生成 → 引用校验)。评估分检索(Recall@K、MRR、nDCG)和生成(忠实度、相关性、引用正确率)。「自己出的几个问题能否代表真实用户」——不能,自出题等于开卷,必须从真实日志采样并覆盖长尾表达。

多知识库分数融合:不同库的分数不能直接比较,因为向量相似度的分布依赖各自的语料与模型(同一个分数在不同库里含义不同)。正确做法是先做归一化或转为排名(RRF,倒数排名融合,只看名次不看绝对分数),或者每个库各自取 TopK 后再统一重排(用一个能同时看 query 与候选的 cross-encoder,它给出的分数是在同一空间里的)。另一种方式是学习一个融合权重,但那需要标注数据。

HashMap 线程不安全与替代:不安全体现在并发扩容时链表/红黑树结构被破坏(JDK8 里可能丢数据或死循环的历史问题在 8 之后缓解但仍有竞态)、size 计数不准、put 时的覆盖丢失。替代:ConcurrentHashMap(JDK8 用 CAS + synchronized 锁单个桶,粒度细)、Collections.synchronizedMap(整表锁,性能差)、或用分段/分片自己实现。

MySQL 两阶段提交:目的是让 redo log(存储引擎层,保证崩溃恢复)和 binlog(server 层,用于复制与备份)保持一致。流程是:写 redo log 并置为 prepare → 写 binlog → 提交 redo log 置为 commit。崩溃恢复时:如果 redo 是 prepare 但 binlog 已完整,就提交;如果 binlog 不完整,就回滚。判断 binlog 是否完整靠其中的 XID 与末尾的 commit 标记。

事务与隔离级别:ACID 四特性;死锁四个必要条件(互斥、占有且等待、不可剥夺、循环等待);隔离级别四级——读未提交(可能脏读)、读已提交(避免脏读,可能不可重复读)、可重复读(避免不可重复读,MySQL 默认,靠 MVCC + 间隙锁在很大程度上避免幻读)、串行化。脏读是读到别的事务未提交的数据;不可重复读是同一事务内两次读同一行结果不同;幻读是同一范围两次查询行数不同。

key 过期后内存是否立即释放:不会。Redis 用「惰性删除 + 定期删除」:访问到过期 key 时才删,后台定期抽样删除;所以在被删之前内存仍占用。这也是为什么大量 key 同时过期会造成内存尖峰和延迟抖动,过期时间要加随机抖动。

AI 调用慢的线程池设计:核心是隔离——给 AI 调用单独的线程池,不要和普通请求共用,避免慢调用把整个应用的线程耗尽。参数上:核心线程数不必大(瓶颈在下游),队列要有界(无界队列会把内存撑爆且延迟无限增长),拒绝策略要明确(快速失败并返回友好提示,或降级到备用模型)。还要配合超时(连接超时 + 读超时 + 整体超时)、并发上限(信号量限制同时在途的调用数)、以及熔断(连续失败就快速失败一段时间)。

同类内方法调用事务不生效:因为 Spring 的事务是基于代理的 AOP,this.method() 不经过代理,注解不生效。解法:把方法拆到另一个 bean、用 AopContext.currentProxy()(需开 exposeProxy)、或注入自身代理。更根本的建议是别依赖「同类调用带事务」这种隐式行为。

流式输出的断线补发:服务端把生成内容按序号分段落库(或写 Redis),客户端重连时带 lastSeq,服务端从该序号之后继续推;如果生成已经结束,就直接返回完整结果。关键是序号单调与幂等:客户端按序号去重,服务端不重复推同一段。要处理「重连时正在生成的段落」——把未完成的段落标记为未定稿,重连后重发该段。

前端断线或用户停止时是否取消后台任务:要分场景。纯查询类任务应该取消(省成本);有副作用的工具调用要谨慎——已经发出的请求无法撤回,能做的只是不再发起后续调用并记录已完成的部分。实现上用 context 的取消信号逐层传递,工具层检查信号决定是否继续。

GC 基本算法:标记-清除(标记存活对象后回收未标记的,简单但产生碎片)、复制(把存活对象复制到另一半空间,无碎片但牺牲一半空间,适合存活率低的新生代)、标记-整理(标记后把存活对象向一端移动,无碎片但移动成本高,适合老年代)、分代收集(按对象年龄分区,新生代用复制、老年代用标记-清除/整理)。现代收集器(G1、ZGC)在此基础上做并发标记与分区回收来压低停顿。