【免费下载链接】AI-Meeting
基于 Spring Boot 3 + Java 17 + Spring AI + MySQL + MongoDB + Redis + SSE/WebSocket,实现 AI 对话、智能体会话、AI 模拟面试、实时语音转写、长文本语音合成等核心功能。架构清晰、文档完整,支持本地运行与 Docker 一键部署,非常适合作为 Spring Boot AI 应用开发、智能体后端设计与简历展示项目。
AI-Meeting 是基于 Spring Boot 3 + Java 17 + Spring AI 的开源项目,实现 AI 对话、智能体会话、AI 模拟面试、实时语音转写与长文本语音合成等核心功能。其中一套容易在面试中被追问、却在很多生产系统里真实存在的难题是:服务扩容到多个实例后,同一个用户的同一次请求会被多台机器各调一遍大模型,Token 费用直接翻倍,用户还会看到两份不一致的回答。AI-Meeting 用一套「本地 + Redis 分布式」双层 Single-flight 框架,把「同一次业务请求的重复 AI 调用」折叠成集群内的一次,本文带你完整拆解它的实现思路。
为什么多实例部署会让 AI 费用翻倍?
先看一个典型故障场景:用户点击「提交回答」,请求经负载均衡落到实例 A,AI 评分需要 10~20 秒;用户等不及刷新页面,请求落到实例 B,于是模型被调用了两次。
单实例时代这个问题可以用「本地缓存 + 锁」解决,因为所有请求都在同一个 JVM 里。但多实例部署后,各实例的本地内存互不可见,去重状态必须放到共享存储(通常是 Redis)中,并且抢占动作必须原子化——否则两个实例会同时认为「我是第一个」,重复调用照样发生。
AI-Meeting 的面试链路(提问、评分、追问、神态分析)都是典型的「单次调用耗时数十秒、单次成本高」的 AI 场景,正是这类框架的主战场。
框架总览:双层 Single-flight 架构
核心入口是 InterviewAiInvoker.java,它负责生成「请求指纹」(single-flight key),格式为:
stage|sessionId|questionNumber|answerHash(评分、追问类调用)stage|sessionId|businessKey(文件、神态分析类调用)
key 由「阶段 + 会话 + 题号 + 答案内容的 SHA-256 摘要前 16 位」拼成。这意味着:同一会话、同一题号、内容完全相同的答案,无论落到哪个实例,都会命中同一条 flight 记录,从而天然实现跨实例去重。
在这一层之上,框架分两级工作:
| 层级 | 实现 | 作用范围 | 核心类 |
|---|---|---|---|
| L1 本地 | ConcurrentHashMap+CompletableFuture | 单实例内 | InterviewAiSingleFlightService.java |
| L2 分布式 | Redis Lua 脚本 + 状态机 + 心跳 | 全部实例 | DistributedInterviewAiSingleFlightService.java |
第一层:进程内 Single-flight(毫秒级去重)
本地层实现非常精巧:用flights.compute(key, ...)的原子操作保证「创建 flight 与复用 flight」不会同时发生,避免瞬时并发下出现多个 leader:
- 首个请求(leader):真实调用 AI,结果通过
CompletableFuture.complete()广播给所有等待者; - 后续请求(waiter):直接
future.get()等待同一个结果,超时(默认 5s)后主动剔除旧 flight,防止永久挂起; - 失败清理:leader 异常时移除 key,等待者也收到同样的异常,保证语义一致。
这一层拦截的是「同一实例内的重复点击、前端重试」等最高频的浪费,且几乎零开销。
第二层:Redis 分布式协调(跨实例去重)
跨实例协调的核心在 FlightCoordinatorRepository.java。整个抢占过程由一段Redis Lua 脚本原子执行,这是避免「双主」的关键——任何「先 GET 再 SET」的两次网络往返都可能被两个实例穿插执行,而 Lua 脚本在 Redis 中是单线程原子跑完的。
脚本逻辑对应一个清晰的五路分支状态机:
OWNER_NEW:key 不存在 → 自增ownerToken,写入PENDING元数据,当前实例成为 owner;REPLAY_SUCCESS:状态已是SUCCEEDED→ 直接回放已有结果,一次模型都不调;REPLAY_FAILURE:上次失败且「不可重试」(如参数校验错误)→ 直接抛出相同错误,避免反复重试确定性失败;FOLLOWER_WAIT:owner 心跳仍在takeoverDetectMillis(默认 10s)内 → 当前实例降级为 follower,等待结果;OWNER_TAKEOVER:owner 心跳超时或上次失败可重试 → 新实例生成新 token接管该请求。
owner 拿到抢占权后还要执行markRunning(CAS 校验 ownerId + ownerToken 双身份),确认无误才真正调用模型,进一步杜绝「抢到又丢」的边界问题。
心跳续租与故障接管:长耗时调用的保活机制
AI 调用动辄 20~60 秒,但 Redis 元数据的runningTtlMillis只有 15 秒左右。如果 TTL 一过就被接管,会出现「两个实例同时调用模型」的灾难。
解决方案是心跳续租:FlightHeartbeatManager.java 按 stage 策略(默认 3s)定时向 Redis 续租,每次续租同样用 Lua 脚本 CAS 校验身份。这样设计带来一个优雅的故障语义:
- 实例正常 → 心跳持续续租,TTL 永不到期;
- 实例宕机 → 心跳停发,
takeoverDetectMillis后其他实例自动接管,故障恢复时间 ≈ 心跳间隔 × 3,无需人工干预。
结果回放与 L1 本地缓存:命中后零模型调用
owner 成功后做两件事(见 DistributedInterviewAiSingleFlightService.java):
- 写入结果:由 FlightResultSerializer.java 完成 gzip 压缩 + Base64 编码 + SHA-256 校验和,控制 Redis 存储成本;
- 状态转
SUCCEEDED并通过 FlightNotificationService.java 向 Redis Stream 发布终态事件,让阻塞等待的 follower 立即被唤醒,而非傻等轮询。
follower 的等待采用「Stream 阻塞读 + 定时兜底轮询」双通道:阻塞读最长 3s,期间每 2s 轮询一次结果,既低延迟又不怕通知丢失。此外每个实例还有 L1 本地回放缓存(默认 1000 条、30s TTL),同一实例的第二次命中连 Redis 都不用查。
三种运行模式:local / distributed / hybrid
模式由 FlightMode.java 定义,通过xunzhi-agent.ai-singleflight.mode切换:
| 模式 | 行为 | 适用场景 |
|---|---|---|
local | 仅进程内去重 | 单实例部署、联调 |
distributed | 强制走 Redis 协调,失败即报错 | 对一致性要求高、宁可失败不可重复计费 |
hybrid | 分布式优先,Redis 异常时自动降级本地模式 | 生产推荐(AI-Meeting 默认) |
hybrid模式体现了一个务实的取舍:Redis 短暂抖动时,宁可退回「实例内去重」保住可用性,也不让整个面试功能不可用——去重是「省钱优化」,不应成为功能可用性瓶颈。
关键配置速查表
全部配置集中在 application.yaml,支持按 stage 差异化覆盖:
| 参数 | 默认值 | 含义 |
|---|---|---|
mode | hybrid | 运行模式 |
distributed-enabled | true | 是否启用 Redis 协调 |
follower-max-wait-millis | 20000 | follower 最长等待时间 |
stream-block-timeout-millis | 3000 | Stream 阻塞读单次上限 |
heartbeat-interval-millis | 3000 | 心跳续租间隔(按 stage) |
running-ttl-millis | 15000 | 运行态 TTL(按 stage) |
takeover-detect-millis | 10000 | 心跳多久未更新判定可接管 |
result-ttl-millis | 600000 | 成功结果回放窗口(按 stage) |
failed-result-ttl-millis | 60000 | 失败结果回放窗口 |
compression-threshold-bytes | 4096 | 超过该字节数结果才 gzip |
例如评分场景(interview-evaluation)结果保留 10 分钟、追问(interview-followup)保留 3 分钟、简历提取(interview-extraction)保留 30 分钟——TTL 就是「多久之内同样的请求允许免费回放」,是防重复扣费最直接的旋钮。更多配置语义可参考项目内置说明 ai-singleflight.md 与 ai-guard.md。
调优实践:三个最容易被忽视的坑
- key 设计决定一切:key 过粗(如只含 sessionId)会把不同题的调用误合并,用户拿到错误答案;key 过细(如含时间戳)则完全打不到。记住原则:「什么算同一次业务调用」完全由 key 语义定义,改 key 等于改去重边界。
- follower 等待要设上限:等待过久用户会感知为卡顿。框架把
follower-max-wait-millis控制在 20s 内,超时后 follower 自行发起新调用(此时大概率已命中结果回放)。 - 与限流熔断分层协作:single-flight 只负责「折叠重复」,超时/并发上限/熔断由同目录下的 AI Guard 体系负责(见 AiCallGuardService.java),两者叠加后,同一请求在集群维度上「最多只产生一次真实的模型调用」,重复扣费问题就此闭环。
总结
AI-Meeting 的分布式 Single-flight 框架给出了一套可直接借鉴的防重复扣费方案,其设计精髓可浓缩为五点:
- Lua 脚本原子抢占:从根上消除「双 owner」;
- ownerToken 递增 + CAS 身份校验:每次接管都是全新身份,历史状态不会串扰;
- 心跳续租代替长锁:TTL 短 + 高频续租,宕机自动接管;
- 终态回放窗口(TTL):成功/失败都可在窗口内零成本回放;
- hybrid 降级:共享存储故障时优雅退化为实例内去重。
这套机制不只适用于 AI 面试,任何「高延迟、高单价、幂等可回放」的第三方调用(大模型、支付、短信、OCR)都可以套用同一模式。项目同时支持本地运行与 Docker 一键部署(docker-compose.yml),非常适合拿来动手阅读与二次实践。
【免费下载链接】AI-Meeting
基于 Spring Boot 3 + Java 17 + Spring AI + MySQL + MongoDB + Redis + SSE/WebSocket,实现 AI 对话、智能体会话、AI 模拟面试、实时语音转写、长文本语音合成等核心功能。架构清晰、文档完整,支持本地运行与 Docker 一键部署,非常适合作为 Spring Boot AI 应用开发、智能体后端设计与简历展示项目。
相关推荐
Redis Lua Single-Flight与Fencing Token:FinSight AI如何杜绝重复AI分析和缓存击穿
Redis Lua Single Flight与Fencing Token:FinSight AI如何杜绝重复AI分析和缓存击穿 FinSight AI 是一款
SmartJavaAI微服务架构:分布式AI能力部署
SmartJavaAI微服务架构:分布式AI能力部署 引言:AI能力部署的挑战与机遇 在当今数字化转型浪潮中,人工智能(AI)能力已成为企业核心竞争力。然而,传
人工智能计算机视觉语音NLPOCR本地部署MCSManager 分布式架构深度解析:如何实现多服务器统一管理
MCSManager 分布式架构深度解析:如何实现多服务器统一管理 MCSManager 是一款功能强大的 开源 Minecraft 服务器管理面板 ,专为分布
后端前端运维
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考