23 KiB
阶段3:缓存与可靠同步方案及实施裁决
2026-09-23状态更新:完整方案、自审及限定独立复审通过;两条收紧策略已于2026-09-22正式追认;3A、3B均已正式验收。3C详细方案、实施任务书和验收矩阵已落盘,等待其他AI独立方案审查,尚未编码或访问正式服。审查裁决后由主会话实施。正式 API 固定 http://s101.local:18080/。
1. 前置证据及目标
阶段2已通过42项契约、16项正式服专用key联调、双账号清理、Unity编译及独立终审。见《阶段2独立KV验收-2026-09-22.md》。不重复这些验证,不重新使用已清理的测试key。
沿用G1已选定的“云端基线+待写日志+三方比较”,先做独立模块和测试宿主。目标是本客户端顺序写入不丢待写数据、失败不伪装成功、环境/账号不串档。当前服务器无revision/CAS/幂等请求查询,不承诺跨设备强一致或资产事务恰好执行一次。
源码核对:
Core/PlayFabMgr.cs当前有五个十key桶、40秒周期,失败只日志,立即同步独立SDK请求;它们是阶段4要替换的现有入口,本批保留。Services/UserService.cs当前拥有私有apiSession和登录代际;CustomServerMgr有SessionVersion,但transport可替换,不能直接承担KV固定会话传输。Services/PlayerData/PlayerKVClient.cs已提供固定注入HttpClient、错误分类、取消和MayHaveCommitted。本批复用,不重新实现HTTP协议。- 阶段1已盘点remoteStore=false和立即同步调用;阶段3不改FishingBehaviorB、TurntableRoll或业务扣减流程。
2. 方案比较与选定路线
| 路线 | 取舍 | 本次判断 |
|---|---|---|
| 独立状态/磁盘层,再串行同步,再接入游戏 | 故障可在纯测试中穷举,独立于Unity和PlayFab;需明确三个小验收门 | 推荐,延续G1及阶段2边界 |
| 直接替换PlayFabMgr内部并同时改登录 | 入口集中,但未验收缓存就改变真实存档,失败难定位 | 阶段4前不采用 |
| 先扩服务端版本条件写/幂等查询 | 可解决客户端无法独自解决的并发/未知结果问题,需服务端项目授权与接口变更 | 本批不改服务器;如要求自动且严格恢复,另立阶段 |
对旧总体方案补充两条保守裁决,须明确确认:
- 写入结果未知后可只读核对,但不能仅凭一次Get成功自动恢复后续写。即使R等于B,也不能证明旧请求稍后不会提交。未知标记跨重启保留,本批无自动清除此标记的公开接口。
- 缓存主文件损坏时,备份只作为恢复材料,不能自动当作最新存档继续同步。备份可能遗漏最后一次已接受的本地修改。没有有效主文件且有遗留/损坏材料时必须阻止可靠写入,禁止静默建空档;主文件有效时,备份较旧或损坏仅诊断,不因此冻结有效主档。
这会降低异常情况下的自动恢复能力,换取不继续自动覆盖数据。没有声称在无CAS条件下获得跨设备保证。后续若要自动解锁未知结果,需要服务端终态证据或另外评审有限风险策略;人工恢复工具不包含本批。
3. 分批实施与文件边界
| 小阶段 | 生产新增文件(均在Services/PlayerData,含meta) | 验收后才推进 |
|---|---|---|
| 3A 状态与持久化 | PlayerKVSyncState.cs、PlayerKVCacheStore.cs、PlayerKVSyncModels.cs | 状态转换、磁盘故障、隔离及重启恢复测试,Unity兼容编译 |
| 3B 固定会话及同步器 | PlayerKVSessionOwner.cs、PlayerKVSyncCoordinator.cs | 串行HTTP、迟到响应、取消/401、立即请求票据、未知结果冻结测试 |
| 3C 联合验收 | 默认不增加生产文件,仅修复评审确认问题 | 3A/3B组合测试、限定正式服fixture、Unity编译、独立review和G3结论 |
测试和证据:CodexOutput/upserver/KVSyncTests/、KVSyncLiveTests/、阶段3报告/日志。离线测试直接编译生产源码,引用现有Newtonsoft,不新增包或修改asmdef。
本批不改:PlayFabMgr、UserService、CustomServerMgr、RTService、游戏初始化/玩法、配置、SDK或服务器。阶段3的SessionOwner可用独立认证会话验证;接入UserService的具体生命周期合同见第8节,由阶段4另行评审实施。
范围扩展、新依赖或现有生产文件改动必须先补差异裁决;不批量回退其他人的外围改动,不自动提交/发布/打包。
4. 身份、快照与存储格式
4.1 固定归属
构造时固定规范化API基址(含路径,不仅host/port)、正整数十进制涂游ID和本次会话epoch。URL不含用户信息/query/fragment,末尾统一斜杠;host/scheme规范化,路径保留大小写。环境指纹为完整规范基址UTF-8的SHA256。不能把s101.local与某IP自动视为同服。
路径:调用方传入的专用根目录下 player-kv/v1/<environmentHash>/<playerId>/state.json。首次接入游戏时根目录取persistentDataPath,离线和联调宿主只用任务目录。路径各段由摘要和验证后的ID生成,拒绝任意路径输入。旧LocalData/LastUpdateTime PlayerPrefs完全不读取或迁移。
每个命名空间持有独占文件句柄锁,冲突返回Busy,不创建第二写入者;文件存在不等于锁活着,不删除仍被占用的锁来抢占。旧owner关闭并完成请求/磁盘操作后才能释放锁,新owner随后加载;同号重登也遵守。跨设备不受此锁保护。
4.2 单快照内容
- formatVersion=1、完整apiBaseUri、playerId、revision/nextSequence(本地单调序号,不冒充服务器版本)。
- baseline:最后确认远端的字典;字典缺键代表不存在,空字符串代表存在空值。
- pending:每key一个最新目标(Set字符串或Delete显式标记)、本地sequence、编辑基线存在标记/值。每项都必须有可靠基线,不能凭null猜测。
- localOnly:remoteStore=false的本地覆盖层;不进入待上传记录。
- activeBatch:在发网络前持久化的不可变批次ID、操作种类、key/sequence/base/目标快照;最多一批。
- unknownBatch:结果未知或重启遗留activeBatch的证据;保留所发版本及后来产生的新pending,不允许后者覆盖前者证据。
- conflictObservations:发生冲突时的远端观察值/存在标记,保留B/L/R;不输出日志。
快照只保存存档必需值,不保存Cookie、token或认证会话。运行日志只记录数量/序号/错误类别,不打印值或完整JSON。
4.3 持久化顺序与失败
同步状态变更先对副本计算,序列化完整快照,写同目录临时文件、flush、原子替换主文件,保留上一份完整备份,成功后才发布内存新状态。不存在主文件时使用同目录首次rename;不允许“先删除主文件再移动”。平台不支持原子替换时返回StorageError,不静默退化为不安全覆盖。
已有主文件校验格式、归属、非重复字段、类型、值/key限制、唯一sequence/批次关联和计数范围;不接受未来版本。sequence在pending内部唯一,active可引用同一key的同一sequence,也可保留该key已被新pending替换的旧sequence;这些关联不能误判为重复损坏。使用项目Json.NET,结构严格且沿用阶段2语法兼容边界。磁盘中的数据不可信,不能反序列化任意运行时类型。
主文件缺失且无bak/tmp等遗留证据,才视为无缓存;仍需远端Get成功后才能Ready。主文件损坏或缺失但有遗留材料,返回RecoveryRequired并保留文件;有效备份可供诊断但不自动Ready。主文件有效时,备份较旧或损坏仅诊断、不影响主文件加载;遗留tmp不替代主文件。第3C须验证崩溃发生在各个替换步骤的结果。
任何保存失败:不发布未持久化编辑,进入StorageError,禁止网络写入及新的可靠编辑。若HTTP200之后确认未能落盘,原磁盘activeBatch仍表示结果不确定,重启继续冻结;若替换已经完成后才报错,则磁盘也可能已有完整确认快照,需按下段核对,不断言active必然还在。不能因本次HTTP成功就虚报调用端可靠完成。无无限队列静默丢弃;序号溢出/容量限制均显式失败,保留已有状态。
本地提交也可能不确定。 已进入磁盘提交步骤之后报错,不能断言主文件未更新;例如替换成功后报告/后续flush失败,磁盘可能是新revision而内存尚未发布。编辑结果必须携带attemptSequence/目标revision及LocalCommitMayHaveSucceeded,进入StorageError,不清掉本次意图或自动补偿重做。只读重新打开并比较该版本可以核对磁盘结果,但不会自动恢复业务操作。后续调用方不可把StorageError解读成“这次修改一定没有保存”。磁盘确认成功是Accepted的必要条件,任何不确定都不能返回Accepted。
不承诺断电后物理介质绝对耐久性;文件替换/flush的运行平台行为需最终设备验收,桌面故障测试不替代该项。
5. 状态与编辑语义
状态:NotLoaded → Loading → Ready;异常为ReadFailed、Conflict、UnknownOutcome、Unauthorized、StorageError、RecoveryRequired、Closed。Busy是打开结果;不覆盖已有owner。主状态之外保留unknown证据与auth失效标志,二者可同时成立;StorageError/Closed等状态不允许抹掉unknown,重登也不能只改一个枚举就恢复写入。
- 加载和初始化期间拒绝编辑/Flush;首次全量Get失败返回ReadFailed,不返回空字典,不初始化默认业务档。
- Ready才开放可靠编辑。正常同步请求等待期间仍可编辑下一版本;锁保护内存与持久化状态,HTTP等待不得持锁。先完成更新落盘,再返回Accepted。
- Update(key,value,remoteStore=true):合法字符串值入pending;保留原编辑基线,替换该key最新目标并递增sequence,同时移除同key的localOnly覆盖。
- Update(...,remoteStore=false):只改localOnly。不自动撤销此前同key已经产生的remote pending,也不修改active快照。后续旧写确认不能覆盖本地专用值;下一次显式remote=true才清除此覆盖并创建新远端意图。
- Delete(key):明确删除意图,与空字符串不同,不新增游戏删除入口;与Set共享sequence和批次规则,在同一次持久化中清除同key的localOnly覆盖。这样删除确认后ReadLocal不会继续返回旧本地专用值。
- ReadLocal只返回只读副本或字符串;合成顺序baseline→pending→localOnly。Closed/未Ready/冲突状态只能使用显式诊断快照,不能向游戏伪装已就绪数据。
- 冲突、未知、Unauthorized等阻塞状态不接纳新的可靠玩法修改;重试Get不会丢弃日志或自动恢复Ready。允许可证明未发送的读失败重试和第6节普通冲突重读。
建议接口以结构化结果表达Accepted/Blocked/Validation/StorageError及本地sequence;不要复用阶段2PlayerKVError表示所有存储状态,也不以bool掩盖未知结果。
6. 初始合并与恢复
普通未发送pending(无active/unknown)适用B=编辑基线、L=最新目标、R=远端观察:
| 比较 | 决策 |
|---|---|
| R等于L(含删除的不存在) | 视为目标在本次观察时已满足,确认对应sequence,落盘后移除 |
| R等于B | 保留待写,允许正常串行发送 |
| 两者均不等 | Conflict,保留B/L/R,整实例暂停远端写 |
| 基线不可靠 | 除R等于L外均冲突,不猜时序 |
没有pending的key采用本次远端字典;保留localOnly。字符串精确比较,不解析合并业务JSON。整个合并结果一次落盘,不在发现后面的冲突前先发前面的key。重复Get若不再冲突可在完整合并/落盘后恢复Ready;否则保持暂停。
UnknownOutcome优先于上表: activeBatch遗留、写失败MayHaveCommitted=true、发出后取消或HTTP成功但确认快照未实际提交,均形成未知批次。允许读取观察并保存对比诊断,但不自动清未知标记、不根据R==B重发、不根据R==L放行后续新值;可能仍存在服务端迟到提交。若连观察都失败,仍保留UnknownOutcome和原始记录。未知结果的解除机制是后续服务端协议/单独恢复评审的前置,不在本批偷偷加“强制继续”按钮。HTTP200且完整确认快照已实际提交、仅提交返回时报错,是本地提交核对问题;重新加载有效确认快照不等于擅自清除持久化unknown,不能将两类不确定混为一谈。
这条保守暂停比旧总体方案更严格。阶段3的“恢复”明确包括未发送日志恢复、正常冲突的再观察以及未知状态的可靠保留,不等于所有网络故障均可自动继续游戏。
7. 串行发送及立即同步
7.1 批次
所有Get恢复、普通Flush、立即Flush和Delete由同一协调器串行调度,禁止两个变更HTTP同时在途。调用方显式触发FlushAsync;40秒定时触发和Unity暂停事件接线留阶段4,不在独立模块内创建无限后台定时器。
按sequence升序选取最多10个连续同类型待写key,遇到不同类型即截断,不能跳过中间Set而先发后面的Delete。先存activeBatch,再发对应Set或Delete。Set/Delete不同类型不能合成一个原子请求。单次Flush捕获调用时水位,处理其范围后返回;新编辑留后续,避免持续编辑导致Flush永不完成。水位内尚未发送的版本若被更高版本覆盖,原版本计为Superseded,不临时把新版本纳入本次Flush。Flush汇总明确Confirmed/Superseded/Blocked数量,不把队列暂时无水位内项理解为所有票据成功。
发送v1等待时编辑v2:v1成功并且会话仍有效,则先把已确认baseline设为v1;pending若仍是v1的sequence则移除;若已经是v2则保留v2并将其基线推进到v1。确认状态和activeBatch清除同一次持久化;失败保持pending及发出证据。
7.2 结果
- 200且持久化确认成功:本批确认成功;不修改更高sequence的目标。
- 未发送的取消/校验失败:无服务端未知写,可保留pending;参数失败不可不停自动重试。
- 已持久化active但尚未送入transport时取消:只有同一进程掌握确定未发送证据才能安全撤销active;崩溃恢复看不到该证据时保守未知。
- MayHaveCommitted=true:进入UnknownOutcome,无自动写重试;401/403同时记录认证失效,先禁止新请求,后续新认证不能清未知。
- Get失败不丢baseline/pending,也不变成空云端。
- 不修改已完成阶段2的保守HTTP错误分类;例如明确HTTP400目前仍可能标记MayHaveCommitted,此阶段不擅自推定服务器未写。
7.3 立即请求
独立模块提供带sequence的写入票据及等待结果Confirmed/Superseded/Blocked/Unknown/Cancelled。立即请求进入同一队列,不抢占在途写;队列中尚未发送v1已被同key v2替换时,v1票据返回Superseded,不因v2成功就称v1成功。
票据的等待取消只取消这个等待者,不撤销已持久化pending,不取消同批其他票据或已安排的Flush;等待者收到Cancelled不代表目标未发送/未保存。编辑准入前取消可以拒绝编辑,准入并提交后不能用取消回滚。协调器HTTP仅绑定owner生命周期token;已经接纳的Flush按其水位完成或进入阻塞,调用者取消等待不会改变共享任务。owner失效/Close才取消共享传输。此规则与阶段2单请求CancellationToken的含义不同,应通过显式WaitForTicketAsync/Flush等待包装表达,不把外部等待token直接传给共享HttpClient请求。
true兼容适配留阶段4:只有该票据确切目标获远端成功且确认落盘,或普通未发送pending在Get中确认等于目标,才返回true。未知/取消/被替换均不能true;失败不能触发玩法补回再扣减。阶段3不改现有SyncDataImmidiate签名或调用方。
8. 会话所有权与生产桥接合同
8.1 阶段3可执行独立Owner
由认证成功的固定S101AccountSession、调用方epoch/生命周期token、IsCurrent判断和缓存根目录构造owner。创建时必须在调用方会话切换的同一串行临界区捕获api/playerId/CookieContainer,验证已认证且归属一致;不开放运行期切换账号的setter。
owner新建自己的HttpClientHandler(该CookieContainer、UseCookies=true、AllowAutoRedirect=false)和HttpClient/PlayerKVClient,拥有它们及缓存锁/协调器;不Dispose调用方的认证session,不清共享Cookie,不使用可替换的CustomServerPost。CookieContainer本身可变,因此约定该捕获session不再Login重用;重新认证必须关闭旧owner并新建。
每个await前后核对epoch、IsCurrent和lifetime,旧响应不发布事件或影响新owner;已发送写晚到只在旧命名空间保留未知记录,不能转到新Cookie重发。Unauthorized只返回给捕获的owner,不能无归属发布全局重登事件。
CloseAsync幂等:先同步置Closed禁止新编辑/请求并取消,在途操作保留已持久化active;等待所有登记操作退出;释放transport和缓存锁,最后完成Close。退出不隐式Flush。取消token不是服务端回滚证明。若传输不响应取消,保持关闭等待/锁占用,禁止通过超时强行释放锁后启动同名新owner。
原子准入与关闭次序: Closed检查、操作登记、获取owner租约使用同一个短临界区;登记成功后才能访问状态/磁盘或进入调度,任何操作退出都在finally归还租约。Close也用这个门原子置Closed并禁止以后登记,因此不存在“检查通过→Close看到零操作并释放锁→旧操作才登记”的窗口。状态/磁盘锁和准入门内均不得await;Close退出锁后才Cancel(取消回调可能同步执行)、终结尚未确认的票据等待为Cancelled/Unknown、等待实际操作退出。被动票据等待者不计入Close必须等待的在途工作;已接纳的协调器worker/磁盘操作才计入。最后只释放一次transport/文件锁。Close不等待自身,不在持有state/dispatch锁时等待worker,避免相互等待。
8.2 阶段4接入点(本批只设计)
- UserService在API登录成功、代际仍有效时创建一次owner,固定认证session及账号/环境。开放的是窄的存档服务引用,不暴露全局任意可换Cookie传输。
- Logout/重新API认证的第一步使旧owner失效并发起CloseAsync,随后才清认证session。同步Logout不能阻塞Unity主线程等待任务;将关闭任务交给新登录流程await,在新owner拿同名缓存锁前必须完成。
- 当前阶段3不会在现有UserService加入这段代码;阶段4先完整审查登入→加载→初始化→Done、未授权、退出以及立即玩法调用,再开放具体文件。
- SessionOwner拥有独立传输,但共享认证Cookie的外部生命周期仍需宿主遵守;阶段3测试要故意失效session/换号验证不会借用新Cookie。SignalR连接继续归现有RTService,不迁到KV层。
9. 分阶段验收用例
G3A:先状态和磁盘,完全离线
- API含路径/账号隔离、非法ID/路径拒绝、同命名空间独占锁、同号关闭再打开。
- 全量Get失败无Ready;合法空字典与缺数据、缺键、空字符串、显式Delete分别处理。
- B/L/R四种规则;多key含冲突时没有部分自动发送;JSON字符串只精确比较。
- 51/101个key无丢失;同key多版本、localOnly与既有pending/在途确认组合;序号溢出。
- 暂存v1→编辑v2→确认v1,v2保留且基线正确;已被替换票据不误报成功。
- 写前持久化active;在每个磁盘步骤注入失败;主文件/bak/tmp缺失与损坏、错误身份/版本/重复序号均有明确阻塞结果。
- active/unknown跨重启保留、读回B/L/其他值均不能自动解除未知;已有记录不得被较旧备份静默覆盖。
- 保存失败不发布编辑/确认成功,不发后续网络;证据不打印值。独立review与Unity兼容编译。
- 原子替换成功后注入异常:返回LocalCommitMayHaveSucceeded及attemptSequence,重开看到新revision不会被当作未曾保存;禁止自动补偿/重复业务编辑。
G3B:可控传输及会话,默认无正式服写
- 使用实际PlayerKVClient配HttpMessageHandler替身;Set/Delete/立即票据串行,批10、51+key、发送中继续编辑、水位退出。
- 写前取消、发出后取消、超时/401/403/400/500、成功后磁盘失败,逐项核对unknown/阻塞/日志保留。
- 101旧成功/旧401→切102、新同号epoch;旧owner不影响新状态,绝不重新拿新Cookie发送旧值。
- Close幂等、并发Close/Flush/Update、transport迟到成功和不响应取消、锁仅等待收尾后释放。
- 未知结果观察无自动写;Get恢复失败不初始化默认档;票据只有精确确认且落盘才Confirmed。
- 在检查Closed与准入登记之间设置可控调度点:Close不得提前释放;Cancel同步回调重入不能死锁;被动票据等待先终结,不阻塞Close。
- 一张票据取消等待、同批另一票据继续确认;取消Flush等待仍不撤销已持久化目标;owner关闭才取消传输,未知证据保持。
G3C:组合与限定联调
离线跨实例/进程重启测试完成后,才用101/102新唯一前缀key验证串行写、重新打开缓存/重登、隔离和清理;fixture适配限制所有请求只能覆盖预先登记测试key,禁止把全量真实业务存档放入测试缓存再执行写入。磁盘测试缓存只在CodexOutput任务目录,退出释放锁。
不得在正式服人为制造未知写或服务端延迟;不确定结果用可控替身复现,避免因“恢复测试”产生未终止的迟到写破坏清理。测试finally精确删除、双账号读回不存在,并记录失败清理项;不覆盖阶段2原证据。
最终Unity编译、独立review、源码hash及本批报告通过才能标G3。独立测试宿主不是Unity业务存档运行验收;平台原子替换/进程终止与真实SDK/设备仍按最终包单列。
10. 自审裁决及执行检查表
主会话自审:文件边界、HTTP复用、双层生命周期、磁盘先行、存在标记、同key多版本、localOnly、未知写和备份风险均已明确;无新增服务器契约/依赖,没有把“正常登录可用”当作“游戏KV可切换”。
- 复用阶段2验收;读取既有设计、当前PlayFabMgr/账号/HTTP源代码。
- 比较实施路线,明确3A/3B/3C和不改游戏入口边界。
- 写出数据模型、状态、磁盘顺序、串行算法、会话桥接合同和验收矩阵。
- 限定独立方案评审及R1–R4问题闭环;见《阶段3缓存同步独立方案审查-2026-09-22.md》。
- 用户在方案评审后明确“继续”,两条收紧策略为用户后续确认;2026-09-22正式追认(不重新评审),3A已实施并验收。
- 3A实现及独立验收通过,见《阶段3A缓存状态与磁盘验收-2026-09-22.md》。
- 3B已实施并正式验收;见《阶段3B文档终审-2026-09-22.md》。
- 3C详细方案待独立审查和主会话裁决;通过后才实施。
当前阶段结论:阶段2、3A、3B完成;3C详细方案已形成,等待独立审查,所有3C矩阵项仍是计划而非测试结果。3C含测试宿主脱敏诊断,其后另立人工恢复专项;阶段4/5外围/6仍未放行。每个小步骤更新开发日志、TODO和交接入口。