多设备应用开发中,数据同步是最容易引发数据事故的环节。一台设备离线修改了文档,另一台设备同时修改了同一份文档,网络恢复后,谁覆盖谁?鸿蒙的分布式数据同步底层采用最终一致性模型,KV 单版本默认采用 LWW(最后写入胜出)策略。这意味着,如果你的业务依赖底层自动合并,稍微复杂的并发场景下必定会丢失数据。

要确保“不丢数据”,不能把命运交给系统的默认策略。开发者必须在业务层建立一套版本基线,手动接管冲突的裁决权。

LWW 策略的业务局限性

LWW 的核心判定依据是时间戳。但在多设备环境下,各设备的系统时间未必完全同步,网络延迟也会导致到达顺序与操作顺序不一致。它只适合那些“最后状态即正确状态”的场景,比如主题色的同步、开关的切换。

对于带有增量属性的数据(比如往列表里加了一条记录),LWW 的粗暴覆盖会导致其中一条记录凭空消失。此时必须引入带版本号的实体结构。

typescript

interface SyncRecord {
  id: string
  payload: Record<string, string>
  rev: number        // 当前逻辑版本
  baseRev: number    // 修改基于的版本基线
  opId: string       // 操作幂等ID
  tombstone: boolean // 墓碑标记
}

当同步端收到数据时,比对 baseRev 和本地当前的 rev。如果一致,说明本地没有发生并发修改,直接更新;如果不一致,说明两端从同一个版本出发,各自产生了修改,构成真正的冲突。

字段级合并与版本基线

检测到冲突后,直接让用户去选“保留哪个”体验极差。更好的做法是走字段级合并。遍历双方的 payload,只有同一个字段两边都有值且不相等时,才触发裁决。

typescript

function merge(local: SyncRecord, remote: SyncRecord): SyncRecord {
  if (remote.tombstone) return remote // 删除优先
  if (local.tombstone) return local

  const merged = { ...local }
  for (const key in remote.payload) {
    const lVal = local.payload[key]
    const rVal = remote.payload[key]
    if (lVal === undefined) {
      merged.payload[key] = rVal // 本地没值直接补
    } else if (lVal !== rVal) {
      // 双方都有修改,按版本高者胜出
      merged.payload[key] = remote.rev > local.rev ? rVal : lVal
    }
  }
  merged.rev = Math.max(local.rev, remote.rev) + 1
  merged.baseRev = merged.rev
  return merged
}

这样,两端各自修改不同字段的操作都能被完整保留,只有真正修改了同一字段才有一方妥协。

墓碑机制防止数据复活

硬删除在多端同步中是绝对禁忌。A 设备删除了记录,B 设备离线时还存着副本。B 上线同步后,这条记录会被当成新增数据推回云端,导致数据复活。

正确的做法是采用软删除,把 tombstone 置为 true,并递增版本号。系统只会把这个标记同步给其他端,其他端收到后隐藏该数据。等确认所有活跃设备都收到了墓碑,再由后台清理任务彻底物理删除。

幂等键的设计与验证

网络抖动导致的重发,会让同一笔操作被服务端接收多次。如果接口本身不具备幂等性(比如“创建订单”),就会产生重复的业务数据。

每次本地操作生成一个唯一的 opId(如 UUID),同步库收到请求时,先查 opId 是否处理过。处理过的直接返回成功。这样哪怕请求发了十次,真正的业务逻辑只执行一次。并发和同步永远没有银弹,明确版本水位、定义冲突规则,才是保障数据不丢失的唯一途径。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐