21-缓存越用越脏-给本地数据加版本过期和重建策略

📅 2026/7/14 5:12:35 👁️ 阅读次数 📝 编程学习
21-缓存越用越脏-给本地数据加版本过期和重建策略

HarmonyOS 本地缓存变脏后怎么恢复:版本、过期与重建缺一不可

摘要:首页摘要缓存能加速首屏,却会随着字段升级、用户切换和时间流逝变成旧事实。本文为缓存包一层包含schemaVersion、生成时间和过期时间的信封;读取时按版本和时效判定,失效后从 Repository 重建,损坏数据则删除并安全回退,避免“清应用数据才恢复”。

真实事故往往发生在升级之后:新版本把“已完成课程数”的统计口径改了,老缓存仍被当成可用数据,首页数字长期错误。缓存不是数据库真相,它必须带着“我适用于哪个规则、何时生成、何时失效”的说明书。



缓存先有边界,才谈性能

数据类型是否适合缓存失效依据
首页聚合摘要适合规则版本、写入事件、TTL
正在编辑的表单谨慎会话结束或显式保存
权限状态不宜长期缓存每次能力调用前检查系统状态
用户身份相关数据可缓存切换账号时立即清除

缓存只做加速读,不能替代数据源。只要不把它当唯一事实,失效和重建就能设计得清楚。

用信封保存数据与元信息

exportinterfaceCacheEnvelope<T>{schemaVersion:numbercreatedAt:numberexpiresAt:numberownerId:stringdata:T}constHOME_SUMMARY_VERSION=3constHOME_SUMMARY_TTL=10*60*1000

ownerId防止 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,页面属于渲染;两者不混在一起,升级和排障都更安全。

验证失效与恢复的完整链路

  1. 写入一份当前版本缓存,确认首页命中并显示正确。
  2. 改低schemaVersion,确认自动重建而非报错。
  3. expiresAt调到过去,确认自动重建。
  4. 写入损坏 JSON,确认删除坏值并回到源数据。
  5. 切换账号,确认不会读到上一账号摘要。
  6. 修改学习记录后返回首页,确认摘要不使用旧缓存。
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,再修复重建链路;不要为了让命中率好看而放宽版本或过期判断。