HarmonyOS 数据缓存一致性实战:内存、本地、远程数据同步
HarmonyOS 数据缓存一致性实战内存、本地、远程数据同步很多应用的缓存问题不是“有没有缓存”而是“缓存之间互相打架”。页面先显示了内存数据后台又拉到远程数据本地数据库里还保留着上一次修改。最后表现出来就是列表闪动、用户刚改的内容被覆盖、离线进入页面一片空白。这篇文章从一个常见场景入手用户资料、订单列表、配置数据这类页面需要先快速展示可用数据再刷新远程数据同时避免把本地未同步修改覆盖掉。示例以 ArkTS 写法组织持久化层可以按项目替换为 Preferences、RDB 或文件缓存。1. 缓存一致性先看数据生命周期缓存一致性不是单纯“读缓存、写缓存”。更准确的模型是数据在三层之间流动。层级适合存什么不能承担什么内存缓存当前会话内的高频读取数据不能作为离线可靠来源本地持久化最近一次可用数据、用户草稿、同步状态不能默认为远程最新远程数据服务端权威结果弱网下不能阻塞页面可用性如果这三层没有统一协调页面就会出现两种极端要么每次都等远程接口体验慢要么只相信缓存数据旧而不自知。2. 资料定位和版本边界设计缓存时建议同时关注官方数据持久化能力和项目已有数据访问层。资料用途华为开发者文档中心https://developer.huawei.com/consumer/cn/doc/确认数据管理、文件、应用上下文等能力入口HarmonyOS 指南https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/查 Preferences、关系型数据库、文件等持久化说明项目 Repository 层确认页面是否绕过仓库直接读写缓存接口返回字段确认是否有版本号、更新时间、删除标记本文示例重点展示一致性策略不绑定某个数据库 API。落地时把LocalStore的读写替换成项目实际的持久化实现即可。3. 先给缓存数据加版本字段没有版本字段就很难判断远程数据和本地数据谁更新。最低限度要有更新时间、来源和同步状态。typeCacheSourcememory|local|remote;typeSyncStateclean|dirty|syncing|conflict;interfaceCacheEntryTData{key:string;data:TData;version:number;updatedAt:number;source:CacheSource;syncState:SyncState;}interfaceUserProfile{userId:string;nickname:string;avatarUrl:string;bio:string;}version用于服务端和本地比较updatedAt用于没有版本号时兜底判断syncState告诉页面数据是否干净。它防止“旧缓存覆盖新数据”也防止本地草稿被远程旧数据冲掉。4. 内存缓存只负责快不负责可靠内存缓存适合减少同一页面或同一会话里的重复读取。它不应该承担离线恢复也不应该长期保存业务状态。classMemoryCacheTData{privatereadonlystorenewMapstring,CacheEntryTData();get(key:string):CacheEntryTData|undefined{returnthis.store.get(key);}put(entry:CacheEntryTData):void{this.store.set(entry.key,{...entry,source:memory,});}remove(key:string):void{this.store.delete(key);}}这里的边界很清楚内存层只提升读取速度不决定数据是否权威。页面打开时可以先读它但后面仍然要经过本地和远程校验。5. 本地存储保存“最后可用状态”持久化层的价值是离线可用和失败可恢复。示例里用内存模拟本地存储项目中可以替换为关系型数据库或 Preferences。classLocalProfileStore{privatereadonlyrowsnewMapstring,CacheEntryUserProfile();asyncread(userId:string):PromiseCacheEntryUserProfile|undefined{returnthis.rows.get(profile:${userId});}asyncsave(entry:CacheEntryUserProfile):Promisevoid{this.rows.set(entry.key,{...entry,source:local,});}asyncmarkDirty(userId:string,profile:UserProfile):Promisevoid{constkeyprofile:${userId};constoldthis.rows.get(key);awaitthis.save({key,data:profile,version:old?.version??0,updatedAt:Date.now(),source:local,syncState:dirty,});}}markDirty很重要用户本地修改后不能等同于远程已同步。它告诉后续同步器这条数据需要上传或冲突处理。6. 远程数据源只返回结果不直接改页面远程层最好不要直接操作 UI也不要直接写内存缓存。它只负责拿到服务端状态。interfaceRemoteProfileResult{profile:UserProfile;version:number;serverTime:number;}classRemoteProfileSource{asyncfetchProfile(userId:string):PromiseRemoteProfileResult{// 实际项目中替换为网络请求层例如统一 HttpClient。returnawaitPromise.resolve({profile:{userId,nickname:HarmonyUser,avatarUrl:https://example.com/avatar.png,bio:offline first profile,},version:12,serverTime:Date.now(),});}}这一层的输入是userId输出是带版本的远程结果。它不判断是否覆盖本地因为覆盖规则属于同步协调器。7. 同步协调器负责合并不让各层互相覆盖真正决定一致性的地方是比较本地状态和远程状态。classProfileSyncCoordinator{merge(local:CacheEntryUserProfile|undefined,remote:RemoteProfileResult):CacheEntryUserProfile{constremoteEntry:CacheEntryUserProfile{key:profile:${remote.profile.userId},data:remote.profile,version:remote.version,updatedAt:remote.serverTime,source:remote,syncState:clean,};if(!local){returnremoteEntry;}if(local.syncStatedirtylocal.versionremote.version){return{...local,syncState:conflict,};}if(remote.versionlocal.version){returnremoteEntry;}return{...local,syncState:local.syncStatesyncing?clean:local.syncState,};}}这段代码防止两类事故远程旧数据覆盖本地修改、本地旧缓存挡住远程新数据。实际项目可以把冲突策略做得更细例如按字段合并、弹窗让用户选择、或者上报冲突事件。8. Repository 让页面先可用再刷新页面不应该自己协调三层缓存。Repository 可以先返回本地可用数据再触发远程刷新。interfaceProfileSnapshot{entry?:CacheEntryUserProfile;refreshing:boolean;message:string;}classProfileRepository{constructor(privatereadonlymemory:MemoryCacheUserProfile,privatereadonlylocal:LocalProfileStore,privatereadonlyremote:RemoteProfileSource,privatereadonlycoordinator:ProfileSyncCoordinator){}asyncload(userId:string):PromiseProfileSnapshot{constkeyprofile:${userId};constmemoryEntrythis.memory.get(key);if(memoryEntry){return{entry:memoryEntry,refreshing:true,message:展示内存数据后台刷新中};}constlocalEntryawaitthis.local.read(userId);return{entry:localEntry,refreshing:true,message:localEntry?展示本地缓存后台刷新中:正在加载远程数据,};}asyncrefresh(userId:string):PromiseCacheEntryUserProfile{constlocalEntryawaitthis.local.read(userId);constremoteResultawaitthis.remote.fetchProfile(userId);constmergedthis.coordinator.merge(localEntry,remoteResult);awaitthis.local.save(merged);this.memory.put(merged);returnmerged;}}load和refresh分开是为了让首屏先可用远程刷新失败时也不清空页面。页面可以先渲染load的结果再在合适时机调用refresh。9. 页面状态要展示数据来源缓存页面最容易忽略“数据来源提示”。用户需要知道当前看到的是最新数据还是缓存数据。typeProfileViewState|{type:loading;text:string}|{type:profile;profile:UserProfile;badge:string;conflict:boolean}|{type:empty;text:string}|{type:error;text:string;keepOldData:boolean};functiontoProfileView(snapshot:ProfileSnapshot):ProfileViewState{if(!snapshot.entry){return{type:loading,text:snapshot.message};}constbadgesnapshot.entry.sourceremote?最新数据:snapshot.entry.sourcelocal?本地缓存:内存缓存;return{type:profile,profile:snapshot.entry.data,badge,conflict:snapshot.entry.syncStateconflict,};}这段代码连接 Repository 和 UI。它不处理数据库也不处理网络只把缓存来源转成页面可以展示的状态。这样用户看到缓存时不会误以为是远程最新状态。10. 写入时不要立刻假装同步成功用户编辑资料后常见错误是先改 UI然后直接标记成功。更稳的做法是本地先标脏再异步同步远程。classProfileCommandService{constructor(privatereadonlylocal:LocalProfileStore,privatereadonlymemory:MemoryCacheUserProfile){}asyncupdateNickname(userId:string,nickname:string):PromiseCacheEntryUserProfile{constoldawaitthis.local.read(userId);constprofile:UserProfile{userId,nickname,avatarUrl:old?.data.avatarUrl??,bio:old?.data.bio??,};awaitthis.local.markDirty(userId,profile);constdirtyawaitthis.local.read(userId);if(!dirty){thrownewError(LOCAL_WRITE_FAILED);}this.memory.put(dirty);returndirty;}}这里的边界是“本地写入成功”不是“远程同步成功”。页面可以显示“已保存到本地等待同步”这比直接提示成功更诚实也更容易处理弱网。11. 一致性验证动作缓存逻辑必须用场景验证不能只看代码。验证场景操作预期结果首次进入清空本地数据后打开页面进入 loading远程成功后写入本地二次进入保留本地数据后断网打开显示本地缓存并提示缓存来源远程更新本地版本 10远程版本 12合并远程数据状态为 clean本地未同步本地 dirty远程版本不高标记 conflict不覆盖本地修改刷新失败本地有数据远程请求失败保留旧数据提示刷新失败建议把这些场景写成 Repository 层测试。不要只在页面手动点因为页面测试很难覆盖版本冲突。12. 缓存问题排查表现象优先看哪里可能修复方式页面数据闪回旧值version比较是否正确远程版本高才覆盖本地用户修改被覆盖本地dirty是否保留冲突时不要直接写远程结果离线页面空白是否先读本地持久化load阶段先返回本地数据刷新失败后列表清空catch 中是否清空 state失败时保留旧快照多页面数据不一致是否绕过 Repository禁止页面直接读写缓存排查时先找“谁写了数据”。如果页面、网络层、本地层都能写同一个缓存问题很难稳定复现。13. 上线前缓存验收清单每个缓存实体都有key/version/updatedAt/source/syncState。页面只通过 Repository 读取业务数据。远程刷新失败不会清空已有可用数据。本地 dirty 数据不会被远程旧版本覆盖。UI 能展示“本地缓存、内存缓存、最新数据”等来源。缓存冲突有明确处理策略不静默丢弃用户修改。首次进入、二次进入、断网、远程更新、冲突合并都有验证记录。缓存一致性专项证据包读写冲突要能复盘缓存问题通常不是“有没有缓存”而是内存、本地和远程三层状态不一致。补强时要记录读取来源、数据版本、写入时间和冲突处理结果。证据字段作用source判断读的是内存、本地还是远程dataVersion判断是否被旧数据覆盖writeAt判断写入顺序conflictPolicy判断冲突如何解决interfaceCacheConsistencyEvidence{key:stringsource:memory|local|remotedataVersion:numberwriteAt:number}functionisNewerCache(a:CacheConsistencyEvidence,b:CacheConsistencyEvidence):boolean{if(a.dataVersion!b.dataVersion)returna.dataVersionb.dataVersionreturna.writeAtb.writeAt}这段代码的边界是缓存冲突判断防止旧数据在异步回写时覆盖新数据。14. 小结缓存一致性靠协调器不靠多写 ifHarmonyOS 应用做缓存时真正需要设计的是数据流向。内存缓存负责快本地持久化负责可恢复远程数据负责权威状态同步协调器负责比较和合并。只要各层职责清楚页面就能做到先可用、再刷新、失败不清空、冲突不覆盖。