使用分布式和可扩展架构时,通常需要最终的一致性。
从图形上看,如何处理这种最终的一致性?
用户习惯于单击保存,并立即查看结果......最终保持一致性是不可能的。
如何处理此类场景的 GUI?
请注意,该问题适用于桌面应用程序和 Web 应用程序。
PS:我正在使用 Microsoft 平台,但我想这个问题适用于任何技术......
使用分布式和可扩展架构时,通常需要最终的一致性。
从图形上看,如何处理这种最终的一致性?
用户习惯于单击保存,并立即查看结果......最终保持一致性是不可能的。
如何处理此类场景的 GUI?
请注意,该问题适用于桌面应用程序和 Web 应用程序。
PS:我正在使用 Microsoft 平台,但我想这个问题适用于任何技术......
基于任务的UI非常适合这个模型。您可以从 UI 创建和执行任务。您还可以使用任务状态监视器之类的东西来向用户显示任务何时执行。
另一种选择是使用来自客户端的某种池。您从客户端发送命令和池,直到命令完成并且新数据可用。在某些情况下,从用户按下保存到他看到新记录的时间会有延迟,但在大多数情况下,它应该几乎是同步的。
另一个(好?)选项是假设/设计不会失败的命令。这不是微不足道的,但您可以在客户端上有一个缓存,并将命令中的数据添加到该缓存中,并在命令执行之前将其显示给用户。如果命令因某些意外情况而失败,那么只需设计一个好的“我们很抱歉”消息来误导用户几秒钟。
您也可以结合上述方法。
通常最终的一致性更多的是业务/领域问题,您应该让您的领域专家处理它。
我认为其他答案通常将 CQRS 混合在一起,特别是最终的一致性。基于任务的 UI 非常适合 CQRS,但它并不能解决最终一致性读取模型的问题。
首先,我想对你的说法提出质疑:
用户习惯于单击保存,并立即查看结果......最终保持一致性是不可能的。
你凭什么?为什么不能立即看到结果?我认为这里的问题是您对 result 的定义。
任何动作的结果是该动作已被执行。有很多方法可以证明这一点!这取决于您要完成什么样的操作。例子:
发送电子邮件:如果用户输入了正确的电子邮件地址,则几乎可以保证该操作将成功完成。为了防止意外失败,可以使用持久队列,因为这种操作不需要同步完成。因此,您只需说“已发送电子邮件”。通常,当您要求重置密码时,您会看到这种响应。
更新用户配置文件中的一些信息:在客户端上验证了新数据后,该命令很可能也会成功,因为唯一可能发生的是数据库错误(如果您使用数据库)。同样,即使这种情况也可以通过使用持久队列来缓解。在这种情况下,您只需以相同的形式显示更新的字段。SPA 的良好实践是在客户端拥有一个全面的数据存储,就像 Redux 一样。在这种情况下,您可以通过发送命令并更新客户端存储来安全地更新服务器,这将导致 UI 显示最新数据。免责声明:一些答案将这种技术称为“欺骗用户”,但我不同意这个定义。
如果您有容易出错的命令,您可以使用其他答案中已经描述的技术,如 Websockets 或服务器端事件来传达错误。这需要相当多的额外工作。您还可以发送命令并等待回复或同步执行命令。有人会说“这不是 CQRS”,但这只是另一个需要挑战的教条。结合上一点(客户端数据存储)确保命令已完成执行将是一个很好的解决方案。
我不确定是否有任何 100% 防弹技术可以让您始终显示来自读取模型的非陈旧数据。我认为这违反了 CQRS 的原则。即使使用实时事件,您也只会收到表明您编写的模型已更新的事件。尽管如此,您的预测可能已经失败,对此做出反应是另一回事。
但是,我不会太专注于这个问题。事实是,经过良好测试的预测和几乎保证的命令将非常有效。对于 90% 情况下的错误处理,有一些手动或半手动过程来从这些错误中恢复就足够了。对于最后 10%,您可以结合从服务器推送的通用“错误”消息,说“抱歉,您的操作 XXX 未能执行”,最优先的操作可能背后有一些创造性的过程,但实际上这些情况会非常非常稀有的。
有2种方式:
我更喜欢第一种选择。
正如有人已经提到的,基于任务的 UI 非常适合这一点,我要做的是采用一种“为您争取时间”的技术来传播命令。
例如,假设我们在一个列表屏幕上,用户可以在其中执行各种操作,其中之一是将新项目添加到列表中。选择添加项目后,您可以显示“您接下来想做什么?” 其中可能有“添加另一个项目”、“执行此任务”、“执行其他任务”、“返回列表”。
当他们点击一个选项时,数据有望被刷新。
此外,如果您使用的是基于任务的 UI,您可以分析任务执行的模式并使用这些“您接下来想做什么”屏幕来简化 UI。类似于亚马逊的“其他人也购买了这些物品”。
如前所述,可以告诉用户请求(命令)已被确认(成功发出)。如果出现故障,系统应通过以下方式将其传达给请求者:
例如,邮件客户端/服务:
我相信通知用户最近失败的一个好方法是在他浏览应用程序时向他显示一个错误面板。可能需要用户手势才能消除该警报等。
例如:
我不会欺骗用户或阻止他执行其他操作。在读取方确认它们之后,我宁愿将数据流向 UI。让我们考虑这两种情况:
用户保存数据并期望结果。与服务器建立连接。在它们被读取方确认后,它们将流向 UI,并且 UI 正在更新。
用户保存数据并刷新网页。重新加载后,将从数据存储中获取数据并建立流连接。如果读取端没有同时更新数据存储,那么仍然有一个打开的流,并且应该在数据到达读取端后更新 UI。
为什么从读取端而不是直接从写入端流式传输?简而言之,这将确认已到达读取端。从技术方面来看,可以使用服务器发送事件。
坏处:
结果仍不会立即被读取方反映。但至少,在大多数情况下,用户将能够继续他的工作而不会被 UI 阻止。
有几种方法可以处理最终的一致性。所有这些都是真正占用从用户操作到后端刷新的时间。
用户读取给定用户只能从他们写入的同一数据库节点读取。其他用户从复制的节点中读取。优点:用户界面足够快,并且应用程序保持同步。缺点:您的服务架构必须跟踪用户并将其路由到特定的数据库节点。
禁用 UI直到操作完成,然后刷新它。Java Server Faces 有一个典型的例子。可以创建一个带有加载微调器的模式来覆盖 UI,直到刷新完成。优点:UI 与应用程序状态保持同步。缺点:大多数操作都会创建一个被阻止的 UI。用户对受限的 UI 感到非常沮丧,并且会抱怨应用程序运行缓慢。
确认立即感谢用户提交。然后让他们稍后(电子邮件、短信、应用内通知)知道操作是否完成。优点:前面很快。缺点:用户界面滞后于系统直到刷新。即使有通知,用户也可能会因为看不到更新而感到困惑。它还需要整合各种沟通渠道。用户不会立即看到他们的更改。如果行动失败,他们可能不知道,直到为时已晚。
假装乐观地假设动作会完成。向用户显示生成的 UI(点赞、评论、信用卡确认等),并让他们像成功一样继续。如果有失败,请立即将它们显示为上下文错误:未完成的投票旁边的警报、带有失败评论的帖子的应用内警报、被拒绝信用卡的电子邮件。优点:用户界面感觉更快。缺点:UI 暂时与应用程序状态不同步,您必须解决这个问题。一种情况:您可能会使用临时 ID 伪造内容创建。但是在创建内容之后,在刷新之前临时 ID 将是错误的。第二种情况,您可能需要在操作后将所有状态更改存储在 UI 上,直到刷新。然后,您需要一些解析器来应用自发出操作以来的所有本地状态更改。这是决议是不平凡的。
Web Sockets为 UI 订阅一个事件流,以便在后端完成操作时,将其推送到前端。是单向流还是双向流?优点:UI 感觉很快,并且与应用程序状态同步。缺点:一致的浏览器支持,需要流事件的后端源,以及套接字服务器的可扩展性。