【问题标题】:Transactions in REST?REST 中的事务?
【发布时间】:2021-04-07 16:44:46
【问题描述】:

我想知道您将如何在 REST 中实现以下用例。是否有可能在不损害概念模型的情况下做到这一点?

在单个事务的范围内读取或更新多个资源。例如,将 100 美元从 Bob 的银行账户转入 John 的账户。

据我所知,实现这一点的唯一方法是作弊。您可以 POST 到与 John 或 Bob 关联的资源,并使用单个事务执行整个操作。就我而言,这破坏了 REST 架构,因为您实际上是通过 POST 隧道调用 RPC 调用,而不是真正对单个资源进行操作。

【问题讨论】:

    标签: rest


    【解决方案1】:

    考虑一个 RESTful 购物篮场景。购物篮在概念上是您的交易包装器。与您可以将多个商品添加到购物篮然后提交该购物篮以处理订单的方式相同,您可以将 Bob 的帐户条目添加到交易包装器中,然后将 Bill 的帐户条目添加到包装器中。当所有部分都到位后,您可以使用所有组件部分发布/放置事务包装器。

    【讨论】:

    • 为什么 TransferMoneyTransaction 不是可行的银行资源?
    • 如果您确保端点引用名词,那么标准 GET、PUT、POST、DELETE 动词对该名词的作用通常很直观。 RPC 允许端点本身是动词,因此它们可能与 HTTP 动词冲突,意图变得混乱。
    • 例如如果您在端点 UpdateXYZ 上执行 HTTP DELETE 会发生什么?它会删除 XYZ 吗?它是删除更新还是仅执行更新并忽略 HTTP 动词删除。通过将动词保持在端点之外,您可以消除混淆。
    • 那么跨多个服务的事务呢?以及当您想要进行一组“不相关”的更改时,该服务不公开任何隐式事务容器。另外,当我们转移到与您的实际数据完全无关的通用事务时,为什么还要使用特定的事务类型变化。事务可能与 restful 不匹配,但似乎事务应该分层,除了请求标头将包含事务引用这一事实之外,与其余调用无关。
    • @meandmycode 数据库事务应该在 REST 接口后面分层。或者,您可以将业务事务(而不是数据库事务)作为资源本身公开,然后您需要在发生故障时采取补偿措施。
    【解决方案2】:

    这个问题没有回答一些重要的案例,我认为这太糟糕了,因为它在谷歌上的搜索词排名很高:-)

    具体来说,一个不错的属性是:如果您 POST 两次(因为中间有一些缓存打嗝),您不应该将金额转移两次。

    为此,您需要创建一个事务作为对象。这可能包含您已经知道的所有数据,并将交易置于待处理状态。

    POST /transfer/txn
    {"source":"john's account", "destination":"bob's account", "amount":10}
    
    {"id":"/transfer/txn/12345", "state":"pending", "source":...}
    

    一旦你有了这个事务,你就可以提交它,比如:

    PUT /transfer/txn/12345
    {"id":"/transfer/txn/12345", "state":"committed", ...}
    
    {"id":"/transfer/txn/12345", "state":"committed", ...}
    

    请注意,此时多个 put 无关紧要;甚至 txn 上的 GET 也会返回当前状态。具体来说,第二个 PUT 会检测到第一个 PUT 已经处于适当的状态,然后将其返回——或者,如果您在它已经处于“提交”状态后尝试将其置于“回滚”状态,您会得到一个错误,并返回实际提交的事务。

    只要您与单个数据库或具有集成事务监视器的数据库通信,这种机制实际上就可以正常工作。您还可以为交易引入超时,如果您愿意,您甚至可以使用 Expires 标头来表示。

    【讨论】:

    • 有趣的讨论!我想补充一点,初始帖子必须一步完成。它不能在以后添加(然后我们在购物车领域,并且购物车有许多制衡,以防止它们对最终用户造成伤害,甚至立法,银行转账都没有)...
    【解决方案3】:

    在 REST 术语中,资源是可以使用 CRUD(创建/读取/更新/删除)动词作用的名词。由于没有“转账”动词,我们需要定义一个可以使用 CRUD 执行的“交易”资源。这是 HTTP+POX 中的一个示例。第一步是CREATE(HTTP POST 方法)一个新的empty事务:

    POST /transaction
    

    这会返回一个交易 ID,例如“1234”并根据 URL “/transaction/1234”。请注意,多次触发此 POST 不会创建具有多个 ID 的同一事务,并且还会避免引入“待处理”状态。此外,POST 并不总是幂等的(REST 要求),因此最好尽量减少 POST 中的数据。

    您可以将事务 ID 的生成留给客户端。在这种情况下,您将发布 /transaction/1234 以创建事务“1234”,如果它已经存在,服务器将返回一个错误。在错误响应中,服务器可能会返回一个当前未使用的 ID 以及相应的 URL。使用 GET 方法向服务器查询新 ID 不是一个好主意,因为 GET 永远不会改变服务器状态,而创建/保留新 ID 会改变服务器状态。

    接下来,我们更新(PUT HTTP 方法)包含所有数据的事务,隐式提交它:

    PUT /transaction/1234
    <transaction>
      <from>/account/john</from>
      <to>/account/bob</to>
      <amount>100</amount>
    </transaction>
    

    如果之前已经 PUT 了 ID 为“1234”的事务,则服务器给出错误响应,否则给出 OK 响应和 URL 以查看已完成的事务。

    注意:在 /account/john 中,“john”应该是 John 的唯一帐号。

    【讨论】:

    • 将 REST 等同于 CRUD 是一个严重的错误。 POST 不一定意味着 CREATE。
    • 严重错误?我知道 PUT 和 POST 之间存在差异,但是与 CRUD 的映射松散。 “认真的”?
    • 是的,认真的。 CRUD 是一种结构化数据存储的方式; REST 是一种结构化应用程序数据流的方式。你可以在 REST 上做 CRUD,但你不能在 CRUD 上做 REST。它们是不等价的。
    【解决方案4】:

    很好的问题,REST 主要通过类似数据库的示例来解释,其中存储、更新、检索、删除某些内容。像这样的例子很少,服务器应该以某种方式处理数据。我不认为 Roy Fielding 在他的论文中包含任何内容,毕竟它是基于 http 的。

    但他确实将“代表性状态转移”作为状态机谈论,链接移动到下一个状态。通过这种方式,文档(表示)跟踪客户端状态,而不是服务器必须这样做。这样一来,就没有客户端状态,只有您在哪个链接上的状态。

    我一直在考虑这个问题,让服务器为你处理一些东西,当你上传时,服务器会自动创建相关资源,并给你链接(实际上,它不需要自动创建它们:它可以只告诉你链接,它只会在你关注它们时创建它们 - 懒惰创建)。并且还为您提供创建 new 相关资源的链接 - 相关资源具有相同的 URI 但更长(添加后缀)。例如:

    1. 您上传 (POST) 包含所有信息的事务概念的表示。 这看起来就像一个 RPC 调用,但它实际上是在创建“提议交易资源”。例如 URI:/transaction 故障会导致创建多个此类资源,每个资源都有不同的 URI。
    2. 服务器的响应说明了所创建资源的 URI 及其表示形式 - 这包括用于创建新“已提交事务资源”的相关资源的链接 (URI)。其他相关资源是删除提议交易的链接。这些是状态机中的状态,客户端可以遵循这些状态。从逻辑上讲,这些是在服务器上创建的资源的一部分,超出了客户端提供的信息。例如 URI:/transaction/1234/proposed/transaction/1234/committed
    3. POST 到链接以创建“提交的交易资源”,这会创建该资源,更改服务器的状态(两个帐户的余额) **。就其性质而言,此资源只能创建一次,并且不能更新。因此,不会发生提交许多事务的故障。
    4. 您可以获取这两个资源,查看它们的状态。假设 POST 可以更改其他资源,则该提案现在将被标记为“已提交”(或者可能根本不可用)。

    这类似于网页的操作方式,最终网页显示“您确定要这样做吗?”最终的网页本身就是交易状态的表示,其中包括进入下一个状态的链接。不仅仅是金融交易;也(例如)预览然后在维基百科上提交。我猜 REST 的区别在于状态序列中的每个阶段都有一个明确的名称(它的 URI)。

    在现实生活中的交易/销售中,交易的不同阶段(提案、采购订单、收据等)通常有不同的实物文件。更适合买房、定居等。

    OTOH 这对我来说就像在玩语义;我对将动词转换为名词以使其成为 RESTful 的名词化感到不舒服,“因为它使用名词(URI)而不是动词(RPC 调用)”。即名词“提交的事务资源”而不是动词“提交此事务”。我想名词化的一个优点是您可以通过名称引用资源,而不是需要以其他方式指定它(例如维护会话状态,所以您知道“这个”事务是什么......)

    但重要的问题是:这种方法有什么好处?即,这种 REST 风格比 RPC 风格好在什么方面?除了存储/检索/更新/删除之外,对网页有用的技术是否也有助于处理信息?我认为 REST 的主要好处是可扩展性。其中一个方面是不需要显式维护客户端状态(但将其隐含在资源的 URI 中,并将下一个状态作为其表示中的链接)。从这个意义上说,它有所帮助。也许这也有助于分层/流水线? OTOH 只有一个用户会查看他们的特定事务,因此缓存它以便其他人可以读取它没有任何优势,这是 http 的一大胜利。

    【讨论】:

    • 您能否解释一下“不需要在客户端保持状态”如何有助于可扩展性?什么样的可扩展性?什么意义上的可扩展性?
    • 事务如何不存储来自客户端的信息? /耸肩
    【解决方案5】:

    我已经离开这个话题 10 年了。回来,我无法相信当你谷歌休息+可靠时你涉足的伪装成科学的宗教。这种混乱是神话般的。

    我会将这个广泛的问题分为三个:

    • 下游服务。您开发的任何 Web 服务都将具有您使用的下游服务,并且您别无选择,只能遵循其事务语法。您应该尝试向您的服务用户隐藏所有这些,并确保您的操作的所有部分作为一个组成功或失败,然后将此结果返回给您的用户。
    • 您的服务。客户希望 Web 服务调用得到明确的结果,而直接在实质性资源上发出 POST、PUT 或 DELETE 请求的通常 REST 模式让我觉得这是一种提供这种确定性的糟糕且易于改进的方式。如果您关心可靠性,则需要识别操作请求。这个 id 可以是在客户端创建的 guid,也可以是来自服务器上的关系数据库的种子值,没关系。对于服务器生成的 ID,使用“预检”请求-响应来交换操作的 ID。如果这个请求失败或成功一半,没问题,客户端只是重复请求。未使用的 id 没有害处。

      这很重要,因为它让所有后续请求都完全幂等,因为如果它们被重复 n 次,它们会返回相同的结果,并且不会导致任何进一步的事情发生。服务器存储所有针对 action id 的响应,如果它看到相同的请求,它会重放相同的响应。在this google doc 中对该模式进行了更全面的处理。该文档提出了一种实现,我相信(!),它广泛遵循 REST 原则。专家肯定会告诉我它是如何侵犯他人的。无论是否涉及下游事务,此模式都可用于对 Web 服务的任何不安全调用。
    • 将您的服务集成到由上游服务控制的“事务”中。在 Web 服务的上下文中,完整的 ACID 事务通常被认为不值得付出努力,但您可以通过在确认响应中提供取消和/或确认链接来极大地帮助您的服务的消费者,从而实现transactions by compensation

    您的要求是基本要求。不要让人们告诉你你的解决方案是不洁的。根据他们解决问题的好坏程度和简单程度来判断他们的架构。

    【讨论】:

      【解决方案6】:

      如果您回过头来总结一下这里的讨论,很明显 REST 不适用于许多 API,尤其是当客户端-服务器交互本质上是有状态的时,因为它对于非平凡事务也是如此。为什么要跳过所有建议的箍,对于客户端和服务器,为了迂腐地遵循一些不适合问题的原则?更好的原则是为客户提供最简单、最自然、最高效的方式来组合应用程序。

      总之,如果您真的在应用程序中执行大量事务(类型,而不是实例),那么您真的不应该创建 RESTful API。

      【讨论】:

      • 对,但是在分布式微服务架构的情况下应该有什么替代方案?
      • @Vitamon 绝对是 gRPC
      【解决方案7】:

      您必须推出自己的“交易 ID”类型的 tx 管理。所以这将是 4 次调用:

      http://service/transaction (some sort of tx request)
      http://service/bankaccount/bob (give tx id)
      http://service/bankaccount/john (give tx id)
      http://service/transaction (request to commit)
      

      您必须处理将操作存储在数据库(如果负载平衡)或内存等中,然后处理提交、回滚、超时。

      在公园里度过了不平静的一天。

      【讨论】:

      • 我不认为这是一个特别好的插图。您只需要两个步骤:创建事务(创建处于“挂起”状态的事务)和提交事务(如果未提交则提交,并将资源移动到已提交或回滚状态)。
      【解决方案8】:

      首先,转账是您在单个资源调用中无法完成的事情。您要做的操作是汇款。因此,您将汇款资源添加到发件人的帐户中。

      POST: accounts/alice, new Transfer {target:"BOB", abmount:100, currency:"CHF"}.
      

      完成。您不需要知道这是一个必须是原子的交易等。您只需转移资金。从 A 向 B 汇款。


      但对于罕见的情况,这里有一个通用的解决方案:

      如果您想在定义的上下文中执行涉及许多资源的非常复杂的事情,并且有很多限制实际上跨越了什么与为什么的障碍(业务与实施知识),您需要转移状态。由于 REST 应该是无状态的,因此您作为客户端需要转移状态。

      如果您传输状态,您需要向客户端隐藏内部信息。客户不应知道仅执行所需的内部信息,但不携带与业务相关的信息。如果这些信息没有商业价值,则应加密状态并使用令牌、通行证等隐喻。

      这种方式可以传递内部状态,并且使用加密和签名系统仍然可以安全可靠。为客户找到正确的抽象为什么要传递状态信息取决于设计和架构。


      真正的解决方案:

      请记住,REST 是在谈论 HTTP,而 HTTP 带有使用 cookie 的概念。当人们谈论跨越多个资源或请求的 REST API 和工作流和交互时,这些 cookie 经常被遗忘。

      记住维基百科中关于 HTTP cookie 的内容:

      Cookie 旨在成为网站记住状态信息(例如购物车中的商品)或记录用户浏览活动(包括单击特定按钮、登录或记录用户访问了哪些页面)的可靠机制。用户早在几个月或几年前)。

      所以基本上如果您需要传递状态,请使用 cookie。它的设计目的完全相同,它是 HTTP,因此它在设计上与 REST 兼容 :)。


      更好的解决方案:

      如果您谈论客户端执行涉及多个请求的工作流,您通常会谈论协议。每种形式的协议都为每个潜在步骤都附带了一组先决条件,例如在执行步骤 A 之前执行步骤 B。

      这是很自然的,但向客户端公开协议会使一切变得更加复杂。为了避免它,只要想想我们在现实世界中必须进行复杂的交互和事情时会做什么......我们使用代理。

      使用代理隐喻,您可以提供一种资源,该资源可以为您执行所有必要的步骤,并将其所执行的实际分配/指令存储在其列表中(因此我们可以在代理或“代理”上使用 POST)。

      一个复杂的例子:

      买房:

      您需要证明您的可信度(例如提供您的警察记录条目),您需要确保财务细节,您需要使用律师和受信任的第三方存储资金购买实际房屋,验证房屋现在属于给您并将购买的东西添加到您的税务记录等中(例如,某些步骤可能是错误的或其他)。

      这些步骤可能需要几天才能完成,有些可以并行完成,等等。

      为了做到这一点,你只需给代理买房子的任务,比如:

      POST: agency.com/ { task: "buy house", target:"link:toHouse", credibilities:"IamMe"}.
      

      完成。该机构会向您发回一份参考资料,您可以使用该参考资料查看和跟踪该工作的状态,其余工作由该机构的代理人自动完成。

      例如考虑一个错误跟踪器。基本上,您报告错误并可以使用错误 ID 来检查发生了什么。你甚至可以使用服务来监听这个资源的变化。任务完成。

      【讨论】:

        【解决方案9】:

        您不得在 REST 中使用服务器端事务。

        REST 约束之一:

        无状态

        客户端-服务器通信进一步受到请求之间没有客户端上下文存储在服务器上的限制。来自任何客户端的每个请求都包含服务请求所需的所有信息,并且任何会话状态都保存在客户端中。

        唯一的 RESTful 方式是创建事务重做日志并将其放入客户端状态。通过请求,客户端发送重做日志,服务器重做事务并

        1. 回滚事务但提供新的事务重做日志(更进一步)
        2. 或最终完成交易。

        但也许使用支持服务器端事务的基于服务器会话的技术更简单。

        【讨论】:

        • 引用来自维基百科 REST 条目。这是真正的来源还是维基百科从某个地方得到的?谁来说什么是客户端上下文和什么是服务器上下文?
        【解决方案10】:

        我认为在这种情况下打破 REST 的纯理论是完全可以接受的。无论如何,我认为 REST 中实际上没有任何内容表明您不能在需要它的业务案例中触及依赖对象。

        我真的认为,当您可以利用数据库来创建自定义事务管理器时,您会跳过额外的障碍来创建自定义事务管理器。

        【讨论】:

        • 那么它只是一个普通的网络调用,rpc 或你想要的名字,但它不是 REST
        • @bebbo ... 13 年后,我的回答有了更多细微差别。如果您需要事务,您的 REST 视图可能与数据库结构耦合,并且您有一个泄漏的抽象。在这种情况下,资源是传输,它应该包含创建资源所需的所有信息。
        【解决方案11】:

        在简单的情况下(没有分布式资源),您可以将事务视为一种资源,其中创建它的行为达到了最终目标。

        所以,要在&lt;url-base&gt;/account/a&lt;url-base&gt;/account/b 之间转移,您可以将以下内容发布到&lt;url-base&gt;/transfer

        /account/a/account/b50数量> 转移>

        这将创建一个新的传输资源并返回传输的新 url - 例如&lt;url-base&gt;/transfer/256

        在发布成功的那一刻,“真实”交易在服务器上进行,金额从一个帐户中删除并添加到另一个帐户。

        但是,这不包括分布式事务(如果说“a”在一个服务后面的银行持有,而“b”在另一个服务后面的另一家银行持有)——除了说“尝试以不需要分布式事务的方式来表述所有操作”。

        【讨论】:

        • 如果您不能“以不需要分布式事务的方式表达所有操作”,那么您确实需要两阶段提交。我能找到在 REST 上实现两阶段提交的最佳想法是 rest.blueoxen.net/cgi-bin/wiki.pl?TwoPhaseCommit,重要的是它不会弄乱 URL 命名空间,并允许两阶段提交在干净的 REST 语义上分层。
        • 这个建议的另一个问题是,如果缓存打嗝和 POST 两次,你会得到两次传输。
        • 是的,在这种情况下,您需要有一个两步过程 - 创建一个具有唯一 URL 的“传输”资源,然后将传输详细信息添加到它作为提交的一部分(两部分作为在其他答案中提到)。当然,这可以表述为创建“事务”资源,然后向其添加“转移”操作。
        【解决方案12】:

        我相信这将是使用在客户端生成的唯一标识符来确保连接中断不会暗示 API 保存的重复性的情况。

        我认为使用客户端生成的 GUID 字段以及转账对象并确保不再重新插入相同的 GUID 将是银行转账问题的更简单的解决方案。

        不了解更复杂的场景,例如多张机票预订或微架构。

        我找到了一篇关于这个主题的论文,与dealing with the transaction atomicity in RESTful services的经历有关。

        【讨论】:

          【解决方案13】:

          我猜你可以在 URL/资源中包含 TAN:

          1. PUT /transaction 以获取 ID(例如“1”)
          2. [PUT、GET、POST 等] /1/account/bob
          3. [PUT、GET、POST 等] /1/account/bill
          4. 删除 ID 为 1 的交易

          只是一个想法。

          【讨论】:

          • 我发现这种方法有两个问题:1)这意味着您无法访问事务之外的资源(尽管这可能没什么大不了的)。 2)到目前为止,没有一个答案涉及服务器不再是无状态的事实,尽管我怀疑对此无能为力。
          • 好吧,/1/account/bob 和 /account/bob 只是两个不同的资源。 :) 和 RE:无状态,这意味着资源始终可用并且不依赖于先前的请求。既然你要求交易,是的,事实并非如此。但话又说回来,你想要交易。
          • 如果客户端必须组装 URI,那么您的 API 不是 RESTful。
          • 我听不懂你们,真的!如果您将事务视为资源(如上面的示例),您只需停止以经典意义处理事务并以“正确的 REST 方式”使用它,这进一步简化了编程事务过程。例如,您可以在响应中包含对事务的 href 以绕过分布式服务器端环境中的位移,它仍然是无状态的(它只是一个资源,不是吗?)并且无论如何您都可以实现实际的事务机制想要(如果后面没有数据库怎么办?)
          • 如果你停止思考 SQL/SOAP 并开始思考 HTTP(就像浏览器一样),那么一切都会变得简单
          猜你喜欢
          • 2010-11-26
          • 2012-07-14
          • 1970-01-01
          • 1970-01-01
          • 2015-07-24
          • 2015-07-25
          • 2016-09-10
          • 1970-01-01
          相关资源
          最近更新 更多