跨REST微服务的交易?


195

假设我们有一个User,Wallet REST微服务和一个将事物粘合在一起的API网关。当Bob在我们的网站上注册时,我们的API网关需要通过User微服务创建用户,并通过Wallet微服务创建钱包。

现在,这是一些可能出错的场景:

  • 用户Bob的创建失败:可以,我们只向Bob返回一条错误消息。我们正在使用SQL事务,因此没人能在系统中看到Bob。一切都很好:)

  • 用户Bob已创建,但是在创建我们的电子钱包之前,我们的API网关会严重崩溃。现在,我们有一个没有钱包的用户(数据不一致)。

  • 用户Bob已创建,并且在我们创建电子钱包时,HTTP连接断开。钱包创建可能成功,也可能没有成功。

有哪些解决方案可以防止这种数据不一致的情况发生?是否存在允许事务跨越多个REST请求的模式?我已经阅读了有关两阶段提交的Wikipedia页面,该页面似乎涉及到此问题,但是我不确定如何在实践中应用它。这种原子分布式事务:一个RESTful设计纸似乎也有意思,虽然我还没有看过它。

另外,我知道REST可能不适合此用例。处理这种情况的正确方法也许会完全丢弃REST,并使用其他通信协议(例如消息队列系统)来进行处理吗?还是我应该在应用程序代码中强制执行一致性(例如,通过具有检测不一致之处并进行修复的后台作业,或者通过在用户模型上使用“创建”,“创建”的值等方式具有“状态”属性)?



3
如果用户没有钱包就没有意义,为什么还要为其创建单独的微服务?最初架构可能不正确吗?为什么您需要通用的API网关,顺便说一句?有什么具体原因吗?
弗拉迪斯拉夫·拉斯特鲁尼(Fladislav Rastrusny),2015年

4
@VladislavRastrusny这是一个虚构的示例,但是您可以将钱包服务视为由Stripe处理。
奥利维尔·拉隆德

您可以使用流程管理器来跟踪事务(流程管理器模式),或者让每个微服务都知道如何触发回滚(传奇管理器模式),或者执行某种两阶段提交(blog.aspiresys.com/software-product-engineering / producteering /…))
安德鲁·派特(Andrew pate

@VladislavRastrusny“如果用户没有钱包就没有意义,为什么要为其创建一个单独的微服务” –例如,除了用户没有钱包而无法存在的事实之外,他们没有任何共同的代码。因此,两个团队将分别开发和部署User和Wallet微服务。难道不是一开始就进行微服务的全部目的吗?
Nik

Answers:


148

没什么意义的:

  • 带有REST服务的分布式事务。REST服务根据定义是无状态的,因此它们不应成为跨越多个服务的事务边界的参与者。您的用户注册用例场景很有意义,但是使用REST微服务创建用户和钱包数据的设计并不理想。

什么会让您头疼:

  • 具有分布式事务的EJB。这是理论上可行但实际上不可行的事情之一。现在,我正在尝试使跨JBoss EAP 6.3实例的远程EJB可以使用分布式事务。我们已经与RedHat支持小组进行了数周的交谈,但尚未奏效。
  • 一般分为两阶段提交解决方案。我认为2PC协议是一种很棒的算法(很多年前,我使用RPC在C中实现了它)。它需要全面的故障恢复机制,包括重试,状态存储库等。所有复杂性都隐藏在事务框架中(例如:JBoss Arjuna)。但是,2PC并非失败证明。在某些情况下,交易根本无法完成。然后,您需要手动识别和修复数据库不一致。如果幸运的话,它可能在百万笔交易中发生一次,但是可能会每100笔交易中发生一次,具体取决于您的平台和方案。
  • Sagas(补偿交易)。创建补偿操作的实施开销很大,最后还有激活补偿的协调机制。但是补偿也不是失败的证明。您可能仍然会遇到不一致的情况(=有些头痛)。

最好的选择可能是:

  • 最终的一致性。类似于ACID的分布式事务和补偿事务都不是故障证明,两者都可能导致不一致。最终的一致性通常比“偶尔的不一致”更好。有不同的设计解决方案,例如:
    • 您可以使用异步通信创建一个更强大的解决方案。在您的方案中,当Bob注册时,API网关可以将消息发送到NewUser队列,并立即回复用户,说“您将收到一封电子邮件以确认帐户创建”。队列使用者服务可以处理该消息,在单个事务中执行数据库更改,然后将电子邮件发送给Bob通知创建帐户。
    • User微服务在同一数据库中创建用户记录钱包记录。在这种情况下,用户微服务中的钱包存储是仅钱包微服务可见的主钱包存储的副本。有一种基于触发器的数据同步机制,或定期启动以将数据更改(例如,新的钱包)从副本发送到主数据库,反之亦然。

但是,如果您需要同步响应怎么办?

  • 改造微服务。如果带有队列的解决方案由于服务使用者立即需要响应而无法正常工作,那么我宁愿将User和Wallet功能重构为并置在同一服务(或至少在同一VM中)以避免分布式事务)。是的,它距微服务仅一步之遥,而且接近整体,但可以使您免于头痛。

4
最终的一致性为我工作。在这种情况下,“ NewUser”队列应具有较高的可用性和弹性。
Ram Bavireddi

@RamBavireddi Kafka或RabbitMQ是否支持弹性队列?
v.oddou

@ v.oddou是的,他们愿意。
Ram Bavireddi

2
@PauloMerson我不确定您如何与众不同将交易补偿为最终一致性。如果按照您的最终一致性,创建钱包失败了怎么办?
balsick

2
@balsick最终一致性设置的挑战之一是设计复杂性的增加。经常需要进行一致性检查和更正事件。解决方案的设计各不相同。在答案中,我建议在处理通过消息代理发送的消息时在数据库中创建电子钱包记录的情况。在这种情况下,我们可以设置一个死信通道,也就是说,如果处理该消息会产生错误,我们可以将消息发送到死信队列中,并通知负责“钱包”的团队。
Paulo Merson

66

这是我最近在一次采访中被问到的一个经典问题,即如何调用多个Web服务,并且在任务中间仍然保留某种错误处理。今天,在高性能计算中,我们避免了两个阶段的提交。多年前,我读了一篇关于交易的“星巴克模型”的论文:考虑订购,付款,准备和接收您在星巴克订购的咖啡的过程...我简化了一些事情,但是采用了两阶段提交模型建议在收到咖啡之前,整个过程将是所有涉及步骤的单一包装事务。但是,使用此模型,所有员工都将等待并停止工作,直到您喝咖啡为止。你看到图片了吗?

相反,通过遵循“尽力而为”模型并补偿过程中的错误,“星巴克模型”可以提高生产率。首先,他们确保您付款!然后,有消息队列,您的订单已附加到杯子上。如果在处理过程中出了点问题,例如您没有喝咖啡,不是您订购的咖啡等,我们将进入补偿流程,并确保您得到想要的或退款给您,这是最有效的模式以提高生产率。

有时,星巴克在浪费咖啡,但整个过程很有效。在构建Web服务时,还需要考虑其他技巧,例如以可以多次调用它们并仍然提供相同最终结果的方式设计它们。因此,我的建议是:

  • 定义Web服务时不要太好(我不相信这些天发生的微服务炒作:走得太远的风险太大);

  • 异步可提高性能,因此更喜欢异步,并尽可能通过电子邮件发送通知。

  • 构建更智能的服务以使它们“可调用”任意次,使用从下至下的顺序进行处理的uid或taskid进行处理,直到最后一步,并在每个步骤中验证业务规则;

  • 使用消息队列(JMS或其他消息队列)并转移到错误处理处理器,这些处理器将通过应用相反的操作将操作应用于“回滚”,顺便说一句,使用异步顺序将需要某种队列来验证流程的当前状态,所以考虑一下;

  • 在万不得已的情况下(由于可能不会经常发生),请将其放入队列以手动处理错误。

让我们返回最初发布的问题。创建一个帐户并创建一个钱包,并确保已完成所有操作。

假设调用了一个Web服务来协调整个操作。

Web服务的伪代码如下所示:

  1. 呼叫帐户创建微服务,向其传递一些信息和一些唯一的任务ID 1.1帐户创建微服务将首先检查该帐户是否已创建。任务ID与帐户记录相关联。微服务检测到该帐户不存在,因此创建该帐户并存储任务ID。注意:可以调用此服务2000次,它将始终执行相同的结果。该服务将回答“收据中包含的最少信息,以便在需要时执行撤消操作”。

  2. 调用电子钱包创建,为其提供帐户ID和任务ID。假设条件无效,并且无法执行钱包创建。调用返回错误,但未创建任何内容。

  3. 协调器被告知该错误。它知道需要中止帐户的创建,但它本身不会这样做。它将要求钱包服务通过传递在步骤1结束时收到的“最小撤回收据”来执行此操作。

  4. 帐户服务读取撤消收据,并知道如何撤消操作;撤消收据甚至可能包含有关另一个微服务的信息,该微服务可能会称自己为完成部分工作。在这种情况下,撤消收据可能包含帐户ID以及执行相反操作所需的一些其他信息。在我们的例子中,为简化起见,假设仅使用帐户ID删除帐户。

  5. 现在,假设Web服务从未收到执行帐户创建撤消操作的成功或失败(在这种情况下)。它只会再次调用该帐户的撤消服务。而且,此服务通常不会失败,因为其目标是不再存在该帐户。因此,它检查它是否存在,并且看不到有什么可以撤消的。因此它返回该操作已成功。

  6. Web服务向用户返回无法创建帐户的信息。

这是一个同步的示例。如果我们不希望系统完全恢复错误,我们可以用不同的方式对其进行管理,并将案件放入针对帮助台的消息队列中。“可以将钩子提供给后端系统,以纠正情况:服务台收到的消息包含成功执行的操作,并具有足够的信息来解决问题,就像我们的撤消收据可以完全自动化地使用一样。

我已经执行了搜索,并且Microsoft网站上有针对此方法的模式说明。这称为补偿交易模式:

补偿交易模式


2
您是否可以扩展此答案以为OP提供更具体的建议。就目前而言,这个答案有些含糊且难以理解。尽管我了解星巴克如何提供咖啡,但是我不清楚在REST服务中应模拟该系统的哪些方面。
jwg

我添加了一个与原始帖子中最初提供的案例有关的示例。
user8098437

2
刚刚添加了一个链接到微软描述的补偿交易模式。
user8098437

3
对我来说,这是最好的答案。如此简单
奥斯卡·内瓦雷斯

1
请注意,在某些复杂的情况下,补偿交易可能是完全不可能的(如Microsoft文档中突出显示的那样)。在此示例中,假设在创建钱包失败之前,有人可以通过对Account服务进行GET调用来读取有关帐户的详细信息,理想情况下,由于帐户创建失败,因此不应首先存在该详细信息。这可能导致数据不一致。这种隔离问题在SAGAS模式中是众所周知的。
Anmol Singh Jaggi

32

所有分布式系统都存在事务一致性方面的麻烦。最好的方法就是像您所说的那样,进行两阶段提交。使钱包和用户处于挂起状态。创建完成后,请单独调用以激活用户。

最后一个呼叫应该可以安全重复(以防连接断开)。

这将需要最后一次调用了解两个表(以便可以在单个JDBC事务中完成)。

或者,您可能想考虑为什么您如此担心没有钱包的用户。您相信这会引起问题吗?如果是这样,也许将那些作为单独的休息电话是一个坏主意。如果没有钱包的用户不应该存在,那么您应该将钱包添加到用户中(在创建用户的原始POST调用中)。


谢谢你的建议。用户/钱包服务是虚构的,只是为了说明这一点。但是我同意我应该设计该系统,以尽可能避免交易。
Olivier Lalonde

7
我同意第二种观点。看来,创建用户的微服务还应该创建一个钱包,因为此操作代表原子工作单元。另外,您可以阅读eaipatterns.com/docs/IEEE_Software_Design_2PC.pdf
Sattar Imamov,2015年

2
这实际上是一个主意。撤消令人头疼。但是在挂起状态下创建某些东西的侵入性要小得多。已经执行了所有检查,但尚未确定。现在,我们只需要激活创建的组件。我们甚至可以非交易方式做到这一点。
蒂莫

10

恕我直言,微服务架构的关键方面之一是,交易仅限于单个微服务(单责任原则)。

在当前示例中,用户创建将是一个自己的事务。用户创建会将USER_CREATED事件推入事件队列。钱包服务将订阅USER_CREATED事件并创建钱包。


1
假设我们要避免所有2PC,并且假设User服务已写入数据库中,那么我们就无法将消息推送到事件队列中,从而使它是事务性的,这意味着它可能永远不会电子钱包服务。
罗曼·哈尔科夫斯基

@RomanKharkovski确实很重要。解决该问题的一种方法可能是启动事务,保存用户,发布事件(不是事务的一部分),然后提交事务。(最坏的情况,极不可能的,提交失败,并且响应事件的人将找不到用户。)
Timo

1
然后将事件以及实体存储到数据库中。有计划的作业来处理存储的事件并将其发送到消息代理。stackoverflow.com/a/52216427/4587961
Yan Khonski,

7

如果我的钱包只是与用户在同一sql数据库中的另一堆记录,那么我可能会将用户和钱包创建代码放在同一服务中,并使用普通的数据库事务处理工具来处理。

在我看来,您是在问钱包创建代码要求您触摸另一个或多个其他系统时会发生什么?能否说出一切取决于创建过程的复杂程度和风险。

如果只是联系另一个可靠的数据存储(例如,一个不能参与sql事务的数据存储),那么根据整体系统参数,我可能愿意冒第二次写入不会发生的极小的机会。我可能什么也没做,但会引发异常并通过补偿性交易或某些临时方法来处理不一致的数据。正如我一直告诉开发人员的那样:“如果这种情况在应用程序中发生,那么它不会被忽视”。

随着创建钱包的复杂性和风险的增加,您必须采取措施来减轻所涉及的风险。假设某些步骤需要调用多个合作伙伴api。

此时,您可能会引入消息队列以及部分构造的用户和/或钱包的概念。

确保您的实体最终正确构建的一种简单有效的策略是让作业重试,直到成功为止,但这在很大程度上取决于您的应用程序的用例。

对于配置过程中为什么容易出错的步骤,我也要经过漫长而艰难的思考。


4

一种简单的解决方案是使用用户服务创建用户并使用消息总线,用户服务在该消息总线上发出事件,然后钱包服务在消息总线上注册,侦听用户创建的事件并为用户创建钱包。同时,如果用户进入电子钱包用户界面查看其电子钱包,请检查是否已创建该用户并显示您的钱包正在创建中,请等待一段时间


3

有哪些解决方案可以防止这种数据不一致的情况发生?

传统上,使用分布式事务管理器。几年前,在Java EE世界中,您可能已经将这些服务创建为EJB,并且已将其部署到不同的节点,并且您的API网关将对这些EJB进行远程调用。应用程序服务器(如果配置正确)将通过两阶段提交自动确保在每个节点上提交事务或将事务回滚,从而确保了一致性。但这要求将所有服务部署在同一类型的应用程序服务器上(以便它们兼容),并且实际上只能与单个公司部署的服务一起使用。

是否存在允许事务跨越多个REST请求的模式?

对于SOAP(好的,不是REST),有WS-AT规范,但是我曾经必须集成的任何服务都不支持该规范。休息,JBoss已经在流水线的东西。否则,“模式”是找到可以插入体系结构的产品,或者构建自己的解决方案(不推荐)。

我已经为Java EE发布了这样的产品:https : //github.com/maxant/genericconnector

根据您参考的论文,还有Atomikos的Try-Cancel / Confirm模式和相关产品。

BPEL引擎使用补偿来处理远程部署服务之间的一致性。

另外,我知道REST可能不适合此用例。处理这种情况的正确方法也许会完全丢弃REST,并使用其他通信协议(例如消息队列系统)来进行处理吗?

有很多方法可以将非事务性资源“绑定”到事务中:

  • 如您所建议,您可以使用事务性消息队列,但是它将是异步的,因此,如果您依赖于响应,则它会变得混乱。
  • 您可以写一个事实,您需要将后端服务调用到数据库中,然后使用批处理来调用后端服务。同样,异步,因此可能会变得混乱。
  • 您可以使用业务流程引擎作为API网关来协调后端微服务。
  • 如开头所述,您可以使用远程EJB,因为它支持开箱即用的分布式事务。

还是我应该在应用程序代码中强制执行一致性(例如,通过具有检测不一致之处并进行修复的后台作业,或者通过在用户模型上使用“创建”,“创建”的值等方式具有“状态”属性)?

扮演魔鬼的人提倡:为什么有这样的产品(当有产品可以为您做到这一点(见上文),并且可能会比您做得更好,因为它们已经过尝试和测试)?


2

我个人很喜欢微服务的概念,即用例定义的模块,但是正如您所提的问题,它们对银行,保险,电信等传统业务有适应性问题。

正如许多人所提到的那样,分布式交易不是一个好选择,人们现在为最终一致的系统而花更多的钱,但是我不确定这对银行,保险等都适用。

我写了一篇有关我提出的解决方案的博客,也许可以帮到您。

https://mehmetsalgar.wordpress.com/2016/11/05/micro-services-fan-out-transaction-problems-and-solutions-with-spring-bootjboss-and-netflix-eureka/


0

最终的一致性是这里的关键。

  • 选择服务之一成为事件的主要处理程序。
  • 该服务将通过一次提交处理原始事件。
  • 主要处理程序将负责将次要效果异步传达给其他服务。
  • 主处理程序将进行其他服务调用的编排。

指挥官负责分布式事务并控制。它知道要执行的指令,并将协调执行它们。在大多数情况下,只有两条指令,但是它可以处理多个指令。

指挥官负责保证所有指示的执行,这意味着退休。当命令程序尝试执行远程更新并且没有得到响应时,它不会重试。这样,可以将系统配置为不易出现故障,并且可以自行修复。

当我们重试时,我们就具有幂等性。幂等性是能够执行两次操作的属性,使得最终结果与只执行一次相同。我们需要远程服务或数据源上的幂等,以便在多次接收到指令的情况下,仅处理一次。

最终的一致性解决了大多数分布式事务难题,但是我们需要在此处考虑几点。每个失败的事务之后都将重试,重试的次数取决于上下文。

一致性是最终的,即在重试期间系统处于不一致状态时,例如,如果客户已订购一本书,并进行了付款,然后更新了库存数量。如果库存更新操作失败,并且假定这是最后可用的库存,则在库存更新的重试操作成功之前,该书将仍然可用。重试成功后,您的系统将保持一致。


-2

为什么不使用支持脚本/编程的API管理(APIM)平台?因此,您将能够在APIM中构建复合服务,而不会干扰微服务。我已为此目的设计使用API​​GEE。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.