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

18

我正在解决一个在服务器和多个客户端之间实现数据同步的问题。我了解到事件溯源(Event Sourcing)的概念,并希望使用它来完成同步部分。

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

我本来想将所有事件即时发送到服务器,但客户端被设计成有时离线使用。

这是基本概念:Visual Concept

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

客户端有自己的JSON存储,也保存所有事件从存储/同步的事件中重建所有不同的集合。

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

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

我目前构建的是服务器端的默认MongoDB实现,在所有查询中隔离特定用户组的所有文档(当前仅处理身份验证和服务器端数据库工作)。在客户端,我构建了一个NeDB存储器(wrapper),使我能够拦截所有查询操作来创建和管理每个查询的事件,同时保持默认查询行为不变。我还通过实现由客户端生成并成为文档数据一部分的自定义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个回答

6
这是一个非常复杂的主题,但我会尝试回答一些问题。
看到您的图表时,我的第一反应是想到分布式数据库如何在节点之间复制数据并在某个节点宕机时进行恢复。这通常通过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和最后更新时间戳(可以随意更改这些数据包的结构,在此我使用冗长的JSON以更好地说明过程)。

节点A -> 节点B

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

步骤2:确认

收到该数据包后,节点B会将接收到的时间戳与自己的进行比较。对于每个文档,如果它的时间戳较旧,则将其放入确认负载中;如果时间戳较新,则将其与其数据一起放置。如果时间戳相同,则不执行任何操作。

节点B -> 节点A

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

第三步:ACK2

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

节点A -> 节点B

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

那样,现在两个节点都合并了最新的数据(以防客户端离线工作) - 而不必发送所有文档。

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

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

Cassandra培训视频

Scylla解释


1
您可以根据自己的需要创建业务规则以确定应该传输什么。您可以查看客户端存储中存储的最新时间戳,并将其作为预同步步骤发送,以便只获取最近的更改。另一个选项是按主题标记流言轮,并在给定时间仅同步所需的主题。由您决定。 - NodeNodeNode
是的,这正是我所需要的,太棒了!然后我会将其与主题结合,以便可以独立同步不同的存储并过滤掉不应该同步的数据。 - Joschua Schneider
这也使我能够将来自不同用户组的所有数据存储在服务器上的一个集合中,非常方便! - Joschua Schneider
1
很高兴能帮忙,我很想知道这件事的进展 :) - NodeNodeNode
1
一旦实现了,我会更新我的最终概念,以便在我的用例中处理这个问题,因为这很难研究 :) - Joschua Schneider
显示剩余5条评论

3
我认为避免所有事件顺序和重复问题的最佳解决方案是使用拉取方法。这样,每个客户端都会维护其最后导入的事件状态(例如带有跟踪器),并要求服务器提供在该状态之后生成的事件。
一个有趣的问题将是检测业务不变量的破坏。为此,您可以在客户端上存储已应用命令的日志,并在出现冲突(其他客户端生成了事件)的情况下,可以从命令日志中重试执行命令。您需要这样做,因为重新执行某些命令可能不会成功;例如,一个客户端在另一个用户同时删除文档后保存该文档。

谢谢您的回答!那么,当每个客户端从服务器获取最新状态时,客户端何时或如何将其更改推送到服务器?我更喜欢在客户端(或导致冲突的特定客户端)上完全解决冲突,因此在这种情况下,客户端命令日志是一个不错的细节。 - Joschua Schneider
就像在Git中一样,你先拉取,解决冲突,然后再推送。 - Constantin Galbenu

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