跨REST微服务的事务?

假设我们有一个用户、钱包REST微服务和一个API网关,它将这些东西粘合在一起。当Bob在我们的网站上注册时,我们的API网关需要通过user微服务创建一个用户,并通过wallet微服务创建一个钱包。

下面是一些可能出错的情况:

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

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

  • 创建了用户Bob,当我们创建钱包时,HTTP连接断开。钱包的创建可能成功,也可能失败。

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

或者,我知道REST可能不适合这个用例。处理这种情况的正确方法可能是完全放弃REST并使用不同的通信协议,如消息队列系统?或者我应该在我的应用程序代码中强制一致性(例如,通过有一个后台作业来检测不一致并修复它们,或者通过在我的用户模型上有一个“state”属性与“creating”,“created”值等)?

87769 次浏览

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

最后一个调用应该是安全可重复的(以防连接断开)。

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

或者,您可能想要考虑一下为什么您如此担心没有钱包的用户。你认为这会引起问题吗?如果是这样,也许将它们作为单独的rest调用是一个坏主意。如果用户没有钱包就不应该存在,那么您可能应该将钱包添加到用户中(在最初的POST调用中创建用户)。

如果我的钱包只是用户在同一个sql数据库中的另一组记录,那么我可能会将用户和钱包的创建代码放在同一个服务中,并使用正常的数据库事务设施进行处理。

在我看来,你是在问,当钱包创建代码要求你接触另一个或多个系统时会发生什么?我认为这完全取决于创建过程的复杂性和风险。

如果只是涉及到另一个可靠的数据存储(比如一个不能参与sql事务的数据存储),那么根据整个系统参数,我可能愿意冒第二次写入不会发生的小概率风险。我可能什么也不做,只是引发一个异常,并通过补偿事务或甚至一些特别方法处理不一致的数据。就像我总是告诉开发者的那样:“如果这类事情发生在应用中,它不会被忽视。”

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

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

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

我也会仔细思考为什么我的配置过程中有一个容易失败的步骤。

恕我直言,微服务架构的一个关键方面是事务被限制在单个微服务中(单一责任原则)。

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

有什么解决方案可以防止这种数据不一致的发生?

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

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

对于SOAP(好吧,不是REST),有ws - at规范,但我曾经集成的任何服务都不支持它。对于REST, JBoss有有些事情正在进行中。否则,“模式”是要么找到一个可以插入到您的体系结构中的产品,要么构建您自己的解决方案(不推荐)。

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

根据您引用的论文,还有来自Atomikos的Try-Cancel/Confirm模式和相关Product。

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

或者,我知道REST可能不适合这个用例。处理这种情况的正确方法可能是完全放弃REST并使用不同的通信协议,如消息队列系统?

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

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

或者我应该在我的应用程序代码中强制一致性(例如,通过有一个后台作业来检测不一致并修复它们,或者通过在我的用户模型上有一个“state”属性与“creating”,“created”值等)?

玩魔鬼的拥护者:为什么要建立这样的东西,当有产品为你做这件事(见上文),可能比你做得更好,因为他们是经过试验和测试的?

不合理的事情:

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

什么会让你头疼:

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

最好的选择是什么:

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

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

    • 重构微服务。如果使用队列的解决方案不能工作,因为服务使用者需要立即得到响应,那么我宁愿重新构造User和Wallet功能,将其配置在同一个服务中(或者至少在同一个VM中,以避免分布式事务)。是的,它离微服务更近了一步,也更接近了一个庞然大物,但它会让你免去一些头疼的事情。

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

就我个人而言,我喜欢微服务的想法,用例定义的模块,但正如你的问题所提到的,它们对银行、保险、电信等经典业务有适应问题……

分布式事务,正如许多人提到的,不是一个好的选择,人们现在更多的是最终一致的系统,但我不确定这是否适用于银行,保险等....

我写了一篇博客关于我提出的解决方案,也许这可以帮助你....

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

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

相反,“星巴克模式”通过遵循“尽最大努力”模式并补偿过程中的错误,更有成效。首先,他们会确保你付钱!然后,将您的订单附加到杯子上的消息队列。如果在这个过程中出现了问题,比如你没有拿到你的咖啡,这不是你点的东西,等等,我们会进入赔偿过程,我们会确保你得到你想要的东西或退款给你,这是提高生产力的最有效的模式。

有时,星巴克浪费了一杯咖啡,但整个过程是高效的。在构建web服务时,还有其他一些技巧需要考虑,比如将它们设计成可以被任意次调用并且仍然提供相同的最终结果。所以我的建议是:

  • 在定义你的web服务时不要太精细(我不相信这些天发生的微服务炒作:太过分的风险太大);

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

  • 构建更智能的服务,使它们可以被任意次数地“召回”,使用uid或taskid进行处理,该uid或taskid将遵循顺序从头到尾,在每个步骤中验证业务规则;

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

  • 作为最后的手段,(因为它可能不经常发生)将其放入队列中,以便手动处理错误。

让我们回到最初的问题。创建一个账户,创建一个钱包,确保一切都完成了。

假设调用一个web服务来编排整个操作。

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

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

  2. 调用Wallet创建,为其提供帐户ID和任务ID。假设条件无效,无法执行钱包创建。调用返回一个错误,但没有创建任何东西。

  3. 将错误通知编排器。它知道它需要中止Account的创建,但它自己不会这样做。它将通过传递在第1步结束时收到的“最小撤销收据”来请求钱包服务完成此操作。

  4. Account服务读取撤销收据并知道如何撤销操作;撤销收据甚至可能包括关于另一个微服务的信息,它可以调用自己来完成部分工作。在这种情况下,撤销接收可能包含Account ID和执行相反操作所需的一些额外信息。在本例中,为了简化操作,我们假设使用帐户id删除帐户。

  5. 现在,让我们假设web服务从未接收到执行Account创建撤销的成功或失败(在本例中)。它将简单地再次调用Account的撤销服务。这个服务通常不会失败,因为它的目标是让帐户不再存在。因此,它会检查它是否存在,并看到没有任何方法可以撤销它。所以它返回操作成功。

  6. web服务返回给用户该帐户不能被创建。

这是一个同步的例子。如果我们不希望系统完全恢复错误,我们可以用不同的方式管理它,并将案例放入针对帮助台的消息队列中。”我曾在一家公司看到过这种情况,该公司没有为后端系统提供足够的钩子来纠正这种情况。帮助台收到了包含成功执行内容的消息,并且有足够的信息来修复问题,就像我们的撤销收据可以以完全自动化的方式使用一样。

我已经进行了搜索,微软网站对这种方法有一个模式描述。它被称为补偿事务模式:

补偿事务模式

一个简单的解决方案是使用用户服务创建用户,并使用消息总线,其中用户服务发出其事件,钱包服务在消息总线上注册,监听用户创建的事件并为用户创建钱包。同时,如果用户进入钱包UI查看他的钱包,检查用户是否刚刚创建,并显示您的钱包正在创建中,请过一段时间再检查

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

  • 选择其中一个服务作为事件的主要处理程序。
  • 该服务将处理单个提交的原始事件。
  • 主处理程序将负责将次要效果异步地传递给其他服务。
  • 主处理程序将对其他服务调用进行编排。

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

指挥官负责保证所有指令的执行,这意味着退休。 当指挥官试图执行远程更新而没有得到响应时,它没有重试。 通过这种方式,系统可以配置为更不容易出现故障,并自我修复。< / p > 当我们进行重试时,我们有幂等性。 幂等性是这样一种性质,即可以做某事两次,最终结果与只做一次一样。 我们需要远程服务或数据源上的幂等性,以便在它多次接收指令的情况下,它只处理一次指令。< / p > < p >最终一致性 这解决了大多数分布式事务的挑战,但是我们需要考虑以下几点。 每一个失败的事务都将被重试,尝试重试的数量取决于上下文。< / p >

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

在微服务世界中,服务之间的通信应该通过rest客户端或消息队列。有两种方法可以跨服务处理事务,具体取决于服务之间的通信方式。我个人更喜欢消息驱动的体系结构,这样长时间的事务对用户来说应该是一个无阻塞的操作。 让我们举个例子来解释:

  1. 使用事件Create user创建用户BOB,并将消息推送到消息总线。
  2. 订阅此事件的钱包服务可以创建用户对应的钱包。

您必须注意的一件事是选择一个健壮可靠的消息主干,它可以在发生故障时保持状态。您可以使用kafka或rabbitmq作为消息骨架。由于最终的一致性,执行将会有延迟,但可以通过套接字通知轻松更新。通知服务/任务管理器框架可以是通过异步机制(如套接字)更新事务状态的服务,并可以帮助UI更新显示正确的进度。