跨REST微服务的事务?

Oli*_*nde 172 architecture rest transactions microservices

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

现在这里有一些可能出错的场景:

  • 用户Bob创建失败:没关系,我们只是向Bob返回错误消息.我们正在使用SQL事务,因此没有人在系统中看到过Bob.一切都很好:)

  • 用户Bob已创建,但在创建我们的Wallet之前,我们的API网关很难崩溃.我们现在有一个没有钱包的用户(数据不一致).

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

有哪些解决方案可以防止这种数据不一致发生?是否存在允许事务跨越多个REST请求的模式?我已经阅读了关于两阶段提交的维基百科页面,它似乎触及了这个问题,但我不确定如何在实践中应用它.这个原子分布式事务:一个RESTful设计文章似乎也很有趣,虽然我还没有读过它.

或者,我知道REST可能不适合这个用例.也许正确的方法来处理这种情况完全放弃REST并使用不同的通信协议,如消息队列系统?或者我应该在我的应用程序代码中强制执行一致性(例如,通过让后台作业检测到不一致并修复它们,或者在我的用户模型上使用"创建","创建"值等具有"状态"属性)?

Pau*_*son 132

什么没有意义:

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

什么会让你头疼:

  • 具有分布式事务的EJB.这是在理论上有效但在实践中不起作用的事情之一.现在,我正在尝试使分布式事务跨JBoss EAP 6.3实例为远程EJB工作.我们几周来一直在与RedHat支持人员谈话,但它还没有奏效.
  • 一般的两阶段提交解决方案.我认为2PC协议是一个很好的算法(很多年前我用C在RPC中实现它).它需要全面的故障恢复机制,重试,状态存储库等.所有复杂性都隐藏在事务框架中(例如:JBoss Arjuna).但是,2PC不是故障证明.有些情况下交易根本无法完成.然后,您需要手动识别和修复数据库不一致性.如果幸运的话,它可能会在一百万次交易中发生一次,但根据您的平台和方案,每100次交易可能会发生一次.
  • Sagas(补偿交易).创建补偿操作的实现开销以及最终激活补偿的协调机制.但补偿也不是失败证明.你可能仍然会出现不一致(=有些头疼).

什么可能是最好的选择:

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

但是如果你需要同步响应呢?

  • 重塑微服务.如果带有队列的解决方案不起作用,因为服务使用者需要立即响应,那么我宁愿重新构建用户和钱包功能,以便在同一服务中并置(或至少在同一个VM中,以避免分布式事务).是的,它距离微服务更近一步,更接近整体,但会让您免于一些头疼.

  • 最终的一致性对我有用.在这种情况下,"NewUser"队列应该具有高可用性和弹性. (3认同)
  • @PauloMerson 我不确定您如何区分补偿交易以达到最终一致性。如果在你的最终一致性中,钱包的创建失败了怎么办? (2认同)
  • @balsick 最终一致性设置的挑战之一是增加了设计复杂性。通常需要一致性检查和纠正事件。解决方案的设计各不相同。在回答中,我建议在处理通过消息代理发送的消息时在数据库中创建钱包记录的情况。在这种情况下,我们可以设置一个死信通道,即如果处理该消息产生错误,我们可以将消息发送到死信队列并通知负责“钱包”的团队。 (2认同)

use*_*437 58

这是我最近在一次采访中被问到的一个经典问题如何调用多个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服务返回给用户无法创建帐户.

这是一个同步的例子.如果我们不希望系统完全恢复错误,我们可以以不同的方式管理它并将案例放入针对服务台的消息队列中."我看到这是在一个不够的公司中执行的可以向后端系统提供挂钩以纠正情况.服务台接收的消息包含已成功执行的操作,并且有足够的信息来解决问题,就像我们的撤销收据可以以完全自动化的方式使用.

我已经进行了搜索,并且微软网站对此方法有一个模式描述.它被称为补偿交易模式:

补偿交易模式

  • 对我来说,这是最好的答案。很简单 (3认同)
  • 您是否可以扩展此答案以为OP提供更具体的建议。就目前而言,这个答案有些含糊且难以理解。尽管我了解星巴克如何提供咖啡,但是我尚不清楚该系统的哪些方面应在REST服务中进行仿真。 (2认同)
  • 如Microsoft所述,仅添加了指向补偿交易模式的链接。 (2认同)
  • 请注意,在某些复杂的场景中,补偿事务可能是完全不可能的(正如微软文档中所强调的那样)。在此示例中,想象一下,在钱包创建失败之前,某人可以通过对帐户服务执行 GET 调用来读取关联帐户的详细信息,理想情况下,该服务一开始就不应该存在,因为帐户创建失败。这可能会导致数据不一致。这种隔离问题在 SAGAS 模式中是众所周知的。 (2认同)

小智 28

所有分布式系统都存在事务一致性问题.这样做的最好方法就像你说的那样,进行两阶段提交.将钱包和用户创建为挂起状态.创建后,单独调用以激活用户.

最后一次调用应该是安全可重复的(如果您的连接断开).

这将需要最后一次调用知道两个表(以便它可以在单个JDBC事务中完成).

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

  • 我同意第二种观点.看来,创建用户的微服务还应创建钱包,因为此操作代表原子工作单元.另外,您可以阅读http://www.eaipatterns.com/docs/IEEE_Software_Design_2PC.pdf (6认同)
  • 这实际上是一个_好_主意。撤消是一个头痛的问题。但是在挂起状态下创建某些东西的侵入性要小得多。已执行任何检查,但尚未创建任何确定的内容。现在我们只需要激活创建的组件。我们甚至可以非事务性地做到这一点。 (2认同)

mit*_*dir 9

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

在当前示例中,用户创建将是自己的事务.用户创建会将USER_CREATED事件推送到事件队列中.电子钱包服务将订阅USER_CREATED事件并创建电子钱包.

  • 如果钱包创建失败并且需要删除用户(没有钱包)那么您的方法是什么?钱包应该将 WALLET_CREATE_FAILED 事件发送到单独的队列中,哪些用户服务将使用并删除用户? (2认同)

Rob*_*kal 7

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

听起来你在询问当钱包创建代码要求你触摸另一个系统或系统时会发生什么?我想这一切都取决于创作过程的复杂程度和风险程度.

如果只是触摸另一个可靠的数据存储区(比如一个不能参与你的sql事务的数据存储区),那么根据整个系统参数,我可能愿意承担第二次写入不会发生的可能性极小的风险.我可能什么都不做,但提出异常并通过补偿事务或甚至一些特殊方法处理不一致的数据.正如我总是告诉我的开发人员:"如果在应用程序中发生这种事情,它就不会被忽视".

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

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

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

我也会长时间地思考为什么我的配置过程中出现了一个容易出错的步骤.