长会话状态治理(上):问题分析、存储分层与恢复机制

发布时间:2026/6/13 1:06:13

长会话状态治理(上):问题分析、存储分层与恢复机制 长会话状态治理上问题分析、存储分层与恢复机制系列导航本文是长会话状态治理系列的上篇聚焦于为什么会丢状态以及Redis 丢了怎么恢复。下篇将讲解数据更新机制、轮次归档、并发保护与可复用设计原则见 长会话状态治理下。一、在很多 AI 驱动的业务系统中用户的操作不再是发一条请求、收一条响应就结束的短交互而是会进入一段持续数分钟甚至数十分钟的在线过程型会话。以 AI 模拟面试为例一场面试过程中系统需要持续跟踪并维护当前进行到第几题是否在追问已经追问了几轮最近几轮的问答内容与评分结果当前流程节点处于出题中、作答中还是评分中当前总分和各维度聚合分简历分、神态分、答题分当前请求是否已经处理过幂等保护会话是否已经进入终态已完成、已关闭这些信息共同组成了所谓的“长会话运行态”Long Session Runtime State。只有这份运行态是连续、准确、可读取的后端才知道下一步该继续问哪一道题、该不该触发追问、该不该累计分数、该不该直接回放历史结果而不会把会话推进错位。本文从一个真实的 AI 面试项目出发系统性地讲解长会话状态治理中的核心问题和恢复机制的工程实现。虽然场景以面试为例但文中提炼的分层存储、懒恢复、Owner-Follower 协作等原则适用于任何需要维护长会话运行态的系统——在线客服、协同编辑、游戏对局、在线考试等。二、问题背景长会话为什么容易丢状态2.1 短请求 vs 长会话短请求系统的状态管理非常简单请求进来、计算一次、返回结果、请求结束。这类系统即使中间某个缓存失效最多重新查一次数据库问题通常不会太大。但长会话不是这种模式。它的核心特征可以总结为六个跨特征说明跨请求一次会话会被多个 API 请求反复访问和修改跨时间会话持续十几分钟甚至更久中间状态不断演化跨节点多实例部署下不同请求可能打到不同机器高频变化每轮答题都会修改 flow、score、turns 等多个维度强状态依赖下一步行为完全取决于当前状态该问哪题、该不该追问强时序敏感状态变化有严格的先后顺序不能回退、不能跳跃一旦会话被拉长状态就不再是一次请求的临时变量而变成一份需要跨请求保存、跨时间延续、跨节点共享的运行上下文。状态本身就会变得脆弱——你不再只是防一个请求失败而是在防半小时内的缓存过期、某次请求写成功但补充刷新失败、某台实例抖动导致局部状态没写完、并发请求把彼此状态覆盖掉、会话恢复时读到了不完整的中间结果。2.2 丢状态到底在丢什么很多人听到丢状态第一反应是数据库里的数据被删了Redis 整个挂了系统把历史记录弄没了但在长会话场景里丢状态往往不是所有数据都没了而是系统失去了继续推进当前会话所需的上下文。比如下面这些信息只要缺了一部分系统就可能已经没法安全往下走当前进行到第几题 →流程状态缺失当前题是不是追问题 →追问上下文缺失最近一轮答题是否已经提交成功 →幂等状态缺失当前累计分数和维度分是否已经聚合 →分数状态缺失最近几轮对话记录是什么 →上下文窗口缺失会话是否已经终态 →生命周期状态缺失用一句话概括状态丢失的本质不是存储里一条数据没了而是系统已经无法准确判断这个会话现在进行到哪里、下一步该怎么走、还能不能继续写。2.3 一个典型的丢状态时间线为了更直观地理解这个问题来看一个真实的危险时间线时间线 T1 用户正在回答第 5 题 → 请求进入答题 Pipeline先走幂等校验 → 幂等层检查 replay key无命中再用 processing key 抢占正在处理 T2 系统确认运行态可写 → ensureRuntime() 确认 Redis 运行态就绪 → 题级锁保证同一题不会并发推进 T3 评估链路计算分数和反馈 → 结果仍然主要停留在内存和 Redis 中尚未落主库 T4 Redis 运行态先推进 → flow 状态机推进到下一题 → 分数通过 addSessionScore() 写入 Redis 聚合值 → 如果分数提交失败代码会把 flow 回滚到推进前 T5 本轮 turn log 追加到 Redis → 成功后追加失败则进入异步修复逻辑 T6 请求标记为成功触发检查点刷新 → 幂等 replay key 写入 → refreshAfterAnswerCommitted() 触发热快照/归档刷新 T7 ⚠️ 风险窗口出现 → Redis 运行态已经前进flow 已到第 6 题 → 但快照/归档刷新还没完成 → 此时如果 Redis key 过期、丢失或被异常覆盖... T8 新请求进来 → 发现 Redis 里什么都没有 → 系统只能尝试重建 → 但最新这一轮还没成功沉淀成检查点 → 结果题号不完整、turn log 不完整、replay 不完整 T9 用户视角 → 我明明答完了为什么系统又像没答一样这个时间线揭示了长会话状态丢失最常见的根因运行态已变检查点未稳。Redis 作为在线态跑得快但持久层作为恢复材料跟不上。一旦 Redis 在这个窗口内丢失系统就只能面对知道会话存在但不知道进行到哪的尴尬。2.4 为什么只靠 Redis 不够把运行态放在 Redis 里当然是合理的——高性能、低延迟、天然适合高频读写。但 Redis 本质上是缓存层它存在几个天然风险风险类型表现后果Key 过期TTL 到期后自动清除业务无感知运行态静默丢失实例抖动主从切换、哨兵选举导致短暂不可用写入丢失或读到旧值局部丢失某次写入成功但后续刷新失败运行态与持久层不一致并发覆盖多个请求对同一个 key 做并发写后写覆盖前写状态回退重启丢失Redis 宕机后内存数据全部清空所有会话的运行态瞬间归零所以真正成熟的方案不是幻想 Redis 永不失手而是承认运行态会缺失并提前设计好恢复入口、恢复依据和恢复边界。三、核心设计思路两条主线构建状态治理闭环3.1 全局架构这套方案的核心可以压缩成一句话更新机制负责生产恢复材料恢复机制负责消费恢复材料。从全局来看整个状态治理体系由两条主线构成主线核心职责核心入口方向恢复机制当 Redis 运行态缺失时从检查点恢复会话状态ensureRuntime(...)消费恢复材料数据更新机制当业务成功推进后把最新状态安全写成可恢复检查点refreshSnapshot(...) Patch CAS生产恢复材料两者共同组成一个完整的闭环业务推进成功 → 更新机制写入检查点Hot Snapshot / Cold Snapshot / Turn Archive → Redis 继续承担在线态 → 如果 Redis 丢了 → 恢复机制读取检查点并回填 Redis → 业务继续推进 → 再由更新机制写入新的检查点 → ...循环往复直到会话结束也就是说没有更新机制恢复机制没有材料可用没有恢复机制更新机制写下来的材料没有真正发挥线上兜底价值关于数据更新机制的详细实现将在下篇中深入讲解。3.2 核心代码落点一览在深入讲解恢复机制之前先列出关键的代码文件和它们的职责文件职责InterviewCacheKeys.java定义 Redis 在线运行态的 Key 体系InterviewSessionRuntimeRehydrateService.java恢复机制核心入口提供ensureRuntime(...)InterviewSessionRuntimeSnapshotService.java更新机制核心提供快照刷新、归档和幂等回放InterviewSessionRuntimeHotRefreshCoordinator.java热刷新防抖与聚合协调器InterviewSessionRuntimeHotSnapshot.java热快照 Mongo 实体高频检查点InterviewSessionRuntimeColdSnapshot.java冷快照 Mongo 实体低频材料层InterviewSessionTurnArchive.java轮次归档 Mongo 实体全量历史回放InterviewSessionRuntimeHotSnapshotRepositoryImpl.java热快照 Patch CAS 的 Mongo 实现InterviewSessionRuntimeColdSnapshotRepositoryImpl.java冷快照字段级 Patch 的 Mongo 实现InterviewSessionRuntimeView.java恢复结果的统一视图对象InterviewSessionRuntimeLockService.java恢复过程的分布式互斥锁InterviewFlowState.java面试流程状态模型InterviewRuntimeConfidence.java恢复置信度枚举InterviewRuntimeRehydrateScope.java恢复范围枚举四、存储分层设计什么放 Redis什么放持久层4.1 三层存储架构┌──────────────────────────────────────────────────────────────────┐ │ Redis在线高频运行态 │ │ │ │ Flow State Hash Turns List Score Aggregate (sum/count) │ │ Questions Hash Suggestions Resume Context / Score │ │ Demeanor Score Follow-up Qs Request ID Sets │ ├──────────────────────────────────────────────────────────────────┤ │ Hot Snapshot热快照 — 高频检查点 · MongoDB │ │ │ │ flow / scoreAggregate / recentTurns / followUpQuestions │ │ archiveWatermark / lastTurnSeq / lastMutationId │ │ snapshotVersion / snapshotLevel / rebuildConfidence │ │ lastCommittedQuestionNumber / lastCommittedTurnDigest │ ├──────────────────────────────────────────────────────────────────┤ │ Cold Snapshot冷快照 — 低频材料层 · MongoDB │ │ │ │ questions / suggestions / resumeContext / direction │ │ resumeScore / demeanorScore / demeanorDetails │ │ materialVersion / interviewType / resumeFileUrl │ ├──────────────────────────────────────────────────────────────────┤ │ Turn Archive轮次归档 — 全量历史回放 · MongoDB │ │ │ │ sessionId seq(单调递增) requestId turnPayload │ │ snapshotVersion createdAt │ └──────────────────────────────────────────────────────────────────┘4.2 分层设计原则热冷分层的核心依据是更新频率热快照Hot Snapshot对应 Mongo 集合interview_session_runtime_hot_snapshot。每轮答题提交后都可能变化保存的是当前进行到第几题、累计分多少、最近几轮对话是什么、最后一次成功的 requestId 是什么。它是恢复机制最优先依赖的检查点。热快照实体中几个关键字段的设计意图字段类型用途snapshotVersionLong版本号每次刷新递增用于 CAS 并发保护snapshotLevelString业务阶段标记DRAFT → QUESTION_READY → ACTIVE → FINALIZEDrebuildConfidenceEnum恢复后的置信度EXACT / DERIVED / READ_ONLY / TERMINALflowObject当前流程状态进行到第几题、是否追问、是否已完成scoreAggregateObject得分聚合累计分、计分次数、会话总分recentTurnsList最近 N 轮问答窗口默认上限 20 轮用于快速恢复上下文archiveWatermarkLong归档水位热快照已同步覆盖到第几条 Turn ArchivelastTurnSeqLong最近归档轮次的顺序号用于单调性保护lastMutationIdString最后一次成功变更的 requestId用于幂等判定lastCommittedQuestionNumberString最后一次成功提交的题号lastCommittedTurnDigestString最后一次成功提交的题号答案的 SHA256 摘要兜底幂等冷快照Cold Snapshot对应 Mongo 集合interview_session_runtime_cold_snapshot。通常在出题完成、简历解析完成、神态评分完成等关键节点才变化保存的是题目列表、简历上下文、面试方向等低频材料。它在热快照数据不够用时提供材料补充。轮次归档Turn Archive对应 Mongo 集合interview_session_turn_archive。每成功提交一轮答题就追加一条永不修改。用于全量历史回放和软回放幂等。热快照和冷快照分开的好处每轮答题后只需要 CAS 更新热快照不需要把整包数据包括大量低频材料重写一遍。这直接降低了写放大和并发冲突概率。4.3 Redis Key 体系在线运行态的 Key 按维度独立管理每个 session 对应一组 Keyinterview:questions:session:{sessionId} → Hash题号 → 题目内容 interview:flow:session:{sessionId} → Hash流程状态 interview:turns:session:{sessionId} → List轮次日志 JSON 序列 interview:score:session:{sessionId} → Value会话总分 interview:score_sum:session:{sessionId} → Value累计分 interview:score_count:session:{sessionId} → Value计分次数 interview:suggestions:session:{sessionId} → Hash题号 → 建议内容 interview:resume_context:session:{sessionId} → Value简历上下文 JSON interview:resume_score:session:{sessionId} → Value简历评分 interview:direction:session:{sessionId} → Value面试方向 interview:demeanor_score:session:{sessionId} → Value神态评分 interview:demeanor_panic:session:{sessionId} → Value神态-紧张度 interview:demeanor_seriousness:session:{sessionId} → Value神态-严肃度 interview:demeanor_emoticon:session:{sessionId} → Value神态-表情处理 interview:demeanor_composite:session:{sessionId} → Value神态-综合分 interview:follow_up_questions:session:{sessionId} → Hash追问题集合 interview:answer:req:session:{sessionId} → Set已处理的答题请求 ID interview:turn:req:session:{sessionId} → Set已处理的轮次请求 ID这种按维度独立 Key 的设计使得恢复机制可以按需恢复——只恢复当前 scope 需要的维度而不必一次性把所有数据都加载出来。既减少了不必要的 IO也降低了恢复失败的概率。所有 Key 统一由InterviewCacheKeys工具类管理以 session ID 作为维度标识TTL 统一设置为 24 小时。五、恢复机制详解当 Redis 丢了怎么把状态找回来5.1 核心目标恢复机制的核心目标只有一个无论 Redis 里有没有数据保证当前请求能拿到可用的运行态。5.2 核心入口恢复机制的核心入口是InterviewSessionRuntimeRehydrateService.ensureRuntime()publicInterviewSessionRuntimeViewensureRuntime(StringsessionId,InterviewRuntimeLoadModeloadMode,InterviewRuntimeRehydrateScopescope)它接收三个参数sessionId面试会话唯一标识loadMode加载模式READ_WRITE_REQUIRED 或 READ_ONLYscope恢复范围FLOW_ONLY / SCORE_ONLY / PLAYBACK_ONLY / MATERIAL_ONLY / HOT_RUNTIME / FULL_RUNTIME5.3 完整恢复流程ensureRuntime(sessionId, loadMode, scope) │ ├── Step 1: 参数校验 │ └── sessionId 为空 → 返回 READ_ONLY NONE 的空视图 │ ├── Step 2: 快速路径检查isRuntimeReady │ └── 按 scope 检查 Redis 中对应的 Key 是否有数据 │ → YES: 直接返回 CACHE 来源、EXACT 置信度的视图 │ → NO: 进入恢复流程 ↓ │ ├── Step 3: 竞争 session 级分布式锁 │ ├── Owner拿到锁: 进入 Step 5 │ └── Follower没拿到锁: 进入 Step 4 │ ├── Step 4: Follower 等待策略 │ └── waitForRecoveredRuntime() │ → 最多轮询 4 次每次间隔 80ms │ → 每次轮询都重新检查 isRuntimeReady() │ → 如果 owner 已经完成恢复直接复用结果 │ → 如果 4 次都没等到再尝试一次拿锁 │ ├── Step 5: 锁内 double-checkisRuntimeReady │ └── 防止在拿锁期间其他线程已经完成了恢复 │ → YES: 返回 CACHE 来源 │ → NO: 进入 Step 6 ↓ │ ├── Step 6: rebuildRuntime() 执行真正恢复 │ │ │ ├── 6a. 优先从 Snapshot 恢复 │ │ └── canRehydrateFromSnapshot(snapshot, scope)? │ │ → 按 scope 检查快照是否包含足够数据 │ │ → YES: writeSnapshotToCache() 将快照数据回填 Redis │ │ → 返回 RUNTIME_SNAPSHOT 来源 │ │ │ └── 6b. 降级从业务材料推导恢复 │ ├── 加载 InterviewSession会话主表 │ ├── 加载 InterviewQuestion出题记录 │ ├── 加载 InterviewRecordDO面试报告 │ ├── buildRuntimeMaterial()组装所有恢复材料 │ │ ├── questions: 从出题记录解析题目 Map │ │ ├── suggestions: 从出题记录或报告解析建议 │ │ ├── resumeContext: 从出题记录解析简历上下文 │ │ ├── direction: 多源优先级推导 │ │ ├── turns: Archive → Snapshot → Record 三级降级 │ │ ├── scoreAggregate: 从 turns 重新计算 │ │ ├── flow: Snapshot → 终态推导 → turns 推导 → 初始 Flow │ │ └── confidence: 按材料完整性判定 │ └── writePartialMaterial()按 scope 写回 Redis │ └── Step 7: 返回 InterviewSessionRuntimeView5.4 isRuntimeReady按 Scope 精确检查isRuntimeReady()不是简单地检查Redis 里有没有数据而是按 scope 检查对应维度的 Key 是否都有数据// 以 FULL_RUNTIME 为例caseFULL_RUNTIME-hasHashEntries(questions)// 题目必须有hasHashEntries(flow)// 流程状态必须有(hasValue(sessionScore)// 分数或轮次至少有一个||hasListEntries(turns))(hasHashEntries(suggestions)// 材料态至少有一项||hasValue(resumeScore)||hasValue(direction)||hasJsonValue(resumeContext));不同 scope 检查不同维度这样当一个请求只需要查分数时SCORE_ONLY不需要等所有维度都恢复就绪。5.5 恢复来源与置信度恢复机制不是只返回成功/失败而是返回一个包含丰富元信息的视图对象InterviewSessionRuntimeViewDataBuilderpublicclassInterviewSessionRuntimeView{privateInterviewRuntimeConfidenceconfidence;// 恢复置信度privateInterviewRuntimeLoadModeloadMode;// 加载模式privateInterviewRuntimeRestoreSourcerestoreSource;// 恢复来源privatebooleancacheRebuilt;// 是否发生了缓存重建privateInterviewSessionRuntimeHotSnapshothotSnapshot;// 热快照privateInterviewSessionRuntimeColdSnapshotcoldSnapshot;// 冷快照privateInterviewSessionRuntimeSnapshotsnapshot;// 组合快照publicbooleancanWrite(){returnconfidenceEXACT||confidenceDERIVED;}publicbooleanisTerminal(){returnconfidenceTERMINAL;}}恢复来源RestoreSource告诉上层数据从哪来来源含义可靠性CACHERedis 运行态完好直接使用最高RUNTIME_SNAPSHOT从 Mongo 热/冷快照恢复高SESSION_QUESTION从出题记录推导中INTERVIEW_RECORD从面试报告推导中NONE无任何可用材料无置信度Confidence告诉上层能不能写等级含义是否可写EXACT运行态完整从缓存或完整快照恢复可写DERIVED从材料推导恢复大部分场景可写可写READ_ONLY材料不完整仅支持只读查询不可写TERMINAL会话已终态只读回放不可写上层业务通过view.canWrite()判断是否可以继续推进流程通过view.isTerminal()判断是否需要直接回放历史结果。这比简单的 boolean 返回值安全得多——它避免了在不可靠的状态上做出不可逆的业务决策。5.6 Scope 控制按需恢复恢复机制支持按 scope 精细控制恢复范围不同业务场景只需要恢复自己关心的维度Scope恢复内容典型场景FLOW_ONLY题目 流程 追问仅需要判断下一题SCORE_ONLY分数聚合仅需要查询累计分PLAYBACK_ONLY轮次列表 requestId仅需要回放历史MATERIAL_ONLY题目 建议 简历上下文仅需要加载材料HOT_RUNTIME题目 流程 分数/轮次 追问答题推进不含完整材料FULL_RUNTIME全部维度完整恢复每个 scope 通过一组includes*()方法定义它包含哪些维度恢复逻辑据此决定需要检查和恢复哪些 KeypublicbooleanincludesQuestionMaterial(){...}publicbooleanincludesFlow(){...}publicbooleanincludesScore(){...}publicbooleanincludesTurns(){...}publicbooleanincludesSuggestionMaterial(){...}publicbooleanincludesResumeMaterial(){...}publicbooleanincludesFollowUpQuestions(){...}publicbooleanincludesRequestIds(){...}5.7 Owner-Follower 协作模型当多个请求同时发现 Redis 缺失时如果每个请求都独立执行恢复逻辑会导致重复的 Mongo 查询和 Redis 写入并发写 Redis 可能产生状态覆盖资源浪费恢复机制通过Redisson 分布式锁 Owner-Follower 模型解决这个问题请求 AOwner 请求 BFollower │ │ ├─ 拿到分布式锁 ├─ 拿锁失败 │ │ ├─ 执行 rebuildRuntime() ├─ 轮询 isRuntimeReady() │ ├─ 从 Snapshot 恢复 │ ├─ 第 1 次还没好 │ ├─ 写回 Redis │ ├─ 第 2 次还没好 │ └─ 释放锁 │ ├─ 第 3 次好了 │ │ └─ 返回 CACHE 来源 │ │ └─ 返回 RUNTIME_SNAPSHOT 来源 └─ 复用 Owner 恢复的结果分布式锁的配置参数值说明锁 Keyinterview:runtime:rehydrate:lock:{sessionId}按 session 粒度互斥等待时间0ms首次尝试非阻塞抢锁租约时间60s自动释放防死锁Follower 重试4 次 × 80ms总等待约 320ms5.8 材料推导恢复的多源降级当快照数据不足以恢复时系统会从多个业务数据源进行降级推导。以 turns轮次记录的恢复为例resolveTurns(sessionId, snapshot, record) │ ├── 第一优先级从 Turn Archive 加载最完整 │ └── runtimeSnapshotService.loadPersistedTurns(sessionId) │ ├── 第二优先级从热快照的 recentTurns 加载 │ └── snapshot.getRecentTurns() │ └── 第三优先级从面试报告的 JSON 中解析 └── parseTurnsFromRecord(record)以 flow流程状态的恢复为例resolveFlow(session, questions, turns, snapshot) │ ├── 第一优先级快照中直接保存的 flow │ └── snapshot.getFlow() │ ├── 第二优先级终态会话直接构建 COMPLETED flow │ └── buildCompletedFlow(totalQuestions) │ ├── 第三优先级从最近一轮 turn 推导下一步 flow │ └── buildFlow(nextQuestionNumber, totalQuestions) │ └── 第四优先级有题目但没有 turn → 初始 flow └── buildInitialFlow(totalQuestions)这种多源降级策略保证了即使最高优先级的数据源缺失系统也能从更低优先级的数据源推导出可用的状态尽最大努力让会话继续。六、本篇小结上篇从一个真实的丢状态场景出发分析了长会话运行态为什么天然脆弱然后讲解了三层存储分层的设计思路最后深入剖析了恢复机制的完整实现——从ensureRuntime的快速路径检查到 Owner-Follower 并发协作再到多源降级推导恢复。恢复机制解决的核心问题是当 Redis 运行态缺失时如何从已有的检查点材料中安全、高效地把状态恢复出来并告诉上层这份恢复结果到底可不可靠。但恢复机制只是闭环的一半。如果没有更新机制持续产出高质量的恢复材料再好的恢复入口也巧妇难为无米之炊。下篇将深入讲解数据更新机制的完整实现——包括 HotRefreshCoordinator 防抖聚合、refreshSnapshot 的 CAS 重试循环、Patch 差量更新、单调性校验、幂等补偿以及轮次归档与软回放幂等的详细设计。见 长会话状态治理下。

相关新闻