主题
时长约 60 分钟 · 9 题 + 反问。首次后端岗全真模拟。总评:项目深挖线依旧稳(乐观锁 9.5、Canal 8.5、记忆召回 8);失分集中在项目外的通用功底——线程池、慢 SQL、容量规划、选型论证,全是「知识储备型」题,对应八股库的补强清单见文末。
一面
1. 自我介绍 — 9.5 分
问题:简单介绍一下自己,两分钟以内。
我的回答:
面试官您好,我叫 XX,杭州某高校计算机技术硕士,27 届。竞赛拿过 ACM、CCPC 省赛银奖,蓝桥杯全国三等奖。技术栈分两块:后端是 Java、Spring Boot、MyBatis、MySQL、Redis、ES、Kafka 这一套;AI 应用这块做过 MCP、Function Calling、ReAct 和 RAG 全链路。实习在某互联网大厂数据中台做了近半年后端研发,负责 MySQL 到 Hive 数据同步链路的落地,包含 Binlog 采集、消息队列和 Spark 计算。核心难点是全量和增量之间有一段天然的重叠窗口,靠批次锚点配合下游主键幂等去重做到不丢不重,数据入湖延迟 P95 稳定在 3 分钟以内。个人做了两个项目。一个是 RAG 知识库:大文件分片上传加断点续传,分片用三方校验做幂等,合并那一步用乐观锁防了并发双合并,1GB 上传从 15 秒优化到 3 秒;检索是向量加 BM25 混合,recall@5 从 63% 提到 85%,回答支持流式和原文溯源。另一个是 CodeAgent CLI,一个终端 AI Agent,里面有一套纯 Java 并发的任务编排:任务拆成带依赖的 DAG,无依赖的分层并行执行,批次之间靠屏障同步保证跨线程可见性。
完整答案:即上(本版已含三方校验、乐观锁、屏障三枚后端钩子),但漏了结尾定位句——「我应聘后端开发岗位,实习和两个项目里练出来的就是消息队列、幂等一致性、并发编排这些后端基本功;AI 应用这块经验,是我能比大多数候选人多走的一步。谢谢。」连续两版都没说,必须背死。
复盘:
- 后端钩子全在,RAG 在前 CodeAgent 在后、顺序正确
- 结尾定位句是「把全场往主场引」的锚,两版连忘,背死
- 关联:自我介绍
2. Canal 原理 — 8.5 分
问题:Canal 怎么拿到 MySQL 变更?伪装成什么、走什么协议?怎么保证不丢?
我的回答:
Canal 是通过把自己伪装成 MySQL 的 Slave 获取 Binlog 信息的。MySQL 主从复制时,从库会向主库发送 dump 协议请求,主库就会把 Binlog 事件流持续推送给从库。所以 Canal 用同样的协议向 MySQL 要 Binlog,收到事件流之后自己解析结构化的变更对象,像库名、表名、事件类型、字段列表、变更前后镜像,再投递到消息队列中。采集到的变更信息通过 Binlog 文件名和位点来保证不丢。
完整答案:
Canal 把自己伪装成 MySQL 的一个 slave。MySQL 主从复制时,从库向主库发 dump 协议请求,带上 Binlog 文件名和位点,主库就从这个位点开始把 Binlog 事件流持续推给从库。Canal 用同样的协议向主库要 Binlog——所以发布流程第 1 步配的 RDS 读权限,本质就是给它开 REPLICATION SLAVE 权限。收到事件流后自己解析成结构化变更对象——库名、表名、事件类型、字段列表、变更前后镜像——再投递到消息队列。
不丢靠位点管理:Canal 记录消费到的 filename + position(或 GTID),每次拉取带位点,重启或断连后从上次位点续传。这也是为什么采集链路要最早建——位点只能往前走,建晚了真空期补不回来。
高可用靠 ZooKeeper 抢占锁:同一时刻只有一个实例消费,实例挂了其他实例抢到锁从位点续传——HA 加位点续传,才把「不丢」兜住。
复盘:
- 协议层讲到了:「对接配置是我做的,原理我掌握到设计级」的口径守住
- 位点只报名词没讲机制:记录 filename+position、重启从上次位点续传——续传动作不说,「不丢」是口号
- 漏 ZK 抢占锁的 HA 层;可白捡一分:第 1 步 RDS 权限 = REPLICATION SLAVE 权限
- 关联:Binlog2Hive链路设计 Q2
3. 线程池参数 — 6 分
问题:批次并行的固定线程池、上限 4 并发——核心参数怎么定的?为什么是 4?超量会怎样?
我的回答:
因为 4 个并发在我的大模型支持的并发个数之内,且能让我的电脑跑满。如果超过 4 个会让其他子线程卡住。批次里的任务数量如果超过了线程池能接的量,就会根据 FIFO 算法把先来的任务先完成,后来的任务后完成。
完整答案:
配置:corePoolSize = maxPoolSize = 4 的固定线程池,配无界 LinkedBlockingQueue。理由从任务性质推:子任务是迷你 ReAct,大部分时间在等 LLM 响应——I/O 密集型,线程大部分时候阻塞,线程数可以远超 CPU 核数;「跑满」的是等待的并行度,不是 CPU。
上限 4 的约束在下游:每个子任务都调 LLM API,并发超过供应商配额不是排队变慢,是直接被限流拒绝——线程数必须卡在配额以内;加上拓扑上同层无依赖任务数天然有限,4 个并发已覆盖典型批次。这个数是「下游配额 + 批次规模」压出来的,不是拍的。
超量行为:固定线程池配无界队列,任务永远不会被拒绝,FIFO 排队、跑完补位;批次用 invokeAll 提交整批并阻塞等全部完成,调度层感知不到排队、只感知到批次变慢。副作用:无界队列任务堆积占内存——这里安全是因为同层任务数天然有限;外部流量驱动场景就得有界队列加拒绝策略(CallerRuns 或上抛),这是同一知识点的另一面。
复盘:
- FIFO 排队的行为大致对,但「让电脑跑满」方向错了——I/O 密集型跑满的是等待并行度,不是 CPU
- 参数没有结构化报出(core=max=4、无界队列);标准执行流程 core→queue→max→reject 没讲
- 「为什么是 4」的真约束说半对:下游 API 限流是对的,要说到「超配额是拒绝不是变慢」
- 补强:线程池八股——参数结构、执行流程、拒绝策略、I/O 密集 vs CPU 密集的定参方法
4. 乐观锁防并发双合并 — 9.5 分(毕业)
问题:乐观锁这条 SQL 具体怎么做的?为什么不用分布式锁?合并到一半服务挂了文件会坏吗?
我的回答:
这个场景里互斥的本质只是「同一时刻只能有一个合并在执行」,一条带状态条件的 UPDATE 就能表达,数据库行级锁天然保证原子性。引入 Redis 或 ZooKeeper 分布式锁,等于为一次性判断增加一个外部依赖和故障面。锁的粒度也只需要到文件这一行,行级条件更新是最贴合的。composeObject 是 MinIO 服务端的原子操作,不存在合并一半的对象,所以「一半」只会出现在清理链上。最棘手的是断在「合并对象已生成、分片元数据已删」之后——重试合并救不回来,因为数量校验永远过不了。这个场景我实际处理过:给合并入口加了清理补偿分支,进合并先检查 MinIO 里完整文件对象是否存在,存在就说明上次合并本体已成功,跳过分片重组直接补做清理。有两个实施细节:一是这个检查要放在分片数量复核之前,否则会被「分片未全部上传」的闸拦住;二是补偿不能重放副作用,文件如果已完成向量化就跳过预估和 Kafka 分发,否则 ES 分块翻倍、token 重复计费。至于进程崩溃导致状态卡在「合并中」的情况,它天然是互斥闸门,挡住所有新写入;人工把状态复位后再触发合并,现在会走补偿分支正常收敛——以前复位了也救不回来,这正是补偿分支补上的洞。
完整答案:在「我的回答」基础上补一句 SQL 形态即可——UPDATE 文件表 SET status='合并中' WHERE id=? AND status='上传中',影响行数为 1 才有资格合并,为 0 则重查状态按幂等成功或冲突退出。
复盘:
- 三场以来最佳回答:互斥本质、依赖面论证、原子性定位「一半」到清理链、补偿分支 + 两个实施细节、前后对比收尾——钩子被钻到底的标准答案
- 唯一半句:SQL 的具体形态和影响行数判断
- 关联:文件上传模块「为什么用乐观锁」「合并到一半服务挂了」
5. 配额计费 — 5 分
问题:额度账本怎么设计?为什么「预扣-结算」两阶段,调完再扣不行吗?
我的回答:
我是以启发式的方法进行预估的,以英文字符占 4 字符、中文字符占 1.5 字符进行估算的,每个人刚开始注册有一定的 token 额度。
完整答案:
账本是双额度:LLM 和 Embedding 分开记——成本单价和用量模式完全不同,混一个池子既看不清成本也没法分别限流。新用户注册送初始额度,叠加充值套餐。
计量是预扣-结算两阶段。调用前:按请求文本量启发式估算 token——1 个 token 约合 1.5 个中文字符、4 个英文字符——和实时余额比对,不够直接拦在调用之前,HTTP 都不发。调用后:按 API 返回的真实用量结算差额,多退少补。
为什么不能调完再扣:调用的成本是对供应商的债务,请求发出去债务就发生了——余额为零的用户照样能发起调用,扣无可扣就是超支。预扣的本质是把支付能力检查放在债务发生之前。而且最终按真实用量结算,启发式的误差不会记错账——它只决定「拦不拦」,账本本身是准的。敢用启发式不用精确 tokenizer,就是这个组合给的底气:零依赖、O(n) 扫描,误差被「预扣从宽、结算以实」消化。
再抠一层:system prompt 里带出去的每个工具定义都计入配额——Agent 的每一分成本都可计量。
复盘:
- 启发式数字对(表述顺过来:1 token ≈ 1.5 中文 / 4 英文字符)
- 骨架三样全缺:双额度账本、预扣-结算两阶段、「债务先于支付能力检查」的为什么
- 关联:文件上传模块「两阶段配额」
6. 存储选型:为什么 ES 不换向量库 — 5.5 分
问题:向量和文本、权限全在 ES 一个索引里——为什么不拆 FAISS/Milvus?什么时候真会拆?
我的回答:
ES 既可以进行向量检索也可以关键词检索,这套方案比专门的向量库好,可以进行二者混合搜索,效率也还行,等到召回的信息达不到要求时才会考虑专门拆一套专门的向量库进行维护。
完整答案:
不是「ES 比专门的向量库好」——论纯向量性能 FAISS、Milvus 都比它强。是当前规模和需求下,ES 更合适,三条理由:
一,规模远没到:全库向量千级,ES 的 HNSW 毫秒级延迟绰绰有余,FAISS 的优势——亿级向量、内存索引——没有发挥空间。二,换过去权限过滤会倒退,这条最关键:ES 的 kNN 是 filter 感知的,图遍历时带上用户和组织标签条件,越权文档根本不进候选集;FAISS 是纯向量索引,换过去权限只能先召回再应用层筛——topN 里越权的占多数时有效结果变少甚至为空,还得扩大 N 重查,正确性和延迟都变差。三,双存储一致性成本:向量另存一份就要补重建、增量同步、故障恢复一整套逻辑,为当前不存在的性能问题买单。
什么时候拆:触发条件是规模和延迟,不是效果——千万级向量、ES 延迟不达标时。而且第一步不是换,是抽象 VectorStore 接口让两种实现并存,优先选支持标量过滤的向量库(Milvus 这类)而不是裸 FAISS——把「换」变成有过渡方案的演进,不是推倒重来。
复盘:
- 「比专门的向量库好」是危险措辞——会被「FAISS 亿级毫秒返回,你好在哪」当场打脸;正确框架:「不是更好,是当前规模下更合适」
- 最强论据(权限过滤倒退)一条没讲;演进触发条件答错方向(效果问题 ≠ 规模问题)
- 关联:知识库检索模块「为什么不用 FAISS」
7. 长期记忆召回 — 8 分
问题:一条长期记忆怎么被想起来?打分怎么打?为什么不用向量检索?防膨胀怎么做?
我的回答:
长期记忆在启动时自动加载到系统提示词里,功能类似 Claude Code 的 CLAUDE.md。打分是通过两个因子相乘得来的:一个是关键词命中率,另一个是时间衰减。查询先经 jieba 分词,整句被记忆包含直接给满分,否则算命中词占比;时间衰减是 24 小时内从线性 1.0 降到 0.5,最后兜底 0.5,保证老记忆完全不会沉底;长期记忆再乘以 1.2,因为他比短期对话更精炼可信。因为长期记忆存的是精炼的事实,不是大段文本,数据量小,用关键词匹配已经够了。防膨胀主要是在入口做的:只有显式保存能进长期记忆,重复内容存不进去,会进行去重,所以条目数天然保持在几十到几百的量级。
完整答案:
先分两层。静态记忆是 CODEAGENT.md——项目规范、构建命令,启动时注入 system prompt,跟 Git 走、团队共享;动态是长期事实库——JSON 文件存盘,启动时全量加载进内存,用的时候按相关度召回 top 几条才注入。
召回打分两因子相乘:关键词命中率 × 时间衰减。查询先 jieba 分词,整句被记忆包含直接满分,否则算命中词占比;衰减 24 小时内从 1.0 线性降到 0.5、之后兜底 0.5,老记忆不至于完全沉底;长期记忆整体再乘 1.2——比短期对话更精炼可信。排序取 top 10,按 token 预算截断注入。
为什么不用向量检索:一是数据形态——存的是精炼事实不是大段文本,几十到几百条,关键词打分够用;二是访问模式——高频读写、要求即时一致,说「记一下」下一轮就得能召回,同步写盘加内存打分零延迟,向量库的 embedding 和索引更新延迟为一个几百条的场景不值得。
防膨胀靠入口不靠配额:存储入口只有两个——对话里说「记一下」触发 save_memory,或 /save 命令;extractFacts 写过但故意不接主流程——自动提取会把临时任务当稳定事实,污染记忆库。写入时精确去重,content 完全相等直接跳过。入口收窄加去重,条目数天然停在几十到几百。
复盘:
- 打分因子全对
- 开头静态/动态混了:长期记忆启动加载进的是内存,注入 system prompt 的是召回 top 几条;启动注入的是 CODEAGENT.md——混了会被「全塞进窗口撑得住吗」追问打穿
- 不用向量检索漏了访问模式半条(高频读写 + 即时一致性);可补 extractFacts 故意不接主流程的设计决策
- 关联:Memory系统
8. 场景题:对话量涨十倍 — 4.5 分
问题:用户量和对话量涨十倍,架构先动哪里?按什么顺序?什么不用急?
我的回答:
架构上我会先评估 ES 作为向量库是否足够,看 ES 延迟是否达标,如果不达标将引入 Milvus 向量库,然后抽象接口使两种实现方式并存。然后再考虑加机器。
完整答案:
瓶颈排序:LLM 外部依赖 > 有状态连接层 > 应用实例 > 中间件 > ES/MySQL。
先动 LLM 依赖:对话量十倍 = 调用十倍,供应商配额、成本、延迟全部顶到头——谈配额或启用多供应商热切换(这个设计本来就有,密钥加密落库不重启),限流按配额比例收紧,成本预算同步上移。全链路最贵、天花板最低的资源,加机器解决不了。
再动有状态层:WebSocket 长连接涨十倍,连接注册表是单机内存模型——加实例后用户和连接不在一台机器,推送丢;改成用户到连接集合的广播模型,或前置粘性会话,生成线程池按实例数重算。
然后应用无状态水平扩容,挂负载均衡——最便宜的一步,但被上一步卡着。中间件按水位扩:Kafka 加分区看分区键有序性,ES 加副本扛读放大。不用急的恰恰是 ES 换 Milvus——千级向量毫秒延迟,十倍流量还是毫秒级;千万级向量、延迟不达标才触发换型,这是自己定的阈值。
一句话:十倍流量下最先死的不是中间件,是 LLM 配额和有状态层;加机器最便宜,也最该最后动。
复盘:
- 本场最弱,且暴露一个习惯性错误:把上一题的答案(ES 换 Milvus)直接套到这一题——两题瓶颈不同。容量思维的标准打法:先量化水位 → 排瓶颈(外部依赖 > 有状态 > 无状态 > 中间件)→ 加机器最后动
- 「看起来该扩其实不用急」的子问题完全没答
- 关联:聊天助手模块「多轮并发隔离」「供应商热切换」
9. 慢 SQL 调优 — 5.5 分
问题:给你一条线上慢 SQL,从发现到解决的完整流程?
我的回答:
首先通过 Explain 命令查询这条 SQL:首先看 type,判断当前查询是通过索引扫描还是全表 ALL——如果是索引扫描,大数据下仍有可能存在性能风险。接着看 key,如果为 null 则说明未用到索引。然后看 Extra,这列包含了 MySQL 解析查询的额外信息,通过这些信息可以更准确地理解 MySQL 到底是如何执行查询的。最后根据不同的问题进行不同的优化方案。
完整答案:
四步:发现 → 定位 → 优化 → 验证。
发现:慢查询日志(long_query_time)或监控抓 Top SQL,按执行频次 × 单次耗时排优先级——优化一条每天十万次的 200ms,比优化一条每天一次的 2 秒收益大。
定位:explain 看四个关键字段。type 访问层级,const/eq_ref/ref/range/index/ALL 依次变差,线上查询至少 range 级;key 与 possible_keys 对照,实际用没用索引;rows 预估扫描行数——和真实行数偏差大就是统计信息过期或区分度差;Extra 看 Using filesort(排序没走索引)、Using temporary(临时表)、Using index(覆盖索引,好信号)。
优化三板斧:先索引——联合索引按最左前缀建、区分度高的列在前、覆盖索引避免回表;再改写——条件列不套函数、杜绝隐式类型转换、like 不前导 %、select 不用 *、深分页改延迟关联或游标;最后才动架构——读写分离、分库分表,别一上来就动。
验证:explain 前后对比 type 和 rows,线上看耗时和 CPU——优化必须有前后数字。
经验收尾:大部分慢 SQL 的根因是「数据量涨了、索引没跟上」——先怀疑索引,再怀疑写法,最后才怀疑架构。
复盘:
- explain 骨架有(type/key/Extra),但漏了 rows——最有信息量的字段
- 「从发现到解决」从 explain 开始,漏了发现环节(慢日志 + 频次×耗时排优先级)
- 「不同的优化方案」是空话,要报得出工具箱:索引三板斧 + 改写清单 + 架构兜底
- 无验证闭环:前后数字对比
- 关联:面渣逆袭 MySQL 索引与优化页
反问(备选,已备好两条)
- 技术向(团队负责人时优先):「想了解团队后端的服务现状——MySQL、ES、Kafka 这些中间件是自建还是云托管?日常的慢查询治理和容量规划是怎么分工的?」
- 成长向(任何面试官都安全):「校招生进来一般从哪类模块入手?导师制怎么运作,大概多久能独立负责一条业务线?」
总复盘
得分曲线:9.5 / 8.5 / 6 / 9.5 / 5 / 5.5 / 8 / 4.5 / 5.5,均分 6.9。
关键发现——两场对比的规律:凡是有深挖笔记托底的题(乐观锁 9.5、Canal 8.5、记忆召回 8、上场的实习三题),全部 8 分以上;失分全部集中在「项目外的通用功底」:线程池标准流程(6)、配额计费设计(5)、选型论证措辞(5.5)、容量规划思维(4.5)、慢 SQL 方法论(5.5)。结论:项目内深度已达标,短板是八股库的成体系程度。
行动项:
- [ ] 线程池八股:参数结构化表达、core→queue→max→reject 执行流程、I/O 密集定参法
- [ ] 慢 SQL 四步流程背熟:发现(频次×耗时)→ explain 四字段(type/key/rows/Extra)→ 三板斧 → 验证
- [ ] 容量思维模板:瓶颈排序「外部依赖 > 有状态 > 无状态 > 中间件」,禁止拿上一题答案套下一题
- [ ] 选型题措辞纪律:「不是更好,是当前规模下更合适」
- [ ] 自我介绍结尾定位句背死——连续两版忘说