首页 文章

跨REST微服务的事务?

提问于
浏览
137

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

现在有几种情况可能会出错:

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

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

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

有哪些解决方案可以防止这种数据不一致发生?是否存在允许事务跨越多个REST请求的模式?我已经阅读了Two-phase commit上的维基百科页面,这似乎触及了这个问题,但我不确定如何在实践中应用它 . 虽然我还没读过,但这篇论文看起来也很有意思 .

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

10 回答

  • 102

    什么没有意义:

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

    什么会让你头疼:

    • EJBs with distributed transactions . 它试图使分布式事务跨JBoss EAP 6.3实例为远程EJB工作 . 我们还没有工作 .

    • Two-phase commit solutions in general . 我认为2PC protocol是一个很好的算法(很多年前我用C在RPC中实现了它) . 它需要全面的故障恢复机制,包括重试,状态存储库等 . 所有复杂性都隐藏在事务框架中(例如:JBoss Arjuna) . 但是,2PC不是故障证明 . 在某些情况下,交易很简单,但每100次交易可能会发生一次,具体取决于您的平台和方案 .

    • Sagas (Compensating transactions) . 创建补偿操作的实现开销,以及最终激活补偿的协调机制 . 但补偿也不是失败证明 . 你可能仍然会出现不一致(=有些头疼) .

    什么可能是最好的选择:

    • Eventual consistency . 类似ACID的分布式事务和补偿事务都不是故障证明,两者都可能导致不一致 . 最终的一致性往往优于"occasional inconsistency" . 有不同的设计解决方案,例如:

    • 您可以使用异步通信创建更强大的解决方案 . 在您的方案中,当Bob注册时,API网关可以向NewUser队列发送消息,并立即回复用户说"You'll receive an email to confirm the account creation."队列消费者服务可以处理消息,在单个事务中执行数据库更改,并发送发送给Bob的电子邮件通知帐户创建 .

    • 用户微服务在同一数据库中创建用户记录和钱包记录 . 在这种情况下,用户微服务中的钱包商店是仅对钱包微服务可见的主钱包商店的副本 . 有一种基于触发器的数据同步机制或定期启动以将数据变化(例如,新钱包)从副本发送到主设备,反之亦然 .

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

    • Remodel the microservices . 如果具有队列的解决方案不是将用户和电子钱包功能改造为在同一服务中并置(或者至少在同一VM中以避免分布式事务) . 是的,它离微服务更远一步,更接近整体,但会让你免于一些头痛 .
  • 0

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

    相反,通过遵循“尽力而为”模型并补偿过程中的错误,“星巴克模型”更具 生产环境 力 . 首先,他们确保你支付!然后,有一些消息队列,您的订单附在杯子上 . 如果在这个过程中出现问题,就像你没有喝咖啡一样,这不是你订购的等等,我们进入补偿过程,我们确保你得到你想要的或退款给你,这是最有效的模式提高 生产环境 力 .

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

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

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

    • 构建更多智能服务,使其可以“重新调用”任意次数,使用uid或taskid进行处理,按照订单自下而上直到最后,验证每个步骤中的业务规则;

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

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

    让我们回过头来发布的初始问题 . 创建一个帐户并创建一个钱包,并确保一切都完成 .

    假设一个Web服务被调用来编排整个操作 .

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

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

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

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

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

    • 现在,假设Web服务从未收到成功或失败(在这种情况下),即执行了帐户创建的撤销 . 它只会再次调用帐户的撤消服务 . 而且这项服务通常应该永远不会失败,因为它的目标是不再存在该帐户 . 因此它会检查它是否存在,并且看不到任何可以撤消它的工作 . 因此它返回操作成功 .

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

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

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

    Compensating transaction pattern

  • 1

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

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

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

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

  • 25

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

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

  • 3

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

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

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

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

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

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

    我也会想到为什么我的配置过程中出现了一个容易出错的步骤 .

  • 2

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

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

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

    对于SOAP(好的,不是REST),有WS-AT规范,但我没有必要集成的服务支持 . 对于REST,JBoss有something in the pipeline . 否则,"pattern"要么找到可以插入架构的产品,要么构建自己的解决方案(不推荐) .

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

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

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

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

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

    • 正如您所建议的那样,您可以使用事务性消息队列,但它将是异步的,因此如果依赖于响应,它将变得混乱 .

    • 您可以写下这样一个事实:您需要将后端服务调用到您的数据库中,然后使用批处理调用后端服务 . 再次,异步,所以可能会变得混乱 .

    • 您可以使用业务流程引擎作为API网关来编排后端微服务 .

    • 您可以使用远程EJB,如开头所述,因为它支持开箱即用的分布式事务 .

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

    扮演恶魔的人提倡:为什么在有产品的时候 Build 类似的东西为你做到这一点(见上文),并且可能做得比你好,因为它们经过了试验和测试?

  • 9

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

    正如许多人提到的那样,分布式交易不是一个好的选择,人们现在更多地寻求最终的系统,但我不确定这对银行,保险等是否有用......

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

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

  • -2

    一个简单的解决方案是使用用户服务创建用户并使用消息总线,其中用户服务发出其事件,Wallet Service在消息传递总线上注册,侦听User Created事件并为用户创建Wallet . 同时,如果用户使用Wallet UI查看他的电子钱包,请检查用户是否刚刚创建并显示您的钱包创建正在进行中,请检查一段时间

  • 7

    最终的一致性是关键 .

    • 选择其中一项服务成为事件的主要处理者 .

    • 此服务将使用单次提交处理原始事件 .

    • 主处理程序将负责将辅助效果与其他服务异步通信 .

    • 主处理程序将执行其他服务调用的编排 .

    指挥官负责分配交易并控制 . 它知道要执行的指令并将协调执行它们 . 在大多数情况下,只有两条指令,但它可以处理多条指令 .

    指挥官负责保证执行所有指令,这意味着退休 . 当指挥官试图实现远程更新并且没有得到响应时,它没有重试 . 通过这种方式,系统可以配置为不易发生故障并自行修复 .

    在我们重试时,我们有幂等性 . 幂等性是能够做两次这样的方式的特性,使得最终结果与仅仅做过一次的结果相同 . 我们需要远程服务或数据源的幂等性,以便在它多次接收指令的情况下,它只处理一次 .

    最终的一致性这解决了大多数分布式事务挑战,但是我们需要在这里考虑几点 . 每次失败的事务都将进行重试,尝试的重试次数取决于上下文 .

    一致性是最终的,即,当系统在重试期间处于一致状态时,例如,如果客户订购了书籍,并进行了支付,然后更新了库存数量 . 如果库存更新操作失败并假设该库存是最后库存,则该库仍将可用,直到库存更新的重试操作成功为止 . 重试成功后,您的系统将保持一致 .

  • 39

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

相关问题