数据库与微服务的一致性

DB consistency with microservices

在基于微服务的系统中实现数据库一致性的最佳方法是什么?

GOTO in Berlin,Martin Fowler 正在谈论微服务,他提到的一个 "rule" 是保留 "per-service" 数据库,这意味着服务不能直接连接到数据库 "owned" 由另一项服务提供。

这非常漂亮和优雅,但在实践中它变得有点棘手。假设您有几个服务:

现在,客户在您的前端进行购买,这将调用订单管理服务,该服务会将所有内容保存在数据库中——没问题。此时,还会调用忠诚度计划服务,以便它从您的帐户中记入/借记积分。

现在,当一切都在同一个数据库/数据库服务器上时,一切都变得容易了,因为您可以 运行 一次交易中的一切:如果忠诚度计划服务无法写入数据库,我们可以滚动整个东西回来了。

当我们在多个服务中执行数据库操作时,这是不可能的,因为我们不依赖一个连接/利用 运行 宁单个事务。 让事情保持一致并过上幸福生活的最佳模式是什么?

我很想听听您的建议!..提前致谢!

我通常不处理微服务,这可能不是一个好的做事方式,但这里有一个想法:

重申一下这个问题,该系统由三个独立但相互通信的部分组成:前端、订单管理后端和忠诚度计划后端。前端想要确保在订单管理后端和忠诚度计划后端都保存了一些状态。

一个可能的解决方案是实施某种类型的 two-phase commit:

  1. 首先,前端在自己的数据库中放置一条包含所有数据的记录。将此称为 前端记录
  2. 前端向订单管理后端请求交易 ID,并将完成操作所需的任何数据传递给它。订单管理后端将此数据存储在暂存区中,将其与新的交易 ID 相关联并将其返回给前端。
  3. 订单管理交易 ID 作为前端记录的一部分存储。
  4. 前端向忠诚度计划后端询问交易 ID,并将完成操作所需的任何数据传递给它。忠诚度计划后端将此数据存储在临时区域中,将其与新的交易 ID 相关联并将其返回给前端。
  5. 忠诚度计划交易 ID 作为前端记录的一部分存储。
  6. 前端通知订单管理后端完成与前端存储的交易 ID 关联的交易。
  7. 前端通知忠诚度计划后端完成与前端存储的交易 ID 关联的交易。
  8. 前端删除其前端记录。

如果实施,更改不一定是原子,但会最终一致。让我们想想它可能失败的地方:

  • 如果第一步失败,则不会更改任何数据。
  • 如果第二次、第三次、第四次或第五次失败,当系统重新联机时,它可以扫描所有前端记录,查找没有关联事务 ID(任一类型)的记录。如果遇到任何这样的记录,它可以从第 2 步开始重放。(如果第 3 步或第 5 步失败,后端会留下一些废弃的记录,但它永远不会移出暂存区,所以没关系。)
  • 如果在第六步、第七步或第八步失败,当系统重新上线时,它可以查找所有填写了两个交易ID的前端记录。然后可以查询后端以查看这些状态事务——已提交或未提交。根据已提交的内容,它可以从适当的步骤恢复。

This is super-nice and elegant but in practice it becomes a bit tricky

这意味着 "in practice" 您需要设计微服务,以便在遵循以下规则时满足必要的业务一致性:

that services cannot directly connect to a DB "owned" by another service.

换句话说 - 不要对他们的职责做出任何假设,并根据需要更改边界,直到您找到一种方法来实现这一点。

现在,回答您的问题:

What are the best patterns to keep things consistent and live a happy life?

对于不需要立即保持一致且更新忠诚度积分似乎属于这一类的事情,您可以使用可靠的 pub/sub 模式从一个微服务分派事件以供其他微服务处理。可靠的一点是,您希望事件处理内容具有良好的重试、回滚和幂等性(或事务性)。

如果您 运行 使用 .NET,一些支持这种可靠性的基础结构示例包括 NServiceBus and MassTransit。完全公开 - 我是 NServiceBus 的创始人。

更新: 以下关于忠诚度积分问题的评论:"if balance updates are processed with delay, a customer may actually be able to order more items than they have points for"。

许多人都在为强一致性的这些需求而苦苦挣扎。问题是,这类情况通常可以通过引入额外的规则来处理,比如如果用户最终的忠诚度积分为负数,请通知他们。如果 T 过去了而忠诚度积分没有被整理出来,则通知用户他们将根据某个转换率向 M 收费。当客户使用积分购买东西时,客户应该可以看到此政策。

我同意@Udi Dahan 的说法。只想补充他的答案。

我认为您需要将请求坚持到忠诚度计划,以便在失败时可以在其他时间完成。 word/do 有多种方法。

1) 使忠诚度计划 API 故障可恢复。也就是说它可以持久化请求,这样它们就不会丢失并且可以在以后的某个时间点恢复(重新执行)。

2) 异步执行忠诚度计划请求。也就是说,首先将请求持久化到某处,然后允许服务从这个持久化存储中读取它。仅在成功执行时从持久存储中删除。

3) 按照 Udi 所说的进行操作,并将其放在一个好的队列中(准确地说是 pub/sub 模式)。这通常需要订阅者做以下两件事之一......要么在从队列中删除之前保留请求(转到 1)--要么-- 首先从队列中借用请求,然后在成功处理请求后,让请求从队列中删除(这是我的偏好)。

这三个人都完成了同样的事情。他们将请求移动到一个持久化的地方,在那里它可以被处理直到成功完成。请求永远不会丢失,并在必要时重试,直到达到满意状态。

我喜欢用接力赛的例子。在允许前一段代码放手之前,每个服务或代码段都必须掌握请求并拥有所有权。一旦移交,当前所有者不得丢失请求,直到它被处理或移交给其他代码。

即使是分布式交易,如果其中一位参与者在交易过程中崩溃,您也可以进入 "transaction in doubt status"。如果您将服务设计为幂等操作,那么生活会变得更轻松一些。无需 XA 即可编写程序来满足业务条件。 Pat Helland 就此撰写了出色的论文 "Life Beyond XA"。基本上,该方法是对远程实体做出尽可能少的假设。他还举例说明了一种称为开放式嵌套事务 (http://www.cidrdb.org/cidr2013/Papers/CIDR13_Paper142.pdf) 的方法来对业务流程进行建模。在这种特定情况下,购买交易将是顶级流程,而忠诚度和订单管理将是下一级流程。诀窍是将细粒度服务创建为具有补偿逻辑的幂等服务。因此,如果流程中的任何地方出现任何故障,个别服务可以进行补偿。所以例如如果由于某种原因订单失败,忠诚度可以扣除该次购买的累积积分。

其他方法是使用 CALM 或 CRDT 使用最终一致性进行建模。我写了一篇博客来强调在现实生活中使用 CALM - http://shripad-agashe.github.io/2015/08/Art-Of-Disorderly-Programming可能会对你有所帮助。