字节后端一面:RAG 召回、MySQL 慢查询与秒杀库存一致性
- 轮次
- 一面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
- RAG 项目召回的准确度有多高?
- 物流测试的文档整体有多大?
- 怎么再提升召回率?
- MySQL 数据库如果出现慢查询,你会怎么办?
- 讲一个你比较了解的设计模式在 Spring 里的应用。
- 你掌握几门语言?
- 现在让你学习一门新语言,你会有什么路径去学习?
- 「高并发内容互动营销平台」这个项目里面,缓存具体是做什么功能的?
- 数据先存到缓存中,然后怎么同步到数据库?
- 实际的扣减,可能是先从数据库里面加,然后再往 Redis 同步。如果这两个操作同时发生,会出现不一致的场景吗?
- 服务本身可能会承受很多很多流量,比如秒杀一个商品它有 10 个库存,现在来了 1 万 QPS,你怎么保证服务的稳定性?
- 手撕:最长有效连续括号子串。
《参考解析》
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],注意下标越界判断。③ 双向计数:左右各扫一遍,( 计数加一、) 计数加一,两者相等时更新答案,右多于左时清零;反向再来一遍处理左括号富余的情况。这题的原帖结果是「编译出错就没让继续做」,所以面试里最实际的建议是先把边界的空串、单字符、全 ) 这三种情况在脑子里过一遍再落笔,写完自己举两三个样例手推——面试官给手撕的时间通常只有十到十五分钟,第一遍就编译不过,后面基本没有第二次机会。