
HarmonyOS 7 API 26 多端数据冲突实战LWW 为什么会丢字段如何保住手机和平板的同时修改手机把任务标题从“需求评审”改成“周五需求评审”平板几乎同时把负责人从“小王”改成“小李”。两次修改碰巧落在同一条记录上网络恢复后页面只剩其中一次修改。日志没有报错、同步也显示成功但数据还是丢了。这类问题的根源不是 HarmonyOS 没有同步数据而是应用把“数据到达”误当成“业务冲突已经解决”。分布式存储常见的最后写入获胜LWW只能决定整条记录最终保留哪个版本无法知道手机改的是 title、平板改的是 owner。本文基于 HarmonyOS 7.0 / API 26把检测、决策和回放拆开用两个可以独立运行的案例说明什么时候可以自动合并什么时候必须让用户选择。先明确系统能力和应用责任HarmonyOS 的 distributedDataObject 可以让相同应用、相同 sessionId 的对象在可信设备间同步并通过 change 和 status 事件通知数据或设备状态变化。它降低了连接、发送和重试的工作量但不会替应用理解字段的业务含义。边界系统或框架负责应用仍要负责设备发现与可信关系Distributed Service Kit权限说明、无设备和掉线提示数据传输与变更通知distributedDataObject记录身份、冲突检测和合并策略默认冲突收敛存储层按规则保留一个版本判断覆盖是否会丢失有效修改页面刷新change 事件给出变化字段去重、自写回声过滤和状态还原官方文档还给出了几个容易忽略的约束同一应用跨设备需要相同 bundleName对象需要相同 sessionId复杂类型通常以根属性作为最小修改单元单个分布式对象和实例数量都有上限。也就是说把整份文档塞成一个巨大 JSON 字符串不但放大同步量还会把原本互不冲突的字段修改变成整块覆盖。问题怎么稳定复现准备一条初始记录{ id: task-42, title: 需求评审, owner: 小王, done: false, version: 1 }然后让手机和平板都在 version1 时断网编辑1. 手机只修改 title产生版本 A。2. 平板只修改 owner产生版本 B。3. 两台设备恢复网络并同步。4. 如果直接按 updatedAt 取较新记录最终只能保住 A 或 B。更隐蔽的现象是页面短暂显示过正确值随后又被远端旧回调覆盖。原因通常是本地保存触发了一次 change远端同步又触发一次 change而页面没有区分提交来源和基线版本。不要只比较更新时间设备时间并不一定完全一致单独使用 Date.now() 会引入时钟漂移。更稳妥的做法是给每次修改附带三类信息baseVersion编辑开始时看到的公共版本。changedFields本次真正修改过的字段集合。logicalClock每台设备自己的单调递增计数。下面是文章示例使用的最小变更包type DeviceId phone | tablet; interface TaskRecord { id: string; title: string; owner: string; done: boolean; version: number; } interface TaskPatch { recordId: string; baseVersion: number; deviceId: DeviceId; logicalClock: number; changedFields: Arraykeyof PickTaskRecord, title | owner | done; values: PartialTaskRecord; }logicalClock 不负责判断真实时间先后它只保证同一设备上的第二次操作一定晚于第一次。跨设备是否冲突仍然要看 baseVersion 和 changedFields。核心实现先检测再决定字段集合没有交集时可以自动合并修改同一字段时不能悄悄覆盖。下面的函数没有依赖 UI 和设备 API可以先作为纯逻辑测试跑通。interface MergeResult { merged?: TaskRecord; conflicts: string[]; reason: string; } function intersect(left: string[], right: string[]): string[] { const rightSet new Set(right); return left.filter((item: string) rightSet.has(item)); } export function mergeConcurrentPatches( base: TaskRecord, first: TaskPatch, second: TaskPatch ): MergeResult { if (first.recordId ! base.id || second.recordId ! base.id) { return { conflicts: [recordId], reason: 补丁不属于当前记录 }; } if (first.baseVersion ! base.version || second.baseVersion ! base.version) { return { conflicts: [baseVersion], reason: 至少一端不是从公共基线开始编辑 }; } const conflicts intersect( first.changedFields as string[], second.changedFields as string[] ); if (conflicts.length 0) { return { conflicts, reason: 两端修改了同一字段需要业务决策 }; } const merged: TaskRecord { ...base, ...first.values, ...second.values, version: base.version 1 }; return { merged, conflicts: [], reason: 字段互不重叠已自动合并 }; }这里故意没有使用“时间更大的一端赢”。对于不同字段两个修改都有效覆盖任何一端都是数据损失对于同一字段谁赢取决于业务而不是谁的设备时间快了几十毫秒。案例一标题和负责人同时修改手机修改 title平板修改 owner二者的 changedFields 没有交集可以自动合并。const base: TaskRecord { id: task-42, title: 需求评审, owner: 小王, done: false, version: 1 }; const phonePatch: TaskPatch { recordId: task-42, baseVersion: 1, deviceId: phone, logicalClock: 7, changedFields: [title], values: { title: 周五需求评审 } }; const tabletPatch: TaskPatch { recordId: task-42, baseVersion: 1, deviceId: tablet, logicalClock: 12, changedFields: [owner], values: { owner: 小李 } }; const result mergeConcurrentPatches(base, phonePatch, tabletPatch); console.assert(result.merged?.title 周五需求评审); console.assert(result.merged?.owner 小李); console.assert(result.merged?.version 2);合并结果只增加一次公共版本。随后把合并后的快照写回同步对象并记录由哪个 mergeId 产生其他设备收到相同 mergeId 时只刷新 UI不再生成新补丁避免两端互相回写形成循环。案例二两端同时修改标题如果手机和平板都修改 title自动拼接通常会产生毫无意义的文本。这时应返回冲突对象让页面展示两个候选值并保留原始基线。interface FieldConflict { field: string; baseValue: string; phoneValue: string; tabletValue: string; } function buildTitleConflict( base: TaskRecord, phone: TaskPatch, tablet: TaskPatch ): FieldConflict { return { field: title, baseValue: base.title, phoneValue: String(phone.values.title ?? base.title), tabletValue: String(tablet.values.title ?? base.title) }; }页面可以提供“保留手机版本”“保留平板版本”和“手动编辑”三个选择但不能默认弹出后立刻选第一项。用户确认后生成一个 baseVersion1、version2 的 resolution 记录再同步到两端。对于 done 这类布尔字段也不要一概使用后写入覆盖。协作任务中“已完成”可能优先于“未完成”审批场景则可能必须保留撤销操作。字段策略应该集中配置而不是散落在每个页面里。接到 distributedDataObject 时怎么组织同步对象适合承载小而明确的协同状态。下面只展示结构和监听边界实际项目还要根据设备拉起流程传递相同 sessionId。import { distributedDataObject } from kit.ArkData; interface SyncEnvelope { recordJson: string; patchJson: string; mergeId: string; } export class CollaborativeTaskChannel { private object distributedDataObject.create(this.context, { recordJson: , patchJson: , mergeId: } as SyncEnvelope); private appliedMergeIds: Setstring new Set(); constructor(private context: Context) {} async join(sessionId: string): Promisevoid { this.object.on(change, this.onChange); this.object.on(status, this.onStatus); await this.object.setSessionId(sessionId); } private onChange (_sessionId: string, fields: string[]): void { const mergeId String(this.object.mergeId ?? ); if (mergeId this.appliedMergeIds.has(mergeId)) { return; } // 只解析 fields 涉及的数据并送入冲突检测器。 }; private onStatus ( _sessionId: string, _networkId: string, status: online | offline ): void { console.info(collaboration status status); }; async leave(): Promisevoid { this.object.off(change, this.onChange); this.object.off(status, this.onStatus); await this.object.setSessionId(); } }复杂对象的子字段修改边界容易让人误判所以示例把 record 和 patch 序列化为根字段并明确由应用层合并。更大的业务数据不要长期堆进一个对象分布式对象负责当前协同会话小型快照落本地 RDB需要跨账号或长期多端一致时再评估端云同步。三种方案怎么选方案优点主要风险适合场景整条记录 LWW实现最少、收敛快不同字段也会互相覆盖临时状态、覆盖无损的数据字段级补丁 公共基线能自动合并互不相干的修改需要维护版本和补丁日志表单、任务、轻量协作本文推荐CRDT 或服务端事务并发能力强、可跨多会话模型和调试成本更高长期文档、多人实时编辑不要因为听到“协同”就直接上完整 CRDT。两到三台设备、字段数量有限、冲突频率不高时字段级补丁已经能解决大部分真实问题也更容易解释和测试。一组可执行测试至少覆盖以下六条而不是只验证一次正常同步const sameField: TaskPatch { recordId: task-42, baseVersion: 1, deviceId: tablet, logicalClock: 13, changedFields: [title], values: { title: 下周需求评审 } }; console.assert(mergeConcurrentPatches(base, phonePatch, tabletPatch).conflicts.length 0); console.assert(mergeConcurrentPatches(base, phonePatch, sameField).conflicts[0] title); console.assert(mergeConcurrentPatches( { ...base, id: other }, phonePatch, tabletPatch ).conflicts[0] recordId);测试条件预期结果两端修改不同字段自动合并两项修改都保留两端修改同一字段返回字段冲突不静默覆盖一端基线版本落后拒绝直接合并先拉取新快照网络离线后恢复补丁按逻辑时钟回放且只应用一次设备反复上下线监听器数量不增加状态可见相同 mergeId 重复到达UI 只刷新一次不生成回写循环本地纯逻辑测试通过后还要在两台真实设备上验证权限拒绝、可信关系解除、切后台再恢复、同一局域网中断和应用重启。分布式设备管理当前不支持模拟器不能把单机 Preview 当成跨设备验证完成。上线前检查清单同一协同会话只生成一次 sessionId并明确由哪一端传给对端。不使用设备墙钟作为唯一冲突依据。每个补丁包含 recordId、baseVersion、changedFields、deviceId 和 logicalClock。不同字段才自动合并同字段冲突必须执行明确策略。change 回调能识别自己写回的 mergeId避免同步回声。页面销毁或会话结束时注销 change/status 监听并退出 session。大数据和长期数据不塞进单个分布式对象。真机覆盖掉线、重连、重复通知和版本落后场景。最后的判断多设备同步成功只能证明数据到达了没有丢修改才说明协同逻辑正确。LWW 适合不怕覆盖的瞬时状态却不适合直接处理用户编辑内容。把每次操作变成带公共基线和字段集合的补丁后应用就能自动合并互不冲突的修改把真正冲突的字段交给业务决策。这套检测器与 HarmonyOS 页面无关可以封装成独立模块并通过纯逻辑测试验证distributedDataObject 只承担会话内传输与通知。职责拆开以后问题发生时也能快速判断是设备离线、消息重复、基线落后还是合并策略本身不完整。参考资料HarmonyOS SDK 26.0.0 Release 版本概览2026-08-29 更新https://developer.huawei.com/consumer/cn/doc/doccenter-release-notes/overview-2600分布式设备管理开发指南2026-09-09 更新https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/devicemanager-guidelines分布式数据对象跨设备同步2025-05-20 更新https://developer.huawei.com/consumer/en/doc/harmonyos-guides-V5/data-sync-of-distributed-data-object-V5ArkData 能力入口2026-04-24 更新https://developer.huawei.com/consumer/cn/doc/doccenter-capabilities/arkdata同应用端云数据同步概述2026-06-09 更新https://developer.huawei.com/consumer/cn/doc/doccenter-capabilities/data-cloud-sync-overview