【问题标题】:Fix inconsistent state right away or lazily when data is requested请求数据时立即或延迟修复不一致的状态
【发布时间】:2021-02-27 15:54:24
【问题描述】:

我们的用户会经历几个工作流程步骤 - 他们走得越远,我们创建的对象就越多。我们还允许用户返回步骤#1 并更改现有对象之一。这可能会导致不一致,因此我们必须在第 2 步更新/删除一些对象。我看到 2 个选项:

  1. 立即从第 2 步更新/删除对象。这导致:

    • 应该是实体字段的简单 PATCH 的操作变得复杂。它是多个工作流之间的共享对象 - 因此我们必须添加 if 语句并根据工作流执行不同的操作。
    • 循环依赖。 Step#1 上的操作必须了解 Step#2 上的对象/操作。
    • 对于步骤#1 中的每个请求,我们必须为步骤#2 加载数据,以确定步骤#2 是否真的需要更新。这会减慢第 1 步的操作。因此,要更改 DB 中的 1 条记录,我们必须为第 2 步加载数百(甚至数千)条记录。
    • Step#1 上的许多操作可能需要在 Step#2 处修复状态。因此,我们必须确保我们不会忘记今天和未来的任何事情。
  2. 懒惰地修复第 2 步 - 当用户去那里时(我们当前的方法)。步骤#2 将识别对象不一致并修复它们。这导致我们只需要关心 1 个地方,但是:

    • 直到用户打开步骤#2 - DB 将包含不一致的对象。到目前为止,这还没有导致任何问题。但我可以想象它可能会使未来的 SQL 迁移变得复杂。
    • 我们根据 GET 请求更新数据库状态。这似乎没什么大不了的,因为 GET 无论如何都保持幂等性。但还是觉得别扭。

有人知道更好的方法吗?或者可能对这两个进行改进?

更新

我还没有找到完美的解决方案,但最终我们实现了 #1 的改进版本。当在 Step#1 上更新状态时,我们还设置了一个标志“需要重建 Step#2”,当 UI 打开 Step#2 时,它首先检查此标志并发出 PUT 以重建状态,然后才获取 Step#2。

这仍然意味着数据库状态在一段时间内不一致。但至少我们可以从 DB 中的标志中确定这一点。如果需要 - 我们可以考虑这个标志来编写迁移。这也允许(如果将来需要)创建一个异步作业来修复状态。

【问题讨论】:

  • 第三种选择:您可以在一夜之间运行批处理作业,以协调任何已修改工作流的状态。然后你在数据库中实现了一种最终的一致性。
  • @jaco0646,好吧,用户可以立即进入步骤#2,所以即使我们继续工作,我们也需要能够在下一次 GET 时更正状态。虽然我们可以结合这些方法并确保即使用户离开家而没有进入下一步,实验也处于一致状态..

标签: oop design-patterns architecture


【解决方案1】:

我认为分离 状态 和存储对象的上下文 更灵活。在任何步骤创建新对象都伴随着上下文的不变性和一致性的保持。

有单独的状态规则 - 这些是从一个状态转换到另一个状态的规则,以及用于创建的可用对象和单独的上下文规则,其一致性规则,每次更改时都会得到保证。

【讨论】:

  • 不确定这有什么关系。在您的情况下,您有一个必须保持一致的上下文。问题仍然存在 - 您是立即“调用”上下文以使事情保持一致(并使 PUT/PATCH 操作复杂化)还是在下一次 GET 期间懒惰地这样做?
  • 我的意思是数据的变化不应该在提取数据的时候发生,而是在创建的时候发生
【解决方案2】:

脏数据异步清理呢?

  1. 每当用户返回第 1 步并更改某些内容时,将所有相关数据标记为“脏”(例如,在“DirtyData”表中添加指向它的链接)并暂时完成。
  2. 让 DataCleanup 工作人员(例如单独的线程或 smth)不断寻找要清理的数据。
  3. 在为第 2 步编辑数据之前,检查数据是否不脏。

根据您的逻辑,3) 可能会导致用户错误(例如,用户需要重复步骤 #2)。如果 DataCleanup 工作人员有足够的资源(即它几乎立即处理 DirtyData 表),那只会在极少数情况下发生。如果这不行,您可以选择在每次提取时检查脏数据,但这可能会很昂贵。

【讨论】:

  • 如果第 2 步中的数据刚刚过时(我们不引用它)并且很容易从 DB 列中确定,则异步检查非常有用。正如您所提到的,这成为了一项清理工作。但我的功能不那么简单 - 例如如果在步骤#1 中添加 某些内容,我需要在步骤#2 中删除、更新或添加 内容,并且很难仅从数据库数据中确定。所以清理工作是不可行的。正如您稍后建议的那样,在每次获取时运行脏检查 - 与第 2 步中的其他内容相比,实际上是非常便宜的操作。这就是我们最初选择它的原因。
【解决方案3】:

听起来您对有关 GET 请求的 HTTP 规范很熟悉,但对于未来的读者来说:

对于第 2 条中的另一个要点,我们可能不需要规范来同意持久化有效数据优于持久化无效数据。

那么我们可以为 1 下的项目符号做些什么来避免特定步骤中复杂的分支逻辑以及循环依赖?我的建议是事件驱动设计。当第 2 步更改时,它应该触发一个更改事件。在这种情况下,步骤 #2 不知道可能接收其事件的具体侦听器,因此它与任何复杂的处理逻辑保持分离。

可能无法保证您以后不会忘记任何事情;但如果工作流中的每一步都定义为一个监听器,它会迫使您在每次实施新步骤时都在一定程度上考虑更改事件。

关于粒度的一个附注:如果一个步骤有很多更改,它可以将其事件批量化,而不是单独触发每个事件。您可以调整大小以提高效率。

总之,我会强烈考虑Observer设计模式。

【讨论】:

  • 是的,Observer 在编译时解开了 2 个功能。唯一的缺点(我忘了提)是为了确定步骤#2 是否需要更新,我们需要加载它的数据。这意味着无论我们是否真的想在 Step#2 上更新某些内容,我们都必须做额外的工作,这会减慢 Step#1 上的所有操作。好吧,除非这些信息来自 UI……但这会将功能联系起来。不过在不同的层面上。
  • 更新可以是异步的。当然,这会增加其自身的复杂性,但可以避免第 1 步中的性能损失。
  • 缓存是性能问题的另一种解决方案,可能比异步逻辑更简单。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-05-11
  • 1970-01-01
相关资源
最近更新 更多