多设备协同编辑,是鸿蒙系统里非常吸引人的特性。比如你在折叠屏上列了一个购物清单,手机端也能实时看到并修改。但真机测试时,一旦网络稍微抖动,断开又重连,两边的数据经常会打架。要么手机上的新修改被平板上几分钟前的旧数据给覆盖了,要么两边的界面各自显示一套完全不同的清单。你以为是分布式同步组件出了Bug,其实是你忽略了分布式数据对象在断连重连时的默认规则。

我们通常用 distributedDataObject 来实现这种跨设备的状态同步。很多人以为只要把对象创建好,加个监听,剩下的框架都会搞定。实际上,它的底层设计采用的是最终一致性模型,并且官方为了保持绝对的轻量和性能,并没有内置复杂的冲突解决策略。说白了,如果两台设备在断网期间各自改了数据,网络恢复后,系统只认最后提交的那个版本,这叫“后写覆盖先写”。

分布式数据对象的最终一致性陷阱

当你调用 distributedDataObject.create 时,本地对象的修改会立即生效,然后异步同步到其他设备。这中间有一个极其危险的真空期。如果在 offline 状态下,设备A改了字段1,设备B改了字段2,一旦重新连接,设备A或B的全量数据快照就会被直接推到对端,覆盖掉对端刚刚做出的努力。

如果你只依赖框架的自动同步,在弱网环境下,很容易出现刚才说的数据错乱。更可怕的是,这种覆盖往往是静默的,用户根本收不到任何提示,只会觉得“怎么我写的突然没了”。

设计带版本号的冲突仲裁机制

既然框架不帮我们判断谁对谁错,我们就必须在业务层自己建立仲裁机制。最直接也最有效的办法,就是给同步的数据模型加上单调递增的版本号,并在监听回调里做版本比对。

typescript

class CollabData {
  version: number = 0
  lastEditor: string = ''
  content: string = ''
}

// 假设我们创建了分布式对象 dataObject
dataObject.on('change', (sessionId: string, fields: string[]) => {
  if (this.writeFrozen) {
    return // 冻结状态下直接丢弃外部变更
  }
  
  const remoteData = dataObject as CollabData
  // 只有远端版本号更大时,才采纳远端的修改
  if (remoteData.version > this.localVersion) {
    this.applyRemote(remoteData)
    this.localVersion = remoteData.version
  }
})

这段代码的核心在于 remoteData.version > this.localVersion 这一行。任何向分布式对象写入数据的操作,都必须先将 version 字段加一。当收到远端的 change 事件时,如果发现版本号没有自己高,说明这是一个迟到的旧快照,直接丢弃,绝不能用它来覆盖本地的最新状态。这样我们就用一个简单的版本号,把轻量级的“后写覆盖”变成了可控的“版本仲裁”。

网络状态监听与本地数据持久化兜底

版本号解决了冲突,但还有另一个隐患:断连时间过长。如果应用在离线状态下被系统杀掉了,内存里的数据就会彻底丢失。所以,监听连接状态是必须的。

distributedDataObject 提供了 on('status') 接口,能拿到 online、offline 和 restored 等状态。一收到 offline,我们不仅要停止接受新的跨设备修改,还要立刻把当前的数据全量持久化到本地的 Preferences 或关系型数据库里。等状态变为 restored 时,再从本地恢复,并带着最新的版本号参与同步。

分布式协同的本质,是把单机问题变成了多机问题。最终一致性给性能让步了,那业务逻辑就必须把一致性的缺补回来。版本号决定了谁的数据更有话语权,离线持久化保证了进程边界之外的数据存活,这两者缺一不可。

Logo

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

更多推荐