客户端-服务器同步模式/算法?

253

我有一种感觉,现在可能会有客户端-服务器同步模式。但是我在Google上完全没有搜到一个。

情况非常简单——服务器是中心节点,多个客户端连接并操作相同的数据。数据可以分成原子级,在冲突的情况下,以服务器上的数据为准(以避免让用户处理冲突)。部分同步由于数据量可能很大而更受欢迎。

针对这种情况,是否有任何模式/良好实践方法?如果您不知道任何解决方法,您会采取什么方法?

以下是我现在考虑解决它的方式:与数据并行,将保留修改日志,其中所有事务都带有时间戳。当客户端连接时,它会收到自上次检查以来的所有更改,以汇总形式呈现(服务器浏览列表并删除跟随删除的添加,合并每个原子的更新等)。就这样,我们保持最新状态。

另一种选择是为每条记录保留修改日期,而不是执行数据删除,只是标记它们为已删除。

你有什么想法吗?


32
同意很少有关于这类事情的模式讨论,即使这种情况相当普遍。 - Jack Ukleja
7个回答

96

您应该了解分布式变更管理的工作方式。查看SVN、CVS和其他管理增量的存储库。

您有几个用例。

  • 同步更改。您的变更日志(或增量历史记录)方法对此非常好。客户端将其增量发送到服务器;服务器合并并将增量分发给客户端。这是典型情况。数据库称之为“事务复制”。

  • 客户端已失去同步。 通过备份/还原或因为某个错误。在这种情况下,客户端需要从服务器获取当前状态,而无需经过增量。这是从主到细节的一次性拷贝,无论增量和性能如何都要拷贝。这只是一次性的操作;客户端出现故障;不要尝试优化这个过程,只需要实现可靠的拷贝。

  • 客户端持怀疑态度。 在这种情况下,您需要比较客户端和服务器,以确定客户端是否是最新的,并且是否需要任何增量。

您应该遵循数据库(和SVN)设计模式,将每个更改按顺序编号。这样,客户端可以在尝试同步之前进行微不足道的请求(“我应该有哪个修订版?”)。即使在这种情况下,查询(“自2149年以来的所有增量”)对于客户端和服务器来说都非常简单。


先生,能否请您解释一下什么是Delta?我的猜测是它是哈希/时间戳的组合...我想听听您的看法。 - Anis LOUNIS aka AnixPasBesoin
一个delta指的是两个版本之间的变更。例如,如果用户的名字改变了,那么delta可能是像{revision: 123, name: "John Doe"}这样的东西。 - dipole_moment

37

作为团队的一员,我参与了许多涉及数据同步的项目,因此我有能力回答这个问题。

数据同步是一个相当广泛的概念,需要讨论的内容太多了。它涵盖了各种不同的方法,包括它们的优缺点。下面是一种基于两个方面的可能分类:同步/异步,客户端/服务器/点对点。同步实现严重依赖于这些因素、数据模型复杂度、传输和存储的数据量以及其他要求。所以在每个特定的情况下,选择应该偏向于最简单的实现来满足应用程序的要求。

根据现有现成解决方案的审查,我们可以区分几个主要同步类别,不同的是同步对象的粒度:

  • 整个文档或数据库同步用于云应用程序,例如Dropbox、Google Drive或Yandex.Disk。当用户编辑并保存文件时,新文件版本完全上传到云中,覆盖之前的副本。如果发生冲突,则保存两个文件版本,以便用户选择哪个版本更相关。
  • 键值对同步可用于具有简单数据结构的应用程序,其中变量被视为原子的,即未分成逻辑组件。此选项与整个文档的同步类似,因为值和文档都可以完全覆盖。然而,从用户角度来看,文档是由许多部分组成的复杂对象,而键值对只是一个简短的字符串或数字。因此,在这种情况下,我们可以使用更简单的冲突解决策略,将最后更改的值视为更相关。
  • 树形或图形结构化数据同步用于更复杂的应用程序,其中数据量足够大,以至于每次更新时必须发送整个数据库。在这种情况下,必须在单个对象、字段或关系的级别上解决冲突。我们主要关注这个选项。

因此,我们将我们的知识整理在本文中,我认为对所有对感兴趣的人都非常有用。(http://blog.denivip.ru/index.php/2014/04/data-syncing-in-core-data-based-ios-apps/?lang=en


3
这绝对是最好的答案,伙计们! - hgoebl
我同意,Denis在这个话题上做出了很大的贡献+文章链接很棒。还谈到了DanielPaull提到的OT。S.Lott的回答也不错,但这篇文章更深入一些。 - Krystian

31
你真正需要的是操作变换(OT)。这甚至可以在许多情况下解决冲突。
这仍然是一个活跃的研究领域,但已经有各种OT算法的实现。我已经参与了这样的研究多年了,所以如果你对这条路感兴趣,我很乐意为你提供相关资源。

8
丹尼尔,感谢您提供相关资源的指引。 - Parand
4
我刚刚重新阅读了维基百科文章。它已经进展了很长一段时间,并在页面底部有许多相关参考资料。我可以向你指出孙成政的作品——他的作品被引用自维基百科。希望这能有所帮助! - Daniel Paull

14

这个问题不是太清楚,但如果我是你,我会研究一下乐观锁定。 可以使用服务器为每条记录返回的序列号来实现。当客户端尝试将记录保存回去时,它将包括从服务器接收到的序列号。如果序列号与数据库中更新时匹配,则允许更新并将序列号递增。如果序列号不匹配,则不允许更新。


2
序列号在这里是您的好朋友。考虑持久化消息队列。 - Daniel Paull

13

大约8年前,我为一款应用程序构建了这样的系统,并且我可以分享一些随着应用使用而发展的方法。

我开始将来自任何设备的每个更改(插入、更新或删除)记录到一个“历史记录”表中。因此,例如,如果某人在“联系人”表中更改其电话号码,则系统将编辑contact.phone字段,并添加一个带有action=update、table=contact、field=phone、record=[contact ID]、value=[new phone number]的历史记录。然后,每当设备同步时,它会下载自上次同步以来的历史记录项,并将其应用于其本地数据库。这听起来像上述的“事务复制”模式。

一个问题是在可能在不同设备上创建项目时保持ID唯一。当我开始时,我不知道UUID,所以我使用自动递增的ID并编写了一些复杂的代码在中央服务器上运行,以检查从设备上传的新ID,如果存在冲突则将其更改为唯一ID,并告诉源设备将其更改为其本地数据库中的ID。仅更改新记录的ID并不那么糟糕,但是如果我在联系人表中创建了一个新项目,然后在事件表中创建一个新的相关项目,现在我也需要检查和更新外键。

最终我学会了使用UUID可以避免这个问题,但那时我的数据库已经很大了,我担心完全实施UUID会导致性能问题。因此,我开始使用随机生成的8个字符的字母数字键作为ID,而不是使用完整的UUID,并留下了现有代码来处理冲突。在我的当前8个字符键和UUID的36个字符之间,必须有一个最佳选择,既可消除冲突又不会增加不必要的负担,但由于我已经有了解决冲突的代码,所以尝试寻找最佳选择并不是优先考虑的事情。

接下来的问题是历史记录表的大小约为整个数据库的10倍。这使得存储变得昂贵,并且对历史记录表进行任何维护都可能很痛苦。保留整个表可以让用户回滚到以前的任何更改,但这开始感觉像过度设计。因此,我添加了一个例行程序,在同步过程中,如果设备最后下载的历史记录项不存在于历史记录表中,服务器将不会提供最近的历史记录项,而是提供一个包含该账户所有数据的文件。然后,我添加了一个cronjob,用于删除90天以上的历史记录项。这意味着用户仍然可以回滚到90天以内的更改,并且如果他们每90天至少同步一次,更新将像以前一样增量进行。但是,如果超过90天没有同步,应用程序将替换整个数据库。

这种变更将历史表的大小减少了近90%,因此现在维护历史表只会使数据库变成原来的两倍,而不是十倍。该系统的另一个好处是,即使需要,同步仍然可以在没有历史记录表的情况下工作 - 就像我需要进行一些离线维护一样。或者我可以为不同价格点的帐户提供不同的回滚时间段。如果有超过90天的更改需要下载,则完整文件通常比增量格式更高效。

如果今天我从头开始,我会跳过ID冲突检查,并努力实现足以消除冲突的关键长度,并进行某种错误检查以防万一。(看起来YouTube使用11个字符的随机ID。)历史表和最近更新的增量下载组合,或在需要时进行完整下载一直运行良好。


1

对于增量同步,您可以使用pubsub模式将更改发布到所有订阅的客户端,像pusher这样的服务可以实现此功能。

对于数据库镜像,一些Web框架使用本地小型数据库将服务器端数据库同步到浏览器中的本地数据库,支持部分同步。请查看meteror


-1

本页面清晰地描述了大多数数据同步方案的模式和示例代码:数据同步:模式、工具和技术

考虑到所有增量同步、处理删除以及服务器到客户端和客户端到服务器同步策略,这是我发现的最全面的资源。它是一个非常好的起点,值得一看。


2
这是一个有趣的资源,但它是不完整的。我在购买了高级访问权限后才发现这一点。只有第一章被写出来了。看网站上的情况,自最后一次更新以来已经过去了一年多,所以不幸的是,似乎作者已经放弃了它。 - Luccas Correa

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接