HarmonyOS 本地缓存变脏后怎么恢复:版本、过期与重建缺一不可
摘要:首页摘要缓存能加速首屏,却会随着字段升级、用户切换和时间流逝变成旧事实。本文为缓存包一层包含schemaVersion、生成时间和过期时间的信封;读取时按版本和时效判定,失效后从 Repository 重建,损坏数据则删除并安全回退,避免“清应用数据才恢复”。
真实事故往往发生在升级之后:新版本把“已完成课程数”的统计口径改了,老缓存仍被当成可用数据,首页数字长期错误。缓存不是数据库真相,它必须带着“我适用于哪个规则、何时生成、何时失效”的说明书。
缓存先有边界,才谈性能
| 数据类型 | 是否适合缓存 | 失效依据 |
|---|---|---|
| 首页聚合摘要 | 适合 | 规则版本、写入事件、TTL |
| 正在编辑的表单 | 谨慎 | 会话结束或显式保存 |
| 权限状态 | 不宜长期缓存 | 每次能力调用前检查系统状态 |
| 用户身份相关数据 | 可缓存 | 切换账号时立即清除 |
缓存只做加速读,不能替代数据源。只要不把它当唯一事实,失效和重建就能设计得清楚。
用信封保存数据与元信息
exportinterfaceCacheEnvelope<T>{schemaVersion:numbercreatedAt:numberexpiresAt:numberownerId:stringdata:T}constHOME_SUMMARY_VERSION=3constHOME_SUMMARY_TTL=10*60*1000ownerId防止 A 账号读取 B 账号缓存;版本针对结构和统计规则;TTL 针对时间敏感性。不要把这些元数据散落在多个读取页面里。
读取前按固定顺序判定
先判空,再判解析,接着判归属、版本和过期。任何一个条件不满足都返回 miss,而不是勉强把旧对象交给 UI。
functionisUsable<T>(cache:CacheEnvelope<T>,ownerId:string,now:number):boolean{returncache.ownerId===ownerId&&cache.schemaVersion===HOME_SUMMARY_VERSION&&cache.expiresAt>now}这样迁移规则也简单:升级版本号即可让旧缓存自然失效;不需要在每个页面里写字段兼容补丁。
Repository 只负责可靠地读写与删除
序列化失败、字段缺失和读取异常都应被视为缓存 miss,并删除坏值。页面永远不直接解析 Preferences 里的 JSON。
asyncreadSummary(ownerId:string):Promise<HomeSummary|undefined>{try{constenvelope=awaitthis.cacheStore.read<CacheEnvelope<HomeSummary>>('home_summary')if(!envelope||!isUsable(envelope,ownerId,Date.now())){returnundefined}returnenvelope.data}catch(error){awaitthis.cacheStore.remove('home_summary')returnundefined}}删除坏缓存不是掩盖错误:应记录不含个人数据的错误码或缓存键,方便观察升级后是否集中发生解析失败。
miss 后从源数据重建,而不是返回假数据
asyncgetHomeSummary(ownerId:string):Promise<HomeSummary>{constcached=awaitthis.repository.readSummary(ownerId)if(cached){returncached}constrecords=awaitthis.learningRepository.listByOwner(ownerId)constsummary=this.summaryBuilder.fromRecords(records)awaitthis.repository.writeSummary(ownerId,summary)returnsummary}服务层拥有“缓存 miss 后去哪取源数据、怎样聚合”的知识。UI 只得到摘要,不需要关心是命中还是重建。
写路径要使缓存立即过期或同步更新
课程进度发生变化时,最安全的首选是删掉相关摘要缓存;下一次读取会按当前规则重建。对计算昂贵且更新频繁的场景,才考虑在同一事务中同步更新缓存。
| 写入事件 | 推荐处理 |
|---|---|
| 完成一节课程 | 删除首页摘要缓存 |
| 切换账号 | 删除该账号或全局缓存 |
| 升级统计口径 | 提升schemaVersion |
| 网络拉取新资料 | 按资源范围失效 |
宁可一次 cache miss,也不要继续展示确定错误的业务数值。
页面不要把缓存对象绑成长期状态
页面在onPageShow从服务读取摘要;用户从详情返回时可得到重建后的最新值。不要把CacheEnvelope放入@StorageLink,更不要直接读取其内部字段。
asynconPageShow():Promise<void>{this.summaryState={kind:'loading'}try{this.summaryState={kind:'data',value:awaitthis.homeService.getHomeSummary(this.ownerId)}}catch(error){this.summaryState={kind:'error',message:'首页数据加载失败'}}}缓存属于 Repository,页面属于渲染;两者不混在一起,升级和排障都更安全。
验证失效与恢复的完整链路
- 写入一份当前版本缓存,确认首页命中并显示正确。
- 改低
schemaVersion,确认自动重建而非报错。 - 把
expiresAt调到过去,确认自动重建。 - 写入损坏 JSON,确认删除坏值并回到源数据。
- 切换账号,确认不会读到上一账号摘要。
- 修改学习记录后返回首页,确认摘要不使用旧缓存。
rg"home_summary|schemaVersion|expiresAt|remove\("entry features common常见误区与结论
| 误区 | 风险 | 正确做法 |
|---|---|---|
| 缓存只存 data | 无法判断能否使用 | 使用版本化信封 |
| 解析错误仍返回默认数据 | 错误被悄悄放大 | 删除并重建 |
| 用户切换不清缓存 | 可能展示他人数据 | 用ownerId校验并失效 |
| 写入后不处理缓存 | 长期旧数据 | 删除或同事务更新 |
缓存治理的核心不是把命中率做高,而是在它不可信时可靠地退出。版本决定规则边界,过期决定时间边界,重建决定用户不会因为一份旧缓存看到空白或错误事实。
TTL 不要一刀切,按业务变化速度定
缓存时长不是越长越好。首页摘要可能十分钟可接受,支付状态、权限状态和账号资料则必须更快重新确认。为每一类数据写清失效原因,比只写一个全局 TTL 更能避免后续误用。
| 数据 | 失效策略 | 原因 |
|---|---|---|
| 课程首页摘要 | 短 TTL + 学习记录写入时失效 | 聚合结果会频繁变化 |
| 离线课程目录 | 较长 TTL + 服务端版本变化时失效 | 变化较慢且读取频繁 |
| 用户资料 | 账号切换即失效 | 不能跨身份复用 |
| 安全或权限信息 | 不持久化为业务缓存 | 系统状态可能随时变化 |
定义策略时要记录“谁能使它失效”。否则新功能写入源数据后,没人知道还要处理哪一份派生缓存。
重建失败时也要有可观察的降级
源数据读取失败不应伪造一个“缓存命中”的默认摘要。服务可以向页面返回明确错误态,页面显示重试;只有业务允许时才展示上次已验证的只读快照,并标记刷新时间。
asyncrefreshSummary(ownerId:string):Promise<HomeSummary>{try{returnawaitthis.getHomeSummary(ownerId)}catch(error){this.logger.warn('cache','rebuild summary failed',{key:'home_summary'})throwerror}}日志中记录缓存键和错误码即可,不要写入摘要内容或用户身份原文。这样可以区分“缓存坏了”与“源数据不可读”,排障不会误伤用户数据。
版本升级要有迁移还是直接失效的判断
并不是每次结构变化都需要兼容旧缓存。只增加可选字段、并且旧数据仍能满足业务时,可以提供小迁移;统计口径、身份归属或安全语义发生变化时,直接失效更可靠。缓存的价值是加速,不值得为了保住它引入复杂且难测的迁移链。
functioncanMigrate(from:number):boolean{returnfrom===2}即使允许迁移,也要在迁移后立即写回新版本信封,并在失败时删除原值后重建。不要每次读取都临时兼容一串历史版本,几年后没人能判断哪一条分支仍然正确。
观察命中率时同时观察错误率
缓存指标不能只看命中率:命中一份过期或归属错误的数据比 miss 更危险。建议观察命中、版本失效、TTL 失效、解析失败、重建失败五类计数,并按应用版本比较。若某次发布后解析失败突然增加,优先检查序列化结构和缓存键是否调整。
这些指标只记录聚合数量与错误码,不保存缓存内容。它们帮助团队确认缓存策略是否真的加速了体验,而不是把数据问题变得更隐蔽。
| 指标 | 表示什么 | 异常时先看 |
|---|---|---|
| 命中 | 读取到可用信封 | 是否真的仍符合业务时效 |
| 版本失效 | 规则发生升级 | 版本号和迁移分支 |
| TTL 失效 | 数据已过时 | TTL 是否与业务频率匹配 |
| 解析失败 | 存储内容不可读 | 序列化格式与缓存键 |
| 重建失败 | 源数据不可用 | Repository 和网络/本地状态 |
观察到问题后先让缓存安全 miss,再修复重建链路;不要为了让命中率好看而放宽版本或过期判断。