长会话状态治理(上):问题分析、存储分层与恢复机制
系列导航:本文是长会话状态治理系列的上篇,聚焦于"为什么会丢状态"以及"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 是什么"。它是恢复机制最优先依赖的检查点。
热快照实体中几个关键字段的设计意图:
| 字段 | 类型 | 用途 |
|---|---|---|
snapshotVersion | Long | 版本号,每次刷新递增,用于 CAS 并发保护 |
snapshotLevel | String | 业务阶段标记:DRAFT → QUESTION_READY → ACTIVE → FINALIZED |
rebuildConfidence | Enum | 恢复后的置信度:EXACT / DERIVED / READ_ONLY / TERMINAL |
flow | Object | 当前流程状态:进行到第几题、是否追问、是否已完成 |
scoreAggregate | Object | 得分聚合:累计分、计分次数、会话总分 |
recentTurns | List | 最近 N 轮问答窗口(默认上限 20 轮),用于快速恢复上下文 |
archiveWatermark | Long | 归档水位:热快照已同步覆盖到第几条 Turn Archive |
lastTurnSeq | Long | 最近归档轮次的顺序号,用于单调性保护 |
lastMutationId | String | 最后一次成功变更的 requestId,用于幂等判定 |
lastCommittedQuestionNumber | String | 最后一次成功提交的题号 |
lastCommittedTurnDigest | String | 最后一次成功提交的题号+答案的 SHA256 摘要,兜底幂等 |
冷快照(Cold Snapshot)对应 Mongo 集合interview_session_runtime_cold_snapshot。通常在出题完成、简历解析完成、神态评分完成等关键节点才变化,保存的是"题目列表、简历上下文、面试方向"等低频材料。它在热快照数据不够用时提供材料补充。
轮次归档(Turn Archive)对应 Mongo 集合interview_session_turn_archive。每成功提交一轮答题就追加一条,永不修改。用于全量历史回放和软回放幂等。
热快照和冷快照分开的好处:每轮答题后只需要 CAS 更新热快照,不需要把整包数据(包括大量低频材料)重写一遍。这直接降低了写放大和并发冲突概率。
4.3 Redis Key 体系
在线运行态的 Key 按维度独立管理,每个 session 对应一组 Key:
interview: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_ONLY)scope:恢复范围(FLOW_ONLY / SCORE_ONLY / PLAYBACK_ONLY / MATERIAL_ONLY / HOT_RUNTIME / FULL_RUNTIME)
5.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-check(isRuntimeReady) │ └── 防止在拿锁期间其他线程已经完成了恢复 │ → 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 恢复来源与置信度
恢复机制不是只返回"成功/失败",而是返回一个包含丰富元信息的视图对象InterviewSessionRuntimeView:
@Data@BuilderpublicclassInterviewSessionRuntimeView{privateInterviewRuntimeConfidenceconfidence;// 恢复置信度privateInterviewRuntimeLoadModeloadMode;// 加载模式privateInterviewRuntimeRestoreSourcerestoreSource;// 恢复来源privatebooleancacheRebuilt;// 是否发生了缓存重建privateInterviewSessionRuntimeHotSnapshothotSnapshot;// 热快照privateInterviewSessionRuntimeColdSnapshotcoldSnapshot;// 冷快照privateInterviewSessionRuntimeSnapshotsnapshot;// 组合快照publicbooleancanWrite(){returnconfidence==EXACT||confidence==DERIVED;}publicbooleanisTerminal(){returnconfidence==TERMINAL;}}恢复来源(RestoreSource)告诉上层数据从哪来:
| 来源 | 含义 | 可靠性 |
|---|---|---|
CACHE | Redis 运行态完好,直接使用 | 最高 |
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*()方法定义它包含哪些维度,恢复逻辑据此决定需要检查和恢复哪些 Key:
publicbooleanincludesQuestionMaterial(){...}publicbooleanincludesFlow(){...}publicbooleanincludesScore(){...}publicbooleanincludesTurns(){...}publicbooleanincludesSuggestionMaterial(){...}publicbooleanincludesResumeMaterial(){...}publicbooleanincludesFollowUpQuestions(){...}publicbooleanincludesRequestIds(){...}5.7 Owner-Follower 协作模型
当多个请求同时发现 Redis 缺失时,如果每个请求都独立执行恢复逻辑,会导致:
- 重复的 Mongo 查询和 Redis 写入
- 并发写 Redis 可能产生状态覆盖
- 资源浪费
恢复机制通过Redisson 分布式锁 + Owner-Follower 模型解决这个问题:
请求 A(Owner) 请求 B(Follower) │ │ ├─ 拿到分布式锁 ├─ 拿锁失败 │ │ ├─ 执行 rebuildRuntime() ├─ 轮询 isRuntimeReady() │ ├─ 从 Snapshot 恢复 │ ├─ 第 1 次:还没好 │ ├─ 写回 Redis │ ├─ 第 2 次:还没好 │ └─ 释放锁 │ ├─ 第 3 次:好了! │ │ └─ 返回 CACHE 来源 │ │ └─ 返回 RUNTIME_SNAPSHOT 来源 └─ 复用 Owner 恢复的结果分布式锁的配置:
| 参数 | 值 | 说明 |
|---|---|---|
| 锁 Key | interview:runtime:rehydrate:lock:{sessionId} | 按 session 粒度互斥 |
| 等待时间 | 0ms(首次尝试) | 非阻塞抢锁 |
| 租约时间 | 60s | 自动释放,防死锁 |
| Follower 重试 | 4 次 × 80ms | 总等待约 320ms |
5.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 差量更新、单调性校验、幂等补偿,以及轮次归档与软回放幂等的详细设计。见 长会话状态治理(下)。