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


224

我感觉那里一定有客户端-服务器同步模式。但是我完全无法用谷歌搜索一个。

情况非常简单-服务器是中心节点,多个客户端连接到该节点并处理相同的数据。可以将数据拆分为原子,如果发生冲突,则服务器上的任何内容都具有优先级(以避免使用户陷入冲突的解决)。由于可能存在大量数据,因此首选部分同步。

对于这种情况,是否有任何模式/良好做法,或者如果您不了解,则将采取什么方法?

下面是我现在想解决的想法:与数据并行,将保存修改日记帐,并为所有交易加上时间戳。客户端连接后,它将以合并的形式接收自上次检查以来的所有更改(服务器遍历列表并删除添加的内容,然后进行删除,合并每个原子的更新等)。瞧,我们是最新的。

另一种方法是保留每个记录的修改日期,而不是执行数据删除,而只需将它们标记为已删除。

有什么想法吗?


27
同意很少讨论这种事情的模式...即使这种情况很常见
Jack Ukleja 2010年

Answers:


88

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

您有几个用例。

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

  • 客户端失去同步。通过备份/还原或由于错误。在这种情况下,客户端需要从服务器获取当前状态,而无需检查增量。这是从主副本到细节副本,增量和性能的副本。这是一次性的事情;客户坏了;不要尝试对此进行优化,只需实施可靠的副本即可。

  • 客户可疑。在这种情况下,您需要将客户端与服务器进行比较,以确定客户端是否是最新的并且需要任何增量。

您应该遵循数据库(和SVN)设计模式,对每个更改进行顺序编号。这样,客户端可以在尝试进行同步之前提出一个琐碎的请求(“我应该拥有哪个修订版本?”)。即使这样,对于客户端和服务器来说,查询(“自2149年以来的所有增量”)仍然非常简单。


先生,您可以解释一下确切的增量是多少?我的猜测是这是哈希/时间戳的组合...我想听听您的先生。
阿尼斯(Anis)

增量是指两个修订之间的更改。例如,如果用户名已更改,则变化量可能类似于{修订版:123,名称:“ John Doe”}
dipole_moment

31

作为团队的一部分,我做了很多涉及数据同步的项目,因此我应该有能力回答这个问题。

数据同步是一个很宽泛的概念,讨论的方法太多了。它涵盖了各种不同的方法,它们都有其优点和缺点。这是基于两个角度的可能分类之一:同步/异步,客户端/服务器/对等。同步的实现严重取决于这些因素,数据模型的复杂性,传输和存储的数据量以及其他要求。因此,在每种特定情况下,都应选择满足应用程序要求的最简单实现。

基于对现有现货解决方案的回顾,我们可以描述几种主要的同步类,这些类的粒度取决于要同步的对象:

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

因此,我们将我们的知识带入了这篇文章,对于对主题=>基于核心数据的iOS应用程序中的数据同步(http://blog.denivip.ru/index.php/2014/04 / data-syncing-in-core-data-based-ios-apps /?lang = zh-CN


3
^^^^^^^到目前为止,这是最好的答案,伙计们!
hgoebl

我同意,丹尼斯(Denis)带来了很多话题+文章链接很棒。还讨论了DanielPaull提到的旧约。S.Lott的回答是好的,但是要深入得多。
克里斯汀(Krystian),

28

您真正需要的是操作转换(OT)。在许多情况下,这甚至可以解决冲突。

这仍然是一个活跃的研究领域,但是周围有各种OT算法的实现。我已经从事此类研究多年了,所以让我知道这条路线是否对您感兴趣,我很乐意将您投入相关资源。


7
丹尼尔(Daniel),指向相关资源的指针,将不胜感激。
帕兰德

4
我只是重新阅读了维基百科的文章。它已经走了很长一段路,并且在页面底部有许多相关的参考资料。我会向您指出孙承正的工作-维基百科引用了他的工作。en.wikipedia.org/wiki/Operational_transformation。希望有帮助!
丹尼尔·保罗

13

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


2
序列号是您的朋友。考虑持久性消息队列。
丹尼尔·保罗

7

大约8年前,我为一个应用程序构建了这样的系统,我可以分享随着应用程序使用率的增长而演变的几种方式。

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

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

最终,我了解到UUID可以避免这种情况,但是到那时我的数据库已经变得很大,而且我担心完整的UUID实施会造成性能问题。因此,我没有使用完整的UUID,而是开始使用随机生成的8个字符的字母数字键作为ID,并且将现有代码保留在适当的位置以处理冲突。在我当前的8个字符的键和UUID的36个字符之间的某个位置,必须有一个甜点,可以消除冲突而不会造成不必要的膨胀,但是由于我已经有了冲突解决代码,因此尝试该冲突并不是优先考虑的事情。 。

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

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

如果我今天从头开始,我将跳过ID冲突检查,而只瞄准足以消除冲突的密钥长度,并进行某种错误检查,以防万一。但是历史记录表以及最近更新的增量下载或需要时进行的完整下载的组合一直运行良好。


1

对于增量(更改)同步,可以使用pubsub模式将更改发布回所有订阅的客户端,诸如pusher之类的服务可以做到这一点。

对于数据库镜像,某些Web框架使用本地微型数据库将服务器端数据库同步到浏览器数据库中的本地数据库,支持部分同步。检查计量器

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.