面灵AI→

字节后端一面:RAG 召回、MySQL 慢查询与秒杀库存一致性

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

《面试题目》

  1. RAG 项目召回的准确度有多高?
  2. 物流测试的文档整体有多大?
  3. 怎么再提升召回率?
  4. MySQL 数据库如果出现慢查询,你会怎么办?
  5. 讲一个你比较了解的设计模式在 Spring 里的应用。
  6. 你掌握几门语言?
  7. 现在让你学习一门新语言,你会有什么路径去学习?
  8. 「高并发内容互动营销平台」这个项目里面,缓存具体是做什么功能的?
  9. 数据先存到缓存中,然后怎么同步到数据库?
  10. 实际的扣减,可能是先从数据库里面加,然后再往 Redis 同步。如果这两个操作同时发生,会出现不一致的场景吗?
  11. 服务本身可能会承受很多很多流量,比如秒杀一个商品它有 10 个库存,现在来了 1 万 QPS,你怎么保证服务的稳定性?
  12. 手撕:最长有效连续括号子串。

《参考解析》

RAG 召回:先把「准确度」定义清楚,再说怎么提升。 面试官问「召回准确度有多高」,最容易被扣分的是直接甩一个百分比。正确答法是先说明指标口径:召回环节看 recall@k(标准答案有没有落在 Top-K 里,K 取 5/10/20 得到的数完全不同)、MRR 或 NDCG(正确答案排得靠不靠前),端到端再看答案的忠实度与人工通过率。口径定完再给数,并且说清评测集是怎么来的——人工标注一批真实 query 与对应正确片段,规模多少条,占比多少。能做到这一步,后面「怎么再提升」就有了抓手。

提升召回的手段按收益从高到低排:① 切块策略,按标题层级/语义边界切,加 10%~20% 重叠避免答案被切断;② 混合检索,向量负责语义泛化、BM25 负责专有名词与编号精确匹配,两路结果融合;③ query 侧改写,把口语化提问扩写成多个子问题或补全缺失上下文(多轮会话里尤其关键);④ rerank,用交叉编码器对 Top-50 重排,这是性价比最高的一步;⑤ 元数据过滤(时间、部门、文档类型),先缩小候选集再检索;⑥ embedding 模型更换或领域微调。文档规模这一问通常不是随便问的——几万页的文档集必须上 ANN 索引(HNSW 或 IVF-PQ),此时召回率和延迟是一对矛盾:nprobe/ef_search 调大召回涨、延迟也涨;文档量再上一个量级就要考虑分库分片和粗排 + 精排两段式。

MySQL 慢查询排查:分清「偶发一条」和「整体变慢」。 第一步永远是看现象——是一条 SQL 慢,还是所有 SQL 都慢。所有都慢先排除机器层面的原因:CPU/IO 打满、连接数爆了、锁等待(SHOW PROCESSLIST 看是不是一堆 Waiting for table metadata lock)、buffer pool 太小导致频繁刷盘。单条慢就走慢查询日志:slow_query_log 打开、把 long_query_time 调到阈值以下捞出来,再 EXPLAIN + EXPLAIN ANALYZE 看执行计划。常见根因和对应手段:type=ALL 全表扫描 → 补索引;key 用错了索引或索引区分度太低 → 强制索引或重建;rows 估算巨大 → 统计信息过期,ANALYZE TABLE;Extra 里出现 Using filesort / Using temporary → 排序分组没走索引;深分页 LIMIT 1000000, 20 → 改成基于上次游标的 WHERE id > ? 或延迟关联;select * 导致大量回表 → 建覆盖索引;单表数据量过大 → 分区、归档或分库分表。还要想到 SQL 本身没错但调用方式有问题:循环里查数据库(N+1)、事务里夹了 RPC 调用导致持锁时间过长。最后补一句治理:慢查询日志接入监控告警、定期用 pt-query-digest 做 TOP SQL 排行,而不是等用户投诉。

Spring 里的设计模式(挑一个讲透,别报菜名)。 最稳的选择是两个:代理模式与模板方法。代理:Spring AOP 对实现了接口的类用 JDK 动态代理,没接口用 CGLIB 生成子类字节码,代理对象在调用的前后织入事务、日志、缓存、权限等切面逻辑——@Transactional 之所以会「同类内部方法调用失效」,根因就是自调用走的是 this 而不是代理对象,这条追问几乎必被问到。模板方法:JdbcTemplate、AbstractApplicationContext.refresh()、RestTemplate 都是同一个套路——父类把「获取连接 → 执行 → 处理异常 → 释放资源」的骨架定死,把 SQL 执行、结果映射这些会变的部分留给回调或子类实现,从而保证资源一定被释放。想再补几个可以顺带提:单例(bean 默认 scope)、工厂(BeanFactory / FactoryBean)、观察者(ApplicationEvent + ApplicationListener)、适配器(HandlerAdapter 统一 DispatcherServlet 对各类 Controller 的调用)、责任链(Filter / Interceptor 链)。语言题是开放题,答「一门主语言 + 一门脚本 + 一门编译到原生的语言」这类组合,再讲新语言的学习路径:先跑通一个能用的最小项目拿到正反馈,再补类型系统与并发模型这类语言内核,最后读官方规范与生产级开源项目。

缓存与数据库的同步:难点从来不在「怎么写」,在并发窗口。 常见四种写法:先更新 DB 再更新缓存、先更新 DB 再删缓存(Cache Aside,业界默认)、先删缓存再更新 DB、订阅 binlog 异步刷新。为什么推荐先更新 DB、再删缓存而不是删了再更——因为「先删缓存、后更新 DB」在删完到写库完成之间有一个窗口,读请求会把旧值重新加载进缓存,之后就没机会被纠正了。即便用 Cache Aside,理论上仍有一个极小的不一致窗口:读请求在缓存失效后读到旧值、写请求随后更新 DB 并删缓存、读请求此时才把旧值写回缓存。工程上的应对是兜底而不是消灭:给缓存设过期时间(最终一致的最后一道保险)、用延迟双删(更新后删一次、延迟几百毫秒再删一次)、读多写少的场景直接上「删缓存 + binlog 异步补偿」,要求强一致的地方(比如资金)就不要拿缓存当数据源,读也走 DB 或者加分布式锁。面试里主动说出「缓存和 DB 的强一致是做不到的,我们能做的是收敛不一致窗口并保证最终一致」,比背四种方案更加分。

双写并发与扣减:Q10 其实在追问「你把 Redis 当缓存还是当账本」。 原帖描述的是「先从数据库里加、再往 Redis 同步」这种双写模型,两次操作不是原子的,只要中间失败、或被并发穿插,就会出现数据库和 Redis 各说各话:DB 加了 Redis 没加,用户看到旧库存;Redis 加了 DB 没加,就是一个永远不会被兑现的虚假库存。判断标准是谁是权威数据源:以 DB 为准的话,Redis 只能是可重建的缓存,丢了就回源重建,写入顺序固定成「先 DB 后 Redis 删」,超时重试 + 对账任务兜底;真要拿 Redis 做扣减账本,就必须让 DB 的最终状态由 Redis 的变更日志(binlog/消息)单向驱动,绝不允许两条链路各自写。这里还有个容易漏的点:INCR/DECR 是原子的,但「读库存 → 判断够不够 → 扣减」这三步不是,必须用 Lua 脚本把判断和扣减合成一次原子执行,否则 1 万 QPS 下必然超卖。

10 库存扛 1 万 QPS:核心思路是「把流量挡在真正扣减之前」,而不是让扣减更快。 分层看:① 最外层,活动页静态化 + CDN,按钮做倒计时前置,别让无效流量进后端;② 网关层,按用户/设备/IP 限流,同一用户只放少量请求进来,其余直接拒绝——1 万 QPS 里绝大多数本就是同一个人的重复点击;③ Redis 原子预扣减,库存预热到 Redis,用 Lua 做「判断 + 扣减 + 记录已购用户」一步原子完成,失败立即返回「已售罄」,把 1 万 QPS 里的 9990 个在内存里打发掉;④ 异步下单,预扣成功的少量请求写 MQ,下游消费端慢慢落库、创建订单,实现流量削峰——这一步要接受「下单可能失败」,所以必须有补偿:消费失败回滚 Redis 库存、超时未支付定时回收;⑤ 幂等,每个请求带唯一 token,防止重复下单和 MQ 重复消费;⑥ 稳定性兜底,对下游 DB、支付等依赖做线程池隔离和熔断降级,别让秒杀把整个站点拖垮,同时保证 Redis 单点故障时有降级方案(本地库存分片 + 异步汇总,或直接限流兜底)。能把这六层按「挡流量 → 原子扣减 → 异步落库 → 补偿对账」讲成一条链,比罗列一堆名词清楚得多。

手撕:最长有效连续括号子串。 三种解法都要能写。① 栈:栈里存下标,先压 -1 作为基准;遇 ( 压入其下标,遇 ) 弹出,弹空说明当前 ) 是多余的分隔符,把它的下标压进去当新基准;栈不空时 i - stack.peek() 就是当前有效长度,取最大值。时间空间都是 O(n)。② 动态规划:dp[i] 表示以 s[i] 结尾的最长有效长度,s[i]==')' 时看前一位——前一位是 ( 则 dp[i] = dp[i-2] + 2;前一位是 ) 且 dp[i-1] 那段的前一个字符是 (,则 dp[i] = dp[i-1] + 2 + dp[i - dp[i-1] - 2],注意下标越界判断。③ 双向计数:左右各扫一遍,( 计数加一、) 计数加一,两者相等时更新答案,右多于左时清零;反向再来一遍处理左括号富余的情况。这题的原帖结果是「编译出错就没让继续做」,所以面试里最实际的建议是先把边界的空串、单字符、全 ) 这三种情况在脑子里过一遍再落笔,写完自己举两三个样例手推——面试官给手撕的时间通常只有十到十五分钟,第一遍就编译不过,后面基本没有第二次机会。