数据库与微服务的一致性
Posted
技术标签:
【中文标题】数据库与微服务的一致性【英文标题】:DB consistency with microservices 【发布时间】:2015-09-15 06:12:42 【问题描述】:在基于微服务的系统中实现数据库一致性的最佳方法是什么?
在GOTO in Berlin,Martin Fowler 谈到了微服务,他提到的一个“规则”是保留“每个服务”数据库,这意味着服务不能直接连接到另一个服务“拥有”的数据库。
这是非常好的和优雅的,但在实践中它变得有点棘手。假设您有一些服务:
前端 订单管理服务 忠诚度计划服务现在,客户在您的前端进行了购买,这将调用订单管理服务,该服务会将所有内容保存在数据库中——没问题。此时,还会调用忠诚度计划服务,以便它从您的帐户中贷记/借记积分。
现在,当一切都在同一个数据库/数据库服务器上时,一切都变得容易,因为您可以在一个事务中运行所有内容:如果忠诚度计划服务无法写入数据库,我们可以回滚整个事情。
当我们在多个服务中执行数据库操作时,这是不可能的,因为我们不依赖一个连接/利用运行单个事务的优势。 保持一致并过上幸福生活的最佳模式是什么?
我很想听听您的建议!..提前致谢!
【问题讨论】:
可以使用诸如 BPEL 引擎或分布式事务管理器之类的东西来确保在业务事务中编排的所有系统之间的最终一致性。我写了一篇博客文章:blog.maxant.co.uk/pebble/2015/08/04/1438716480000.html,如果你在 Java 环境中运行,有一个 JCA 适配器可以在这里使用:github.com/maxant/genericconnector 【参考方案1】:这非常漂亮和优雅,但实际上它变得有点棘手
“在实践中”的意思是,您需要以这样一种方式设计您的微服务,以便在遵循规则时满足必要的业务一致性:
服务不能直接连接到另一个服务“拥有”的数据库。
换句话说,不要对他们的职责做出任何假设,并根据需要改变界限,直到你找到一种方法来实现它。
现在,回答你的问题:
让事情保持一致和过上幸福生活的最佳模式是什么?
对于不需要立即一致性的事情,并且更新忠诚度积分似乎属于该类别,您可以使用可靠的发布/订阅模式从一个微服务分派事件以供其他微服务处理。可靠的一点是,您希望事件处理具有良好的重试、回滚和幂等性(或事务性)。
如果您在 .NET 上运行,支持这种可靠性的一些基础架构示例包括 NServiceBus 和 MassTransit。全面披露 - 我是 NServiceBus 的创始人。
更新:以下 cmets 对忠诚度积分的担忧:“如果延迟处理余额更新,客户实际上可能能够订购比他们所拥有的积分更多的商品”。
许多人都在为这些强一致性要求而苦苦挣扎。问题是,这些情况通常可以通过引入额外的规则来处理,比如如果用户最终获得负的忠诚度积分,请通知他们。如果 T 过去了,没有整理出忠诚度积分,则通知用户他们将根据某个转换率向他们收费 M。客户在使用积分购买商品时应该可以看到此政策。
【讨论】:
非常好的文章!我们实际上使用了你提到的东西(即幂等性、异步)——尽管我认为我有一个不太直接的例子可能会激发对话。假设处理完订单后,您必须更新库存服务中的可用库存,您将如何处理?我假设订单管理系统需要将订单写入数据库,然后通过 HTTP 调用库存服务将产品从库存中取出——并说这需要超级快速/健壮(这样人们就不会添加购物车/购买实际缺货的产品)。 企业可能不希望/不需要保证您所描述的库存一致性,因为它可以补货并且可以稍后履行订单。 请注意忠诚度积分的棘手部分 - 如果延迟处理余额更新,客户实际上可能能够订购比他/她的积分更多的物品。我在类似的情况下苦苦挣扎,并没有找到一个很好的解决方案来实现强一致性。还发现了 Martin Fowler 的这篇文章,它表明强一致性是微服务的一个挑战:martinfowler.com/articles/microservice-trade-offs.html @PavelGrushetzky 可以通过引入额外规则来处理 - 如果用户最终获得负忠诚度积分,请通知他们。如果 T 过去了,没有整理出忠诚度积分,则通知用户他们将根据某个转换率向他们收费 M。客户在使用积分购买商品时应该可以看到此政策。 @UdiDahan 也许,但请注意这些规则有多少假设/先决条件。我立刻想到了这些:(1) 当忠诚度计划会员 (LPM) 的意图是花费积分时,可以向他们收取真钱。 (2) 从业务角度来说,让 LPM 接受新的条款和条件是可以的。 (3) 公司可以存储信用卡数据。 (4) 每个 LPM 都定义了信用卡,或者可以强制输入一个发出赎回订单请求。【参考方案2】:我通常不处理微服务,这可能不是一个好的做事方式,但这里有一个想法:
为了重申这个问题,该系统由三个独立但可通信的部分组成:前端、订单管理后端和忠诚度计划后端。前端希望确保在订单管理后端和忠诚度计划后端都保存一些状态。
一种可能的解决方案是实现某种类型的two-phase commit:
-
首先,前端在自己的数据库中放置一条记录,其中包含所有数据。将此称为前端记录。
前端向订单管理后端询问交易 ID,并将完成操作所需的任何数据传递给它。订单管理后端将此数据存储在暂存区域中,将新的交易 ID 与其关联,并将其返回给前端。
订单管理事务 ID 存储为前端记录的一部分。
前端向忠诚度计划后端询问交易 ID,并将完成操作所需的任何数据传递给它。忠诚度计划后端将这些数据存储在暂存区中,将新的交易 ID 与其关联,并将其返回给前端。
忠诚度计划交易 ID 作为前端记录的一部分存储。
前端告诉订单管理后端完成与前端存储的事务 ID 关联的事务。
前端告诉忠诚度计划后端完成与前端存储的交易 ID 关联的交易。
前端删除其前端记录。
如果实现了这一点,则更改不一定是原子的,而是最终一致的。让我们想想它可能失败的地方:
如果第一步失败,则不会更改任何数据。 如果它在第二、第三、第四或第五次失败,当系统重新联机时,它可以扫描所有前端记录,查找没有关联事务 ID(任一类型)的记录。如果遇到任何这样的记录,它可以从第 2 步开始重播。(如果第 3 步或第 5 步失败,后端会留下一些废弃的记录,但它永远不会移出暂存区,所以没关系。) 如果在第六步、第七步或第八步失败,当系统重新上线时,它可以查找所有前端记录并填写了两个事务 ID。然后它可以查询后端以查看这些事务的状态——承诺或未承诺。根据已提交的内容,它可以从适当的步骤恢复。【讨论】:
【参考方案3】:我同意@Udi Dahan 所说的话。只是想补充他的答案。
我认为您需要将请求保留到忠诚度计划,以便如果失败,可以在其他时间完成。有多种方式来表达/做到这一点。
1) 使忠诚度计划 API 故障可恢复。也就是说,它可以持久化请求,这样它们就不会丢失,并且可以在以后的某个时间点恢复(重新执行)。
2) 异步执行忠诚度计划请求。也就是说,首先将请求持久化到某个地方,然后允许服务从这个持久化存储中读取它。仅在成功执行后从持久存储中删除。
3) 照 Udi 所说的去做,并将其放在一个好的队列中(准确地说是发布/订阅模式)。这通常要求订阅者做两件事之一......要么在从队列中删除之前保留请求(转到 1)--或者 - 首先从队列中借用请求,然后在成功处理请求后,有请求从队列中删除(这是我的偏好)。
这三个都完成了同样的事情。他们将请求移动到一个持久化的地方,在那里可以处理它直到成功完成。请求永远不会丢失,并在必要时重试,直到达到令人满意的状态。
我喜欢以接力赛为例。在允许前一段代码释放它之前,每个服务或一段代码都必须持有请求并拥有该请求的所有权。一旦它被移交,当前所有者不得丢失请求,直到它被处理或移交给其他一些代码。
【讨论】:
【参考方案4】:即使对于分布式事务,如果其中一位参与者在事务中崩溃,您也可能进入“事务有疑问状态”。如果您将服务设计为幂等操作,那么生活会变得容易一些。无需 XA 即可编写程序来满足业务条件。 Pat Helland 就此写了一篇出色的论文,名为“Life Beyond XA”。基本上,该方法是对远程实体做出尽可能少的假设。他还展示了一种称为 Open Nested Transactions (http://www.cidrdb.org/cidr2013/Papers/CIDR13_Paper142.pdf) 的方法来对业务流程进行建模。在这种特定情况下,采购交易将是***流程,而忠诚度和订单管理将是下一级流程。诀窍是将细粒度服务创建为具有补偿逻辑的幂等服务。因此,如果流程中的任何地方出现任何故障,个别服务可以对其进行补偿。所以例如如果由于某种原因订单失败,忠诚度可以扣除该购买的累积积分。
其他方法是使用 CALM 或 CRDT 使用最终一致性进行建模。我写了一篇博客来强调在现实生活中使用 CALM - http://shripad-agashe.github.io/2015/08/Art-Of-Disorderly-Programming 可能会对你有所帮助。
【讨论】:
以上是关于数据库与微服务的一致性的主要内容,如果未能解决你的问题,请参考以下文章