使用EventSourcing(NodeJS,MongoDB,JSON)在多个偶尔连接的客户端之间同步数据

时间:2017-02-28 10:37:10

标签: javascript node.js mongodb synchronization event-sourcing

我在服务器和多个客户端之间实现数据同步时遇到问题。 我读到了关于事件采购的内容,我想用它来完成同步部分。

我知道这不是一个技术问题,而是一个概念的问题。

我只是将所有活动直播发送到服务器,但客户端设计为不时脱机使用。

这是基本概念: Visual Concept

服务器存储每个客户端应了解的所有事件,它不会重播这些事件以提供数据,因为主要目的是同步客户端之间的事件,使其能够重放所有事件本地活动。

客户拥有一个JSON存储,同时保留所有事件重建存储/同步事件中的所有不同集合。

由于客户端可以脱机修改数据,因此拥有一致的同步周期并不重要。考虑到这一点,服务器应该在合并不同事件时处理冲突,并在发生冲突时询问特定用户。

因此,我的主要问题是确定客户端和服务器之间的差异,以避免将所有事件发送到服务器。我也遇到了同步过程顺序的问题:首先推送更改,首先提取更改?

我目前构建的是服务器端的默认MongoDB实现,它在我的所有查询中隔离特定用户组的所有文档(目前仅处理身份验证和服务器端数据库工作)。 在客户端上,我构建了一个围绕NeDB存储的包装器,使我能够拦截所有查询操作,以创建和管理每个查询的事件,同时保持默认查询行为不变。我还通过实现客户端生成的自定义ID并且是文档数据的一部分来补偿neDB和MongoDB的不同ID系统,因此重新创建数据库不会弄乱ID(同步时,这些ID应该在所有客户中保持一致)。

事件格式如下所示:

{
   type: 'create/update/remove',
   collection: 'CollectionIdentifier',
   target: ?ID, //The global custom ID of the document updated
   data: {}, //The inserted/updated data
   timestamp: '',
   creator: //Some way to identify the author of the change
}

为了在客户端上节省一些内存,我将在特定数量的事件中创建快照,这样就可以更有效地重放所有事件。

所以,缩小问题范围:我能够在客户端重播事件,我还能够在客户端和服务器端创建和维护事件,合并事件在服务器端也不应该是一个问题,也不能用现有工具复制整个数据库,因为我只是同步数据库的某些部分(甚至不是整个集合,因为文档被分配了应该同步的不同组)

但我遇到的问题是

  • 确定在同步时从客户端发送的事件的过程(避免发送重复事件,甚至是所有事件)
  • 确定要发送回客户端的事件(避免发送重复事件,甚至发送所有事件)
  • 同步事件的正确顺序(推/拉变化)

我想问的另一个问题是,将更新直接存储在类似修订版的文档中是否更有效?

如果我的问题不清楚,重复(我发现了一些问题,但他们在我的方案中没有帮助我)或者缺少某些东西,请发表评论,我会尽可能地保留它以保持它很简单,因为我刚写完了所有内容,可以帮助你理解这个概念。

提前致谢!

2 个答案:

答案 0 :(得分:3)

我认为避免所有事件顺序和重复问题的最佳解决方案是使用pull方法。通过这种方式,每个客户端都维护其上次导入的事件状态(例如,使用跟踪器),并向服务器询问在最后一个之后生成的事件。

一个有趣的问题是检测业务不变量的中断。为此,您可以在客户端上存储已应用命令的日志,如果发生冲突(事件由其他客户端生成),您可以从命令日志重试命令的执行。你需要这样做,因为一些命令在重新执行后不会成功;例如,客户端在其他用户同时删除该文档后保存文档。

答案 1 :(得分:3)

这是一个非常复杂的主题,但我会尝试某种形式的答案。

我看到您的图表时的第一反应是考虑分布式数据库如何在它们之间复制数据并在一个节点发生故障时进行恢复。这通常是通过gossiping完成的。

八卦轮确保数据保持同步。时间戳修订保留在两端按需合并,例如节点重新连接时,或者仅在给定间隔(通过套接字等发布批量更新)。

像Cassandra或Scylla这样的数据库引擎每次合并使用3条消息。

演示:

节点A中的数据

{ id: 1, timestamp: 10, data: { foo: '84' } }
{ id: 2, timestamp: 12, data: { foo: '23' } }
{ id: 3, timestamp: 12, data: { foo: '22' } }

节点B中的数据

{ id: 1, timestamp: 11, data: { foo: '50' } }
{ id: 2, timestamp: 11, data: { foo: '31' } }
{ id: 3, timestamp: 8, data: { foo: '32' } }

第1步:SYN

它列出了所有文档的ID和最后一个upsert时间戳(随意改变这些数据包的结构,这里我使用详细的JSON来更好地说明这个过程)

Node A -> Node B

[ { id: 1, timestamp: 10 }, { id: 2, timestamp: 12 }, { id: 3, timestamp: 12 } ]

第2步:确认

收到此数据包后,Node B将收到的时间戳与它自己的时间戳进行比较。对于每个文档,如果它的时间戳较旧,只需将其放在ACK有效负载中,如果它更新,则将其与其数据一起放置。如果时间戳是相同的,那么什么都不做 - 显然。

Node B -> Node A

[ { id: 1, timestamp: 11, data: { foo: '50' } }, { id: 2, timestamp: 11 }, { id: 3, timestamp: 8 } ]

第3步:确认

如果提供了ACK数据,节点A会更新它的文档,然后将最新数据发送回节点B,用于那些没有提供ACK数据的数据。

Node A -> Node B

[ { id: 2, timestamp: 12, data: { foo: '23' } }, { id: 3, timestamp: 12, data: { foo: '22' } } ]

这样,两个节点现在都可以通过两种方式合并最新数据(如果客户端进行了离线工作) - 无需发送所有文档。

在您的情况下,您的真实来源是您的服务器,但您可以轻松地在WebRTC的客户端之间实现点对点的闲聊。

希望这在某种程度上有所帮助。

Cassandra training video

Scylla explanation