InterviewGuide
Snailclimb/interview-guide项目目录
项目简介
InterviewGuide 是基于 Java、Spring Boot 与 Spring AI 的面试训练平台,提供简历分析、模拟面试、知识库练习和评估报告。简历分析以 Redis Stream 异步执行,通过数据库状态和执行代次协调领取、恢复与结果保存。
如何写进简历
面伴 · 校园求职训练平台 | 场景化示例
项目描述:基于 InterviewGuide,面向应届生与就业指导老师提供简历分析、模拟面试及训练记录,跟踪多轮修改与薄弱项。
技术栈:Java、Spring Boot、Spring AI、PostgreSQL、Redis
主要工作:
- 异步分析与中断恢复:基于
Redis Streams消费组接入简历分析,以数据库保存任务状态,结合待确认消息补领和超时巡检恢复中断任务,使上传请求与耗时模型分析解耦。 - 旧执行结果隔离:利用状态条件更新和
attemptId约束任务领取、阶段心跳与完成操作;结果保存时锁行复核执行代次,拦截超时重领后旧消费者的写回。 - 结构化评价与解析重试:基于 Java
record和Schema接入结构化评分与改进建议,通过统一调用器处理格式校验和有限重试,将模型输出转为可保存、可展示的分析记录。
面试题与回答
用 Redis 作消息队列时,如何处理宕机导致的消息丢失?
展开收起
核心回答
InterviewGuide用Java和Spring实现简历分析等异步任务,Redis Stream负责投递,数据库保存正文、结果与任务状态。以简历分析为例,先保存PENDING任务,再发送包含resumeId和retryCount的消息;消费者先领取数据库任务,执行分析、保存结果并更新完成状态,最后确认消息。恢复有两层:Stream 层主动补领未确认消息,数据库层扫描长期卡住的任务并补投。 前者解决消息交给某个消费者后无人继续处理的问题,后者为消息丢失、裁剪或业务状态停滞提供恢复依据。它允许重投,依靠业务状态约束重复执行,并不承诺 Redis 永不丢消息。
未确认消息怎样重新被消费
消费者启动时准备消费组,每个实例使用独立消费者名称。循环先调用
autoClaim(),按游标补领空闲超过 5 分钟的待处理消息,每批最多 10 条;本轮没有补领结果时,再阻塞读取该组尚未投递的新消息。由此,原消费者宕机后,恢复并不依赖它以原名称重新上线。具体顺序见补领与新消息读取。补领成功只表示获得消息,还要通过数据库的
PENDING→PROCESSING条件更新领取业务任务。已完成或无法领取的任务会确认消息并跳过。若旧任务仍停在PROCESSING,补领消息不会直接使它重新执行,需要下一层数据库恢复将过期状态重置后补投。PEL 状态和业务任务状态是两套记录,不能混为一条状态机。业务失败、重试与 ACK 的顺序
公共消费者的正常路径是:领取任务、执行业务、更新完成状态、
ACK。异常路径会检查消息里的retryCount:小于 3 时重新投递,达到上限后标记失败,再确认旧消息。这里最多是首次执行加 3 次消息重试,业务抛出的异常才会进入这条路径。简历分析重投前先条件重置为
PENDING,新消息写入增加后的重试次数;重投入队失败时,把仍待处理的任务标为FAILED。格式错误消息会直接确认丢弃,ACK 失败则记录日志,后续仍可能再次被补领。见消费确认与异常处理及简历分析重投。当前超过重试上限的处理是数据库
FAILED终态与手动重试,没有独立死信 Stream。若增加死信能力,可以保存原消息、业务标识、异常原因和重试历史,完成持久转存后再确认原消息。另一方面,分析服务会把部分模型调用异常转换为错误报告返回,队列可能仍按正常结果保存并完成;消息成功处理与模型分析结果有效也要分别判断。队列记录丢了,数据库怎样兜底
上传流程先提交简历记录,再发送消息;发送失败会保留文件与记录,并把待处理任务标记失败,供用户重试。这是分步写入,未使用覆盖数据库和
Redis的原子事务或事务发件箱。数据库恢复调度器默认每分钟分别扫描过期的
PENDING和PROCESSING:前者默认超过 10 分钟尝试补投;后者默认 15 分钟无进展时条件重置,再补投。恢复有次数上限,避免无限循环,具体见卡住任务恢复调度。阈值需要覆盖排队和实际业务耗时,否则会误判仍在执行的任务。消息发送使用近似
MAXLEN=1000裁剪,未确认消息也可能先被裁掉;补领发现这类 ID 时只记录警告日志,无法恢复已裁剪的消息体。因此数据库任务和已保存的简历正文很关键,但仍需要核对恢复是否成功、失败任务是否有人处理。要进一步提高可靠性,应结合部署环境配置持久化与复制、按积压调整保留策略,并验证“入队前宕机、领取后宕机、结果保存后 ACK 前宕机”三个窗口,而不能只测试正常消费。租约失效后为什么还需要 Fencing Token,如何防止旧 Worker 写回?
展开收起
核心回答
InterviewGuide的简历分析任务通过数据库中的状态和随机attemptId识别当前执行者。消费者原子地把任务从PENDING改成PROCESSING,并记录本次尝试;超时恢复或重试会清除旧尝试,新消费者重新领取时生成新的标识。心跳、完成、失败和重试重置均检查当前尝试,分析结果保存也在数据库事务中验证所有权。这里采用的是当前尝试的等值校验,能阻止失去执行权的旧消费者写入分析结果或改动新尝试的状态。
attemptId是随机UUID,并不是面向外部资源的单调递增Fencing Token;它也无法撤销已经发给模型的分析请求。怎样领取与恢复任务
AnalyzeStreamConsumer.tryMarkProcessing()生成UUID,调用tryMarkAnalyzeProcessing()。只有数据库中仍为PENDING的任务才能更新成功,受影响行数为1时才继续分析;重复消息或其他消费者不会仅凭消息到达就取得业务执行权。消费者领取、条件领取。心跳通过
PROCESSING + analyzeAttemptId条件更新进展时间,更新失败就抛出执行权失效异常。当前简历分析链在下载解析完成、模型分析返回后报告心跳,并非持续运行独立心跳线程。长调用可能超过无进展阈值,因此阈值需要考虑调用和重试的总耗时。业务阶段与心跳、恢复配置说明。恢复调度器扫描无进展任务,以状态和时间条件将过期的
PROCESSING重置为PENDING、清除旧attemptId,再补投消息。旧消费者仍可能继续计算,但它持有的旧标识已经无法匹配当前数据库状态。恢复调度、恢复条件更新。为什么保存前再检查仍然有用
仅靠模型返回后的心跳仍不够:消费者可能刚通过心跳检查,就再次暂停。
saveAnalysisIfOwned()会在事务内通过findByIdForUpdate()锁住简历行,重新检查状态和attemptId,通过后才保存分析结果。这次校验与结果落库受同一个事务保护,恢复调度器不能在二者之间替换任务所有权。锁行查询、事务内验证与结果保存。完成、失败以及重试重置也带尝试条件。旧消费者失权后,它的重试重置更新不到当前任务,就不会重新投递旧尝试;旧的完成或失败回调也不会覆盖新尝试。条件状态更新、重试处理。
分析结果保存与随后标记
COMPLETED是两个独立事务,不能把这条链描述为“结果、终态和消息确认一次原子提交”。它保护的是相应数据库写回;模型可能被重复调用,端到端也不具备外部副作用只发生一次的保证。消费执行顺序。怎样验证旧结果确实被隔离
可以让尝试 A 停在模型调用中,触发恢复并让 B 取得新
attemptId,再释放 A,检查 A 的心跳与结果写回被拒绝;还要覆盖“A 心跳成功后暂停、B 接管、A 再保存”的时序,以验证最终事务检查。分别核对分析记录、任务状态和外部模型调用次数,才能区分旧结果隔离与重复计算。简历评分结果如何结构化输出?
展开收起
核心回答
InterviewGuide以 Javarecord描述简历分析结果,用BeanOutputConverter生成格式要求,再通过统一的StructuredOutputInvoker调用与解析模型响应。结果包括总分、五项评分、摘要、优点及建议,最终保存为分析记录。这一实现解决了结构交接和重试复用,但仍要单独看分值约束与失败语义:当前 DTO 没有表达评分区间,服务外层还可能把失败转换为零分报告,因此结构能保存不等于分析有效。
DTO 和 Prompt 怎样配合?
内部 DTO 定义
overallScore、scoreDetail、summary、strengths、suggestions;建议包含类别、优先级、问题和推荐修改。服务把outputConverter.getFormat()追加到系统提示词,再将简历正文填入用户模板。当前分析方法接收的是简历正文,没有显式目标岗位参数。DTO 与格式构造。系统模板要求项目、技能、内容、结构、表达五项贡献分分别为
40/20/15/15/10,合计 100。对外响应类的部分注释仍是旧的分项上限,不能将注释直接当成当前执行规则;这些标准需要统一维护。实际评分模板、响应字段注释。转换失败怎样重试?
统一调用器默认开启 Schema 校验,通过
call.entity(outputConverter, spec -> spec.validateSchema())获取对象。关闭校验的兼容分支才采用文本转换,并尝试修复字符串中的未转义引号;这不是任意 JSON 错误都能修好的通用修复器。两条解析分支。外层调用器按配置重试,后续尝试可增加严格 JSON 要求和上一轮错误,默认外层最多尝试两次。Schema 校验和 SDK 自身可能还有内部调用,所以两次不能直接当成全部 HTTP 请求的上限。重试封装、默认配置。
为什么还需要业务校验和独立失败状态?
评分 DTO 的分数字段只有
int,没有最小值、最大值或分项合计约束;转换后也没有重新计算总分。生成的对象即使通过现有结构校验,仍可能分值越界或总分不一致。完善时应让分项标准进入可执行约束,再检查总分与评分依据。此外,
ResumeGradingService外层捕获异常后返回一份零分错误报告。只要当前消费者仍拥有执行权,它就可以按普通分析结果保存,并随后完成任务。因此不能把任务COMPLETED或一条零分分析记录直接解释为评价成功。错误报告回退、结果保存入口。执行代次校验能阻止旧消费者写入,却不会判断报告内容有效。进一步完善时,可把分析调用失败、报告校验失败与正常评分分开,避免用户将系统错误理解为自己的简历得了零分;再用缺字段、越界分数、总分冲突和服务不可用样例分别验证。