腾讯日常 AI 应用一面:21 问完整回答(多 Agent + 秒杀多级缓存)
使用说明:Q1-Q10 按「协调者 + 专家 Agent + 共享任务/产物存储 + 分层记忆」作答;Q11-Q21 按「Scope Cache → Memcached → Redis → MySQL」的秒杀链路作答。文中的 QPS、命中率、耗时都是示例量级,面试时替换成你自己的真实数据;如果某处和你实际实现不一致,优先改事实,不要背话术。
答题总原则:先给结论,再讲分层/机制,最后讲失败兜底和观测。面试官真正想听的不是“用了什么组件”,而是“为什么这样分层、出错怎么办、怎么证明有效”。
第一部分:多 Agent、记忆与上下文(Q1-Q10)
Q1. 多 Agent 是怎么实现的?
一句话答案:不是把多个 Prompt 放在一起,而是做一套“编排层 + 专家 Agent + 共享状态 + 工具/记忆 + 可观测”的分布式任务系统;LLM 只是执行器,可靠性靠机制而不是靠模型自觉。
展开回答:
我会把它拆成六层:
| 层 | 职责 | 关键设计 |
|---|---|---|
| 编排层 | 意图识别、任务分解、DAG 生成、调度、汇总 | 协调者模式,统一入口,子 Agent 不横向通信 |
| 执行层 | 每个专家 Agent 干一类事 | 角色 Prompt + 工具集 + 输出 Schema + 独立记忆命名空间 + 超时/重试策略 |
| 状态层 | 任务和产物的唯一事实 | Task 表、Artifact 表、状态机,跨 Agent 只传 ID,不传整段对话 |
| 工具层 | 接外部系统 | MCP/内部 API 统一网关,鉴权、限流、审计 |
| 记忆层 | 短期会话、长期记忆、知识库 | 按 Agent/租户分区,召回后带来源 |
| 观测层 | 出问题能查 | trace_id 贯穿,记录每个 Agent 的输入、输出、Token、耗时、失败原因 |
一次请求的完整流程是:
- 入口接收请求,做意图识别和槽位补全;
- 规划器把目标拆成子任务,生成带依赖的 DAG;
- 调度器按“依赖是否就绪 + 业务优先级 + 并发配额”派发;
- 专家 Agent 用 ReAct 或 Plan-Execute 执行,调用工具、读写记忆;
- 结果按约定 Schema 回传,协调者做校验、去重、冲突仲裁;
- 汇总 Agent 做归一化和覆盖度检查,生成最终答案;
- 与原始目标回检,不满足就回到规划器补做。
实现选型上,LangGraph 适合“状态图 + 条件边 + checkpoint 断点恢复”,自研编排器适合调度逻辑复杂、要接存量系统的场景。但框架不是重点,重点是三件事:任务结构化、结果结构化、状态可持久化。只要这三件事做到,换什么框架都能落地。
加分句:多 Agent 的本质是分布式任务系统,不是“多个聊天机器人”。分布式系统该有的幂等、超时、重试、状态机、可观测,一个都不能少。
Q2. 你是怎么提高回复质量的?
一句话答案:不是把 Prompt 写得更长,而是“评测驱动 + 分层拦截”:先定义指标和评测集,再定位瓶颈在输入、召回、生成还是校验,最后做数据闭环。
展开回答:
我会按五层做:
- 定义指标和评测集。正确性、相关性、完整性、格式合规、时延、成本都要有口径;离线固定评测集保证可比,线上用 bad case 回流补充。没有评测集,调 Prompt 就是玄学。
- 输入层。做 Query 改写、意图澄清、多轮指代消解、槽位补全。很多“答非所问”不是模型不行,是问题本身没被理解。
- 检索层。混合召回(BM25 + 向量)、Metadata 过滤、Rerank、Parent-Child 分块、多路召回用 RRF 融合。RAG 场景里,召回错了,生成再强也救不回来。
- 生成层。结构化 Prompt、Few-shot、输出 Schema、引用约束、分模型路由(简单问题小模型、复杂问题大模型)、Temperature 控制。让模型“按格式答、按证据答”。
- 校验层。规则校验 + LLM-as-Judge + 事实一致性检查(结论是否能被引用支撑)+ 敏感/合规校验;不通过就重试、降级或转人工。
- 数据闭环。Bad case 分类归因、Prompt 版本化、A/B 实验、定期回归。每次改动都要能回答“哪个指标涨了、哪个指标掉了”。
举个定位例子:如果 Bad case 集中在“答非所问”,先查召回和意图;如果“召回对了但答错”,查生成和校验;如果“格式经常错”,查 Schema 和解析兜底。能说清瓶颈在哪一层,才叫提高质量;否则只是碰运气。
加分句:质量提升的终点不是“感觉变好了”,而是“固定评测集上的指标可复现地变好了”。
Q3. 相关记忆召回里的相似度是怎么匹配的?
一句话答案:两阶段匹配——召回阶段用向量 ANN 算语义相似度,精排阶段用 Cross-Encoder 重排;记忆场景再叠加时间衰减、重要度、置信度,并对订单号、手机号这类精确查询走 KV/BM25 兜底。
展开回答:
第一阶段,召回。
- 用 Embedding 模型把 Query 和记忆条目编码成向量,比如 bge-m3、text-embedding-3;
- 向量做归一化后,余弦相似度等价于内积,工程上直接用内积更省;
- 用 HNSW 或 IVF-PQ 做 ANN 检索,先取 Top 50-100,保证召回率;
- 同时跑一路稀疏检索(BM25),专有名词、编号、代码符号这类向量不敏感的内容靠它兜底;
- 两路结果用 RRF 融合,
score = Σ 1/(k + rank_i),比手工加权更稳; - 检索前先做 Metadata 过滤:tenant_id、user_id、memory_type、时间范围,保证多租户隔离和时效性。
第二阶段,精排。
- 用 bge-reranker 这类 Cross-Encoder 对 Top 50 逐条打分,取 Top 3-5;
- Cross-Encoder 能看 Query 和记忆的交互,精度比单向量高,但贵,所以只用在精排;
- 再用 MMR 做多样性去冗,避免召回五条几乎一样的内容。
记忆场景的额外打分。
记忆不是普通文档,除了语义相似,还要看“该不该现在用”:
final_score = α·cosine + β·recency + γ·importance + δ·confidence- recency:时间衰减,越久远的记忆权重越低,半衰期按业务定;
- importance:用户显式强调、反复出现、关键决策,权重更高;
- confidence:来源可靠度,用户原话 > 模型推断;
- 最后卡阈值:低于阈值宁可不用,避免“召回一堆无关记忆污染上下文”。
精确匹配兜底。 订单号、手机号、文件路径、错误码这类内容,向量相似度不可靠,直接走 KV、BM25 或前缀索引。向量库不是万能检索,它只擅长模糊语义。
工程细节:Query 和文档必须用同一个 Embedding 版本;记忆更新/删除要同步索引;召回结果带来源、时间、版本,方便后续校验和追责。
陷阱应对:只答“余弦相似度”太浅。要答出“召回 + 精排 + 阈值 + 隔离 + 精确兜底”这一整套。
Q4. 多 Agent 的协调机制具体是怎么做的?
一句话答案:用协调者模式(Orchestrator-Worker),协调者负责规划、分派、监控、仲裁、汇总;子 Agent 之间不横向通信,所有协作通过共享的任务表和产物表完成。
展开回答:
第一,角色分工。 协调者只做四件事:拆任务、派任务、收结果、做仲裁。专家 Agent 只对自己那类子任务负责,不互相聊天。这样做的好处是所有决策经过一个点,出问题看协调者日志就能定位;代价是协调者会成为瓶颈,规模大了要按业务域分片或做层级协调。
第二,任务状态机。 每个任务都有明确状态:
PENDING → READY → RUNNING → SUCCEEDED / FAILED / TIMEOUT / CANCELLED每次状态迁移带 version,用 CAS 更新,防止两个调度器重复派发同一个任务。
第三,DAG 调度。 任务拆完后是有向无环图,拓扑排序,入度为 0 的进就绪队列;无依赖的并行跑,有依赖的等前驱完成。这样既保证顺序,又不会把能并行的活串行化。
第四,通信与状态。 消息队列里只传 task_id 和少量元数据,大结果写 Artifact Store,消息只带引用。不要把整段结果塞进消息,也不要把整段对话历史传给下一个 Agent,否则既烧 Token 又会把上一个 Agent 的上下文偏见带过去。
第五,超时、重试、幂等。 任务领取用 lease + heartbeat,Agent 挂了或超时,协调者回收任务重新派发;重试有上限和指数退避;用 task_id + attempt 做幂等键,防止重复执行和重复写结果。
第六,冲突仲裁。 两个 Agent 给出不同结论时,按证据强度、置信度、权威数据源、时间新鲜度排序;裁不了就启动 Judge Agent 或升级人工,不要强行取平均。
第七,可观测。 trace_id 贯穿全链路,记录每个任务的开始/结束、输入摘要、输出摘要、Token、耗时、失败原因。没有这条时间线,多 Agent 出问题基本没法查。
加分句:协调机制是“组织设计”,不是 Prompt 设计。Prompt 只能让 Agent 更会干活,组织机制才能保证它干对活、不重复干、干完能交回来。
Q5. Agent 会不会拿错任务或者拿错结果?
一句话答案:会,而且一定会。所以要靠“四把锁”防:任务归属锁、能力匹配锁、结果契约锁、幂等版本锁;不能靠 Agent 自觉。
展开回答:
第一把锁,任务归属。 任务带 task_id + tenant_id + agent_id + lease,Agent 只能拉取绑定给自己的任务;领取时 CAS 抢占,租约到期自动回收。没有归属校验,多实例部署一定会出现“同一个任务被两个 Agent 领走”。
第二把锁,能力匹配。 任务声明需要的能力和输入输出 Schema,调度器只投给能力注册表里匹配的 Agent;不匹配直接拒绝,而不是“先干了再说”。这能避免“让检索 Agent 去执行退款”。
第三把锁,结果契约。 结果必须带 task_id、agent_id、attempt、trace_id、schema_version。协调者按 task_id 归并,不按消息到达顺序归并,也不按自然语言内容猜;Schema 校验失败直接拒收。拿错结果的典型原因就是用自由文本回传、按“最后一条消息”归并、多个 Agent 共享一个上下文。
第四把锁,幂等与版本。 task_id + attempt 做幂等键,重复结果去重;结果写入带 version,CAS 防止旧结果覆盖新结果。重试场景下,旧 attempt 的结果后到,必须被识别并丢弃。
兜底策略:结果对不上 task_id → 丢弃并告警;任务超时 → 回收重派;同一个 Agent 连续失败 → 熔断,切备用 Agent 或转人工;结果 Schema 不合法 → 打回重做,不进入汇总。
加分句:跨 Agent 传递的一切都应该是“ID + 结构化 payload”,自由文本只用于展示,不用于控制。把这句话做到,拿错任务和拿错结果的概率会下降一个数量级。
Q6. Agent 的任务有没有先后顺序 / 优先级?
一句话答案:有,分两层——依赖顺序决定“能不能跑”,业务优先级决定“先跑谁”;再叠加截止时间和并发配额,防止饿死和打爆下游。
展开回答:
依赖顺序。 任务拆成 DAG,拓扑排序决定执行先后;强依赖必须等前驱完成,软依赖可以并行、最后合并。比如“先检索政策,再生成答案,再做合规校验”,这就是硬依赖。
业务优先级。 实时用户请求优先于后台批处理;高价值、高时效任务优先于普通任务。调度器用优先级队列,不是先来先服务。
截止时间。 有 SLA 的任务用 EDF(最早截止优先);超时直接失败或降级,不能让一个任务无限占着资源。
并发配额。 按 Agent、工具、模型分别设信号量和令牌桶。LLM 有 RPM/TPM 限制,下游 API 也有 QPS 限制,不设配额就会把下游打挂。
防饿死。 等待越久优先级越高(aging);给低优先级任务预留槽位;避免高优先级任务源源不断把低优先级饿死。
抢占的取舍。 一般不做真抢占,因为 LLM 调用很难安全中断;实际做的是“取消标记 + 结果丢弃”,或者把长任务拆小,让调度粒度更细。高优先级任务依赖低优先级资源时,临时提升资源优先级,避免优先级反转。
加分句:优先级不是写在 Prompt 里的“请优先处理”,而是调度器的队列、配额和超时策略决定的。
Q7. 不同结果怎么决定下一步走哪条链路?
一句话答案:靠“结构化路由字段 + 规则优先、模型兜底 + 置信度分档”,不靠模型自由发挥;路由表是代码和配置,不是 Prompt。
展开回答:
第一步,让每个 Agent 输出固定 Schema。 至少包含:intent、status、confidence、need_clarification、risk_level、next_action_hint、evidence。路由函数只读这些字段,不读自然语言。
第二步,规则优先。 金额、权限、合规、风控这类硬规则一票否决或直接路由,模型不能覆盖。比如命中黑名单就直接转人工,不允许模型“再想想”。
第三步,模型兜底。 语义模糊时用分类模型或 LLM 做路由,但输出必须是受约束的枚举,不能让模型自由生成“下一步建议”。
第四步,置信度分档。
- 高置信:直接走下一节点;
- 中置信:补充检索或二次校验后再走;
- 低置信:澄清、多路投票或转人工。
第五步,失败路由。 重试 → 换工具/换 Agent → 降级 → 人工,每一级都有上限和超时,不能无限重试。
第六步,可配置和可复盘。 状态机、路由表版本化,支持灰度和回滚;每次路由记录“用了哪个字段、命中哪条规则、为什么走这条边”,出问题才能复盘。
第七步,回检。 执行完检查是否满足原始目标,不满足回到规划器补做,而不是直接把结果交出去。
加分句:好的路由是“可解释、可配置、可回滚”的;坏的路由是“模型说走哪就走哪”。
Q8. 一个 Agent 能不能执行多个任务?
一句话答案:能,但要区分“串行多任务”“并行多任务”“子 Agent 隔离”三种模式;核心是三件事——上下文隔离、状态外置、并发配额。
展开回答:
串行多任务。 同一个 Agent 顺序处理同域任务,上下文可以复用,但要定期压缩,避免越跑越脏。适合同一用户的连续请求、同一项目的多步操作。
并行多任务。 每个任务独立 session/namespace,只共享只读的全局状态,结果按 task_id 回传。禁止共享可变对话历史,否则一定会串话。
子 Agent 隔离。 主 Agent 派生 sub-agent 处理大任务,sub-agent 在自己的上下文窗口里跑,只回传摘要和产物引用。这是控制上下文膨胀最有效的手段。
并发控制。 按 Agent 的模型配额、工具 QPS、内存限制设并发上限;超限排队或拒绝,不要无限开线程。LLM 调用是稀缺资源,并发不是越多越好。
状态外置。 Agent 进程不保存业务状态,任务、产物、进度都放外部存储,方便水平扩展和故障迁移。Agent 本身应该是无状态服务。
不建议的做法:一个 Agent 同时持有几十个任务、共享一个超长上下文。结果是注意力分散、互相污染、成本失控,而且一个任务失败会拖垮整批。
加分句:Agent 能不能多任务,本质不是“模型能不能”,而是“你的上下文隔离和状态管理做没做到”。
Q9. 多个 Agent 的结果最后怎么汇总?
一句话答案:汇总不是字符串拼接,而是“归一化 → 去重 → 冲突仲裁 → 聚合 → 覆盖度回检”,并且保留每个结论的来源。
展开回答:
第一,归一化。 不同 Agent 的输出 Schema、单位、口径、时间范围可能都不一样,先映射到统一字段。比如一个报“元”,一个报“万元”,不统一就会汇总出错误数字。
第二,去重。 相同事实合并,保留来源和置信度;重复内容不要把最终答案撑爆。
第三,冲突仲裁。 同一字段出现多个值时,按权威数据源、证据强度、置信度、时间新鲜度排序;裁不了就显式标注分歧,不要强行编一个看似确定的答案。
第四,聚合策略。
- Map-Reduce:分段总结,再合并成总答案;
- 投票:多个 Agent 独立作答,多数票或加权票;
- 加权融合:按置信度和历史准确率加权;
- 层级汇总:先子汇总,再总汇总,适合大规模任务。
第五,覆盖度检查。 逐条对照原始子问题,看有没有漏答;缺项回补或重跑,不要直接交。
第六,保留 provenance。 记录“哪段来自哪个 Agent、哪份证据、哪次执行”,方便展示引用和追责。
第七,长结果压缩。 分层摘要,控制上下文;最终由汇总 Agent 生成统一口径的答案。
加分句:好的汇总会保留分歧和不确定性;差的汇总会把分歧磨平成一个看似确定的错误答案。
Q10. 你的上下文管理具体是怎么做的?
一句话答案:分层 + 预算 + 压缩 + 按需加载 + 隔离 + 可观测。上下文不是“能塞多少塞多少”,而是每一块都有预算和淘汰规则。
展开回答:
第一,分层。 我会把上下文分成六块:
| 上下文块 | 放什么 | 策略 |
|---|---|---|
| System/规则 | 角色、约束、工具定义 | 稳定不变,放最前,利于 Prompt Cache |
| 任务状态 | 目标、约束、已完成、待办、失败记录 | 结构化 JSON,替代长对话 |
| 检索证据 | 文档片段、记忆、工具结果 | 按需加载,带来源,控制条数 |
| 对话历史 | 最近多轮 | 滑动窗口 + 滚动摘要 |
| 工具结果 | API 返回、文件内容 | 截断/结构化抽取,原始结果存外部 |
| 长期记忆 | 偏好、项目事实、经验 | 常驻摘要 + 索引,按需读 1-2 份证据 |
第二,预算。 按模型窗口分配,比如 System 10%、任务状态 15%、证据 35%、历史 25%、输出预留 15%。超出预算就按优先级淘汰,而不是等超限报错。
第三,压缩。 到 70%-80% 触发压缩:先删寒暄、重复内容、低价值工具输出;保留指令、约束、数字、ID、用户纠正和关键决策。滚动摘要要版本化,能回溯原始日志,不能摘要完就把事实丢了。
第四,按需加载。 常驻的只有摘要和索引,证据用的时候再取;大文件调研丢给子 Agent,主上下文只收摘要。这就是“索引常驻 + 内容懒加载”。
第五,隔离。 不同 Agent 独立上下文命名空间;跨 Agent 交接传状态和引用,不传整段历史。否则一个 Agent 的偏见会污染整条链路。
第六,Prompt Cache 友好。 System、工具定义、固定规则放前面且顺序稳定,变化的内容放后面,提高缓存命中率,降低成本。
第七,可观测。 记录每次上下文的组成、Token 数、压缩次数、缓存命中率;上下文突然膨胀或反复压缩,都是异常信号。
一个容易忽略的点:异步线程和线程池不会自动继承 ThreadLocal,上下文要显式传播,否则子线程拿不到主线程的任务状态。这一点和秒杀里的 Scope Cache 是同一类问题。
加分句:上下文管理的目标不是“记住更多”,而是“在有限窗口里,让模型看到当前决策最需要的信息”。
第二部分:秒杀系统与多级缓存(Q11-Q21)
Q11. 秒杀系统整体链路是怎么实现的?
一句话答案:分两条链路——读链路做“缓存漏斗”,写链路做“Redis 原子扣减 + MQ 异步下单 + DB 最终对账”;核心是把并发收敛到一个原子点,再把写操作异步化。
展开回答:
读链路(商品详情/活动页):
浏览器 → CDN(静态资源)→ 网关(限流/鉴权/防刷)
→ 详情聚合服务 → Scope Cache(请求级)
→ 本地缓存(热点)→ Memcached(商品/活动元数据)
→ Redis(库存/价格热点)→ MySQL写链路(秒杀下单):
客户端 → 网关(令牌桶 + 验证码 + 黑名单 + 签名)
→ 秒杀服务 → Redis Lua(校验库存 + 限购 + 原子扣减)
→ MQ → 订单服务(幂等消费 + DB 扣库存 + 创建订单)
→ 支付 → 超时未支付关单 → 回补库存 → 定时对账关键设计:
- 库存预热:活动前把库存写入 Redis,DB 仍是真相源;预热后做一次对账,避免“缓存库存”和“DB 库存”开局就不一致。
- 原子扣减:Lua 脚本把“查库存、查用户已购、扣减、写购买标记”合成一个原子操作,避免查改竞态。
- 限购:Redis Hash/Set 记录
user_id已购数量,DB 唯一索引做最终兜底。 - 幂等:请求 ID + 用户 ID + 活动 ID 做唯一键;MQ 消费幂等,重复消息不重复下单。
- 削峰:MQ 异步下单,前端返回“排队中”,轮询或推送结果;把瞬时写压力摊平。
- 防超卖:Redis 是闸门;DB 扣减带
stock >= 0条件,唯一索引兜底;两层都过才算成功。 - 回补:未支付超时关单,Redis 和 DB 回补库存;失败进补偿队列。
- 对账:定时比对 Redis 库存、DB 库存、订单量,发现偏差告警并修复。
如果单 Key 热点扛不住,再做库存分桶:把 1000 件库存拆成 10 个桶,请求按用户 ID 哈希路由到桶,降低单 Key 竞争;代价是复杂度上升和跨桶汇总,必须配合对账。不是所有秒杀都需要分桶,先把原子扣减和异步下单做好。
加分句:秒杀不是“缓存扛流量”,而是“把并发收敛到一个原子点,再把写操作异步化,最后用对账兜底”。
Q12. 为什么需要这么多层缓存?
一句话答案:每层解决不同问题,不是重复建设;越靠前越快越便宜但一致性越弱,越靠后越权威但越慢,多级缓存就是在这三者之间做分层过滤。
展开回答:
| 层 | 解决什么 | 一致性 | 成本 |
|---|---|---|---|
| 浏览器/CDN | 静态资源、网络距离 | 静态强一致 | 极低 |
| Scope Cache | 单请求内重复查询、请求内快照 | 请求内一致 | 极低 |
| 本地缓存 | 单机热点、抗 Redis 故障 | 秒级最终一致 | 低 |
| Memcached | 跨实例共享的大容量只读 | 秒级最终一致 | 中 |
| Redis | 库存原子操作、分布式协调、热点 | 准强一致 | 高 |
| MySQL | 真相源、事务、审计 | 强一致 | 最高 |
第一,漏斗效应。 大部分请求在前几层就被拦掉,DB 只承接极少量。比如 10 万 QPS,本地缓存挡掉 90%,Memcached 挡掉剩下的 9%,Redis/DB 只看到 1% 的流量。
第二,职责不同。 Scope Cache 管“一次请求内不重复查”,本地缓存管“单机热点”,Memcached 管“跨实例共享的只读大对象”,Redis 管“需要原子性和协调的操作”,DB 管“最终事实”。把库存扣减放 Memcached 是不合适的,把商品描述放 Redis 又太贵。
第三,故障隔离。 Memcached 挂了可以回源 Redis;Redis 挂了可以限流 + 本地缓存 + DB 保护;不同业务用不同集群,避免一个热点拖垮全站。
第四,成本约束。 全放 Redis 太贵,而且 Redis 的原子操作和集群扩展成本高;全放 DB 扛不住。分层是用成本换吞吐。
第五,历史演进。 多级缓存通常不是一次设计出来的,而是按瓶颈逐层加出来的。每加一层都要有命中率和收益数据;没有收益的层应该撤掉,不要为了“架构好看”而堆层。
加分句:多级缓存的本质是“用空间换时间、用一致性换吞吐”;面试时要能说清每一层到底挡掉了什么、允许落后多久。
Q13. Scope Cache 到底有什么作用?
一句话答案:Scope Cache 是请求级缓存,生命周期只有一次请求,解决“同一请求内重复读”和“请求内一致性快照”,不负责跨请求共享。
展开回答:
实现方式。 用 ThreadLocal<Map<Key, Value>> 保存,请求入口初始化,请求结束在 finally 里 remove()。Key 一般由“业务维度 + 场景 + 参数”组成,比如 product:1001:detail、activity:2001:stock。
四个作用:
- 合并重复查询:一次详情页或下单请求内,商品信息会被多个下游、多个校验点调用,Scope Cache 只让第一次真正回源,后面直接命中。
- 请求内快照一致:同一请求内多次读取同一份数据,看到的是同一个版本,避免“前面校验通过、后面状态变了”的诡异问题。
- 降低下游 QPS:省掉网络往返、序列化/反序列化、连接池占用;乘上 QPS 后收益很可观。
- 故障隔离简单:生命周期短,不需要复杂的跨请求失效机制,天然不会长期脏。
适用场景:读多写少、请求内重复读、对一致性要求是“请求内一致”的场景,比如商品详情聚合、下单前校验、权限/配置读取。
不适用场景:跨请求热点(用本地缓存)、需要强一致跨请求(用 Redis/DB)、写操作(缓存写会掩盖冲突)。
注意事项:线程池复用必须 remove(),否则会内存泄漏和跨请求串数据;异步线程不继承 ThreadLocal,需要上下文传播;缓存 Value 要不可变或防御性拷贝;单请求缓存条目要限量,避免大请求把内存撑爆。
度量:命中率、单请求平均回源次数、节省的 RPC 次数。命中率低就说明这层不值得,应该撤。
Q14. 一个请求结束以后 ThreadLocal 就没了,那这一层缓存有什么意义?
一句话答案:它的意义在“请求内”,不是“跨请求”;短生命周期不等于没价值,收益等于“单请求重复次数 × QPS × 单次远程调用开销”。
展开回答:
一次请求内重复查询是常态。比如商品详情页聚合服务要调商品、营销、库存、评论、推荐五个下游,每个下游内部可能都查一次商品基础信息;下单链路要校验商品状态、活动状态、库存、限购,也会多次读同一个商品。
假设 QPS 5 万,每个请求平均查同一商品 5 次,Scope Cache 后只回源 1 次,相当于每秒省掉 20 万次远程调用(示例量级,换成你的真实数据)。这不是“请求结束就没了”,而是“在这一个请求的生命周期里,已经把该省的都省了”。
第二个收益是请求内一致性:同一请求内看到同一份数据,避免“库存校验时还有货,创建订单时没货”的前后矛盾。
它不负责跨请求热点,那是本地缓存、Memcached、Redis 的职责。分层缓存就是每层只做一件事,不能指望一层解决所有问题。
如果实测命中率很低,比如每个请求本来就只查一次,那这层确实没意义,应该去掉。不要为了“多级”而多级,每一层都要有数据证明它挡掉了重复流量。
陷阱应对:面试官问“请求结束就没了,有什么用?”——答“它本来就不是给下一个请求用的,是给当前请求内多次调用用的;收益要按 QPS 乘起来看,同时它提供请求内快照一致性”。
Q15. 为什么一个请求会多次查询同一个商品?
一句话答案:因为一个请求背后是多个服务、多个校验点、多次业务复用,天然会重复查同一商品;这不是代码写得烂,而是分层架构的正常副作用。
展开回答:
第一,聚合服务拆分。 详情页分别调商品服务、营销服务、库存服务、评论服务、推荐服务;每个下游内部又可能查一次商品基础信息。
第二,多层校验。 网关鉴权、风控、活动资格、限购、库存校验,每一层都可能加载商品;这些校验逻辑分布在不同的类或服务里,很难天然共享一次查询结果。
第三,业务复用。 算价格要查商品,算运费要查商品,算优惠要查商品,渲染 SKU 还要查商品;同一个请求里多个业务逻辑都依赖商品。
第四,N+1 查询。 SKU 列表循环里每个 SKU 查一次商品/库存,这是最常见的放大来源。
第五,重试和回源。 一次远程调用失败重试,或者缓存未命中回源,都会放大查询次数。
第六,不同视图。 基础信息、活动信息、库存信息分开查,但都依赖同一个商品 ID。
所以用请求级缓存按“商品 ID + 场景”合并;跨请求再用本地缓存、Memcached、Redis。先埋点统计“单请求回源次数”,再决定缓存放在哪一层,不要凭感觉。
Q16. 为什么 Redis 前面还要再放 Memcached?
一句话答案:不是 Memcached 比 Redis 强,而是“把只读大流量和原子交易流量分开”:Memcached 承接商品/活动等只读 KV,Redis 承接库存扣减、锁、限购等需要原子性的操作。
展开回答:
第一,分工不同。 Memcached 适合大 value、高并发、可容忍短时不一致的只读数据,比如商品详情、活动配置、用户基础信息;Redis 适合库存扣减、分布式锁、限购、排行榜这类需要原子性和复杂结构的数据。把两者混在一起,热点读会把 Redis 打满,库存扣减又会被大 value 阻塞。
第二,保护 Redis。 Redis 的命令执行主线程是单线程的,热 Key 或大 value 会阻塞其他命令;把普通读流量挡在 Memcached,Redis 只处理少量关键操作,延迟更稳。
第三,成本。 Memcached 是纯内存 KV,无持久化、无主从复制开销,单机内存利用率高,扩容便宜;Redis 的持久化、复制、集群都有额外成本。用 Memcached 承接海量只读,单位成本更低。
第四,故障隔离。 商品域和交易域用不同集群:Memcached 挂了不影响扣减,回源 Redis/DB;Redis 挂了可以限流降级,不至于全站不可用。
第五,历史演进。 很多系统是先有 Memcached,后来因为有原子操作和复杂结构需求才加 Redis。从零设计可能用 Redis Cluster + 本地缓存就够,但要能说清当时的取舍:我们不是用 Memcached 替代 Redis,而是用它分流。
一致性问题:Memcached 只存可容忍最终一致的数据;库存绝不放 Memcached 做扣减,否则 CAS 竞争和一致性都很难处理。
加分句:缓存选型不是“谁功能多选谁”,而是“谁更适合这类流量”。
Q17. Memcached 的能力比 Redis 强吗?为什么它能解决 Redis 的压力?
一句话答案:能力上 Memcached 不如 Redis,但在“简单 KV 高并发只读”这个 workload 上更合适,所以能把这一类流量整体搬走,从而分担 Redis 压力。
展开回答:
| 维度 | Memcached | Redis |
|---|---|---|
| 线程模型 | 多线程,能吃多核 | 命令执行主线程单线程(6.0+ I/O 多线程) |
| 数据结构 | 只有 KV | String/List/Hash/Set/ZSet/Stream |
| 原子能力 | CAS | Lua、事务、原子命令 |
| 持久化 | 无 | RDB/AOF |
| 集群 | 客户端分片 | 原生 Cluster |
| 内存效率 | Slab 分配,纯缓存开销低 | 有对象编码、过期、复制开销 |
| 适合 | 大流量简单只读 | 原子操作、复杂结构、持久化 |
为什么能减压:
- 多线程 + 简单协议,单机吞吐高,适合海量小 KV 读;
- 无持久化、无复制,没有 fork、AOF 重写、主从同步的开销;
- 客户端一致性哈希水平扩展,加机器接近线性提升;
- 把“只读、可短暂不一致”的流量整体搬走,Redis 的 QPS 和内存都降下来;
- Redis 只保留“必须原子、必须复杂结构”的少量操作,延迟更可控。
但 Memcached 的弱项很明显:无持久化、无复杂数据结构、无原生集群、value 默认 1MB、key 最大 250B、重启即空、不支持 Lua/事务。所以它只能分流,不能替代 Redis。
陷阱应对:千万别说“Memcached 比 Redis 强”。正确说法是“在特定 workload 上更合适”。
Q18. 多级缓存的数据一致性怎么保证?
一句话答案:先分级——库存强一致,商品元数据最终一致;不同数据用不同策略,不能一刀切。核心是“单一真相源 + 写后失效 + 版本号 + TTL 兜底 + 对账”。
展开回答:
第一,定义一致性级别。
| 数据类型 | 一致性要求 | 策略 |
|---|---|---|
| 库存/资格 | 准强一致(不超卖,DB 最终对账) | Redis Lua 原子扣减,DB 异步落库 + 对账 |
| 价格/活动 | 秒级最终一致 | DB 更新后删缓存 + binlog 失效 + 版本号 |
| 商品描述 | 分钟级最终一致 | TTL + 定期刷新 |
| 静态资源 | 强一致 | CDN 版本化 URL |
第二,单一真相源。 DB 是最终事实,所有缓存都是派生视图;任何缓存坏了都能回源重建。这条是底线。
第三,写路径统一。 DB 提交成功后删除缓存,不是更新缓存;删除失败交给重试、消息补偿和 TTL 兜底。多级缓存通过 binlog → Canal → MQ 广播失效,保证本地缓存、Memcached、Redis 最终都收到通知。
第四,版本号。 缓存 Value 带 version 或 updated_at;读时比对版本,旧版本不能覆盖新版本;本地缓存记录版本,收到新版本才更新。这样即使消息乱序,也不会把新值覆盖成旧值。
第五,TTL 分层。 Scope Cache 请求级;本地缓存 1-5 秒;Memcached 1-5 分钟;Redis 按业务定;TTL 加随机抖动,防雪崩。TTL 是最后一道保险,不是一致性方案。
第六,库存特殊处理。 库存不走普通 Cache Aside:Redis Lua 是闸门,DB 异步扣减,唯一索引兜底,定时对账修复。Redis 与 DB 短暂不一致是可接受的,但不能超卖。
第七,读路径校验。 关键数据读 Redis/DB 版本;发现缓存版本落后,直接 bypass 并刷新,不要继续用旧值。
第八,监控。 不一致率、删除失败率、对账差异、回源 QPS;超过阈值告警。
加分句:多级缓存一致性不是“让所有层同时更新”,而是“定义每层允许落后多久,以及落后时怎么发现和修复”。
Q19. 数据库和缓存的更新顺序是什么?
一句话答案:普通读缓存用 Cache Aside——先更新 DB,再删除缓存;秒杀库存是例外——先 Redis 原子扣减,再异步落 DB。顺序错了,就会长期脏数据。
展开回答:
为什么是先更新 DB、再删缓存?
如果先删缓存、再更新 DB,会出现这个时间线:
T1 写线程删除缓存
T2 读线程缓存未命中,读到 DB 旧值
T3 读线程把旧值回填缓存
T4 写线程更新 DB 为新值结果缓存里长期是旧值,直到 TTL 过期。反过来,先更新 DB 再删缓存,即使删除失败,也有 TTL 兜底,下一次读会回源新值。
为什么是删除,不是更新?
并发写时,两个写线程更新缓存的顺序可能和 DB 提交顺序相反,旧值会覆盖新值;删除是幂等的,下次读自然重建,简单可靠。
剩下的并发窗口。 先更新 DB 再删缓存也不是绝对安全:读线程在删除前拿到旧值、在删除后回填,仍会短暂脏。解决办法是延迟双删:DB 提交后删一次,延迟 500ms-1s 再删一次;更稳的是 binlog 订阅删除,由数据库变更事件驱动,不依赖业务代码。
极端强一致:分布式锁、串行化、读写锁都能做,但代价高,普通商品数据不值得;只有库存、余额这类关键数据才考虑更强的手段。
秒杀库存为什么例外?
秒杀写 QPS 太高,不能每单同步 DB,也不能用普通 Cache Aside。做法是:
- Redis Lua 原子扣减,作为闸门;
- 扣减成功写 MQ,异步创建订单、扣 DB 库存;
- DB 扣减带
stock >= 0条件和唯一索引; - 失败补偿、定时对账。
这不是“缓存更新”,而是“Redis 先记账、DB 最终落账”。
陷阱应对:面试官问“先删缓存再更新 DB 行不行?”——答“有并发读回填旧值的窗口,不推荐;除非加锁、版本号或延迟双删”。
Q20. Memcached 是怎么同步更新的?
一句话答案:Memcached 原生没有主从/集群同步,它是纯缓存;“同步更新”靠应用层删除 + 消息广播 + 版本号 + TTL,不是靠 Memcached 自己。
展开回答:
第一,写路径。 DB 提交成功后,应用删除对应 Key(delete,不是 set),让下一次读回源重建。删除是幂等的,失败可以重试。
第二,多实例定位。 Memcached 没有原生集群,客户端用一致性哈希分片,一个 Key 只落在一个实例上;删除时定位到对应实例即可。如果本地缓存也有副本,需要广播删除。
第三,异步失效。 Canal 订阅 MySQL binlog → 发 MQ → 各服务消费 → 删除 Memcached 和本地缓存。这样业务代码不用在每个写路径都写删除逻辑,解耦且可重试。
第四,版本号。 Value 里带 version;读时和权威版本(Redis/DB)比对,落后就 bypass 刷新。这样即使删除消息丢了、乱序了,也不会长期用旧值。
第五,TTL 兜底。 每个 Key 设 TTL,加随机抖动;删除失败也能自然过期。
第六,重启恢复。 Memcached 重启后数据全空,靠 Cache Aside 回源重建;热点 Key 提前预热。
第七,失效顺序。 先删分布式缓存(Memcached/Redis),再广播本地缓存失效;本地缓存 TTL 很短,做最后兜底。顺序反了,本地缓存可能从还没删的分布式缓存里回填旧值。
加分句:Memcached 不负责同步,它只负责存;同步是业务代码和消息系统的事。
Q21. 如果 Memcached 更新失败怎么办?
一句话答案:不能装作没发生;靠“重试 + 补偿 + TTL + 版本校验 + 读路径降级 + 告警对账”,保证不长期脏、不拖垮主流程。
展开回答:
第一步,立即重试。 指数退避重试 2-3 次,区分网络超时和节点宕机;超时可能是网络抖动,宕机要摘节点。
第二步,落补偿。 失败写 MQ 或补偿表,定时重放;超过次数进死信,告警并人工处理。不要只打一行日志就算完。
第三步,TTL 兜底。 Key 设置较短 TTL,保证最终过期;库存类数据根本不放 Memcached,所以不受影响。
第四步,版本校验。 读路径比对 version,发现旧值 bypass 回源并覆盖;这样即使删除失败,也不会长期返回旧数据。
第五步,降级。 连续失败时,把该 Key 标记为“缓存不可信”,短时间内直接回源 Redis/DB;同时限流保护 DB,避免缓存故障引发数据库雪崩。
第六步,节点故障处理。 客户端摘除故障节点,一致性哈希重映射;缓存 miss 回源,热点预热恢复。
第七步,观测与对账。 监控删除失败率、脏读率、回源 QPS、P99;关键数据定时比对缓存与 DB,发现差异修复。
核心原则:缓存是加速器,不是真相源。失败时优先保证“可回源、可过期、可发现”,而不是让主流程失败。对商品元数据可以 fail-open,短暂读旧值;对库存则根本不应依赖 Memcached 的更新语义。
面试收尾:三句话总结
- 多 Agent:编排 + 契约 + 状态交接 + 可观测;可靠性靠机制,不靠模型自觉。
- 记忆与上下文:召回两阶段、记忆分层、上下文预算;向量库只是组件,不是记忆系统。
- 秒杀缓存:读链路做漏斗、写链路收敛到 Redis 原子点、DB 最终对账;每一层都要说清“挡掉了什么、允许落后多久、坏了怎么回源”。
高频追问速查
| 追问 | 一句话回答 |
|---|---|
| 多 Agent 为什么不用一个 Agent? | 上下文隔离、能力专精、并行执行、可观测;代价是协调复杂度 |
| 怎么防止 Agent 死循环? | 最大步数、超时、Token 预算、重复检测、人工接管 |
| 记忆召回不准怎么办? | 混合检索 + Rerank + 阈值 + 时间衰减 + Bad case 回流 |
| 上下文超了怎么办? | 分层预算、滚动摘要、按需加载、子 Agent 隔离 |
| Scope Cache 命中率低怎么办? | 撤掉;每层缓存都要有数据证明收益 |
| Memcached 挂了怎么办? | 摘节点、回源 Redis/DB、限流、预热恢复 |
| Redis 挂了怎么办? | 限流、本地缓存兜底、DB 保护、降级排队 |
| 缓存删除失败怎么办? | 重试、MQ 补偿、TTL、版本校验、对账 |
| 怎么防超卖? | Redis Lua 闸门 + DB stock >= 0 + 唯一索引 + 对账 |
| 消息重复消费怎么办? | 幂等键 + 唯一索引 + 状态机 |