数据库与微服务的一致性
DB consistency with microservices
在基于微服务的系统中实现数据库一致性的最佳方法是什么?
在 GOTO in Berlin,Martin Fowler 正在谈论微服务,他提到的一个 "rule" 是保留 "per-service" 数据库,这意味着服务不能直接连接到数据库 "owned" 由另一项服务提供。
这非常漂亮和优雅,但在实践中它变得有点棘手。假设您有几个服务:
- 一个前端
- 订单管理服务
- 忠诚度计划服务
现在,客户在您的前端进行购买,这将调用订单管理服务,该服务会将所有内容保存在数据库中——没问题。此时,还会调用忠诚度计划服务,以便它从您的帐户中记入/借记积分。
现在,当一切都在同一个数据库/数据库服务器上时,一切都变得容易了,因为您可以 运行 一次交易中的一切:如果忠诚度计划服务无法写入数据库,我们可以滚动整个东西回来了。
当我们在多个服务中执行数据库操作时,这是不可能的,因为我们不依赖一个连接/利用 运行 宁单个事务。
让事情保持一致并过上幸福生活的最佳模式是什么?
我很想听听您的建议!..提前致谢!
我通常不处理微服务,这可能不是一个好的做事方式,但这里有一个想法:
重申一下这个问题,该系统由三个独立但相互通信的部分组成:前端、订单管理后端和忠诚度计划后端。前端想要确保在订单管理后端和忠诚度计划后端都保存了一些状态。
一个可能的解决方案是实施某种类型的 two-phase commit:
- 首先,前端在自己的数据库中放置一条包含所有数据的记录。将此称为 前端记录 。
- 前端向订单管理后端请求交易 ID,并将完成操作所需的任何数据传递给它。订单管理后端将此数据存储在暂存区中,将其与新的交易 ID 相关联并将其返回给前端。
- 订单管理交易 ID 作为前端记录的一部分存储。
- 前端向忠诚度计划后端询问交易 ID,并将完成操作所需的任何数据传递给它。忠诚度计划后端将此数据存储在临时区域中,将其与新的交易 ID 相关联并将其返回给前端。
- 忠诚度计划交易 ID 作为前端记录的一部分存储。
- 前端通知订单管理后端完成与前端存储的交易 ID 关联的交易。
- 前端通知忠诚度计划后端完成与前端存储的交易 ID 关联的交易。
- 前端删除其前端记录。
如果实施,更改不一定是原子,但会最终一致。让我们想想它可能失败的地方:
- 如果第一步失败,则不会更改任何数据。
- 如果第二次、第三次、第四次或第五次失败,当系统重新联机时,它可以扫描所有前端记录,查找没有关联事务 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可能会对你有所帮助。
在基于微服务的系统中实现数据库一致性的最佳方法是什么?
在 GOTO in Berlin,Martin Fowler 正在谈论微服务,他提到的一个 "rule" 是保留 "per-service" 数据库,这意味着服务不能直接连接到数据库 "owned" 由另一项服务提供。
这非常漂亮和优雅,但在实践中它变得有点棘手。假设您有几个服务:
- 一个前端
- 订单管理服务
- 忠诚度计划服务
现在,客户在您的前端进行购买,这将调用订单管理服务,该服务会将所有内容保存在数据库中——没问题。此时,还会调用忠诚度计划服务,以便它从您的帐户中记入/借记积分。
现在,当一切都在同一个数据库/数据库服务器上时,一切都变得容易了,因为您可以 运行 一次交易中的一切:如果忠诚度计划服务无法写入数据库,我们可以滚动整个东西回来了。
当我们在多个服务中执行数据库操作时,这是不可能的,因为我们不依赖一个连接/利用 运行 宁单个事务。 让事情保持一致并过上幸福生活的最佳模式是什么?
我很想听听您的建议!..提前致谢!
我通常不处理微服务,这可能不是一个好的做事方式,但这里有一个想法:
重申一下这个问题,该系统由三个独立但相互通信的部分组成:前端、订单管理后端和忠诚度计划后端。前端想要确保在订单管理后端和忠诚度计划后端都保存了一些状态。
一个可能的解决方案是实施某种类型的 two-phase commit:
- 首先,前端在自己的数据库中放置一条包含所有数据的记录。将此称为 前端记录 。
- 前端向订单管理后端请求交易 ID,并将完成操作所需的任何数据传递给它。订单管理后端将此数据存储在暂存区中,将其与新的交易 ID 相关联并将其返回给前端。
- 订单管理交易 ID 作为前端记录的一部分存储。
- 前端向忠诚度计划后端询问交易 ID,并将完成操作所需的任何数据传递给它。忠诚度计划后端将此数据存储在临时区域中,将其与新的交易 ID 相关联并将其返回给前端。
- 忠诚度计划交易 ID 作为前端记录的一部分存储。
- 前端通知订单管理后端完成与前端存储的交易 ID 关联的交易。
- 前端通知忠诚度计划后端完成与前端存储的交易 ID 关联的交易。
- 前端删除其前端记录。
如果实施,更改不一定是原子,但会最终一致。让我们想想它可能失败的地方:
- 如果第一步失败,则不会更改任何数据。
- 如果第二次、第三次、第四次或第五次失败,当系统重新联机时,它可以扫描所有前端记录,查找没有关联事务 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可能会对你有所帮助。