REST中的事务处理?

165

我想知道如何在REST中实现以下用例。是否有可能在不损害概念模型的情况下完成?

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

据我所知,唯一的实现方式是通过欺骗来完成。您可以将POST请求发送到与John或Bob相关联的资源,并使用单个事务执行整个操作。就我而言,这会破坏REST架构,因为您实际上是通过POST隧道传输RPC调用,而不是真正地操作单个资源。

13个回答

93

考虑一个 RESTful 购物篮的场景。购物篮在概念上是您的交易包装器。与将多个项目添加到购物篮中,然后提交该篮子以处理订单类似,您可以将 Bob 的帐户条目添加到交易包装器中,然后将 Bill 的帐户条目添加到包装器中。当所有组件都就位时,您可以使用所有组件 POST/PUT 交易包装器。


20
为什么TransferMoneyTransaction不是可行的银行资源? - Darrel Miller
8
如果您确保您的端点指向名词,那么通常很容易理解标准的 GET、PUT、POST、DELETE 动词将对该名词执行什么操作。远程过程调用(RPC)允许端点本身成为动词,因此它们可能会与 HTTP 动词产生冲突,导致意图变得混淆。 - Darrel Miller
10
请问如果在 UpdateXYZ 端点执行 HTTP DELETE 请求会发生什么?它会删除 XYZ 吗?它会删除更新还是只执行更新并忽略 HTTP 动词 delete?将动词从端点中去掉可以消除混淆。 - Darrel Miller
5
跨多个服务的交易怎么办?当你想要进行一系列“不相关”的更改,但服务没有隐含的事务容器时,又该怎么办呢?此外,在我们转向与实际数据更改完全无关的通用事务时,为什么需要特定的事务类型?事务可能与RESTful不匹配,但似乎应该在其之上进行分层处理,与REST调用无关,除了请求头包含事务引用这一事实。 - meandmycode
4
数据库事务应该在REST接口的后面分层。或者你可以将业务事务(而不是数据库事务)本身公开为一种资源,然后在失败时需要采取补偿措施。 - Darrel Miller
显示剩余5条评论

64

这个问题没有回答几个重要的情况,我认为这很遗憾,因为它在Google上的搜索排名很高,针对这些搜索词。 :-)

具体来说,一个好的属性应该是:如果您发出两次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", ...}

注意此时多次放置不重要;即使对交易执行GET也将返回当前状态。特别是第二个PUT会检测到第一个PUT已经处于适当的状态,只返回它--或者,如果您尝试在将其放置在“已提交”状态后将其放置在“回滚”状态,则会收到错误消息,并返回实际提交的交易。

只要您与单个数据库或具有集成事务监视器的数据库进行通信,此机制实际上将完美地工作。您还可以为交易引入超时,甚至可以使用Expires标头来表达它们。


有趣的讨论!我想补充一点,即初始帖子必须在一个步骤中完成。它不能以后添加(否则我们就处于购物车领域了,而购物车有许多检查和平衡措施,以防止它们对最终用户造成伤害,即使是法规、银行转账也是如此)... - Erk

35

在REST术语中,资源是名词,可以使用CRUD(创建/读取/更新/删除)动词进行操作。由于没有"转账"动词,因此我们需要定义一个"交易"资源,可以用CRUD来操作它。下面是HTTP+POX的一个例子。第一步是创建(HTTP POST方法)一个新的空的交易:

POST /transaction

这个方法返回一个事务ID,例如“1234”,对应的URL为“/transaction/1234”。请注意,多次使用POST请求不会创建具有多个ID的相同事务,并且可以避免引入“挂起”状态。此外,POST请求并不总是幂等的(REST的要求),因此通常最好将POST数据最小化。

你可以让客户端负责生成事务ID。在这种情况下,您将使用POST /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请求提交了编号为"1234"的事务,服务器将返回错误响应;否则,响应将返回OK和一个URL,指向已完成事务的页面。
注意:在/account/john中,"john"实际上应该是约翰独特的账户号码。

5
将 REST 和 CRUD 等同起来是一个严重的错误。POST 不一定意味着 CREATE(创建)。 - user713516
14
严重的错误吗?我知道PUT和POST之间有区别,但它们与CRUD有松散的映射关系。 "严重"吗? - Ted Johnson
3
没问题。CRUD是一种结构化数据存储的方式,REST是一种结构化应用程序数据流的方式。你可以在REST上执行CRUD操作,但你无法在CRUD上实现REST。它们不是等同的。 - Jon Watte

20

非常好的问题,REST通常使用类似数据库的例子进行解释,其中会存储、更新、检索和删除一些东西。很少有像这个例子一样的,其中服务器应该以某种方式处理数据。我认为罗伊·菲尔丁在他的论文中没有包含任何此类例子,毕竟他的论文是基于HTTP的。

但是他确实谈到了“表现状态转移”作为状态机,其中链接移动到下一个状态。通过这种方式,文档(表示)跟踪客户端状态,而不是服务器必须跟踪它。因此,在这种情况下,没有客户端状态,只有关于你所在链接的状态。

我一直在思考这个问题,我认为当你上传内容时,让服务器为你处理某些内容并自动创建相关资源,并给你指向它们的链接是合理的(事实上,它不需要自动创建它们:它可以告诉你这些链接,只有当你跟随它们时,它才会创建)。同时还要给你创建新的相关资源的链接 - 相关资源具有相同的URI但更长(添加后缀)。例如:

  1. 您上传(POST)包含所有信息的交易概念的表示。这看起来就像一个RPC调用,但实际上它是创建“拟议交易资源”。例如URI:/transaction。故障将导致多个此类资源被创建,每个资源具有不同的URI。
  2. 服务器的响应说明已创建的资源的URI及其表示形式 - 这包括创建新的“已提交交易资源”的相关资源的链接。其他相关资源是链接到删除拟议交易的链接。这些都是状态机中的状态,客户端可以跟随。从逻辑上讲,这些都是在服务器上创建的资源的一部分,超出了客户端提供的信息。例如URI:/transaction/1234/proposed/transaction/1234/committed
  • 您需要提交(POST)请求到链接,以创建“已提交事务资源”,从而创建该资源并改变服务器的状态(两个账户的余额)。由于此资源的本质,它只能被创建一次,无法进行更新。因此,提交多个事务的故障不会发生。
  • 您可以获取(GET)这两个资源,以查看它们的状态。假设提交可以更改其他资源,则该建议现在将被标记为“已提交”(或者根本不可用)。
  • 这类似于网页的操作方式,最终网页会显示“您确定要执行此操作吗?”那个最终网页本身就是交易状态的表示形式,其中包含一个指向下一个状态的链接。不仅仅是金融交易;例如,预览后在维基百科上提交。我想在REST中的区别在于,状态序列中的每个阶段都具有显式名称(其URI)。

    在实际交易/销售中,通常有不同的物理文档用于交易的不同阶段(提案、采购订单、收据等)。如果买房子,可能还会有结算等更多步骤。

    另一方面,对于我来说,这感觉像是在玩弄语义;我不喜欢将动词转换为名词以使其具备RESTful特性,“因为它使用名词(URI)而不是动词(RPC调用)”。也就是说,使用名词“已提交事务资源”而不是动词“提交此事务”。我想名词化的一个优点是,您可以通过名称引用该资源,而无需以其他方式指定它(例如维护会话状态,以便知道“这个”交易是什么...)

    但重要的问题是:这种方法有什么好处?即以何种方式REST风格比RPC风格更好?这种技术对于处理信息是否也有帮助,而不仅仅是存储/检索/更新/删除?我认为REST的关键优势是可扩展性;其中一方面是不需要显式地维护客户端状态(而是在资源的URI中将其隐含,并在其表示中作为链接的下一个状态)。在这个意义上,它很有帮助。也许这有助于分层/流水线?另一方面,只有一个用户会查看他们特定的交易,因此没有将其缓存以便其他人阅读的优势,HTTP才是大赢家。


    请问您能否解释一下“不需要在客户端上保持状态”如何帮助可扩展性呢?是哪种类型的可扩展性?从哪个方面来看的可扩展性? - jhegedus
    交易如何不存储来自客户端的信息?/耸肩 - bebbo

    16

    我已经离开这个话题10年了。回来后,当你在谷歌搜索rest+reliable时,我无法相信你会陷入伪装成科学的宗教。混乱是神话。

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

    下游服务。您开发的任何Web服务都将具有您使用的下游服务,其交易语法您别无选择,只能遵循。您应该尝试隐藏所有这些内容,使您的服务用户看不到,并确保操作的所有部分成功或失败为一组,然后将此结果返回给您的用户。
    您的服务。客户端希望Web服务调用具有明确的结果,而通常的REST模式是直接在实体资源上进行POST、PUT或DELETE请求,这对于提供这种确定性来说是一种较差且容易改进的方式。如果您关心可靠性,则需要识别操作请求。此ID可以是在客户端上创建的GUID,也可以是服务器上的关系型DB中的种子值,无论哪种情况都没有关系。对于服务器生成的ID,请使用“预检”请求-响应来交换操作的ID。如果此请求失败或半成功,那么没问题,客户端只需重复请求即可。未使用的ID不会造成任何伤害。
    这很重要,因为它使得所有后续请求都是完全幂等的,也就是说,如果它们重复n次,它们将返回相同的结果并且不会引起任何进一步的事情发生。服务器针对操作ID存储所有响应,如果它看到相同的请求,则重放相同的响应。该模式的更全面的处理在this google doc中。该文档提出了一种实现方法,我相信(!),它大体上遵循REST原则。专家们肯定会告诉我它如何违反其他原则。无论是否涉及下游事务,此模式都可用于您的Web服务的任何不安全调用。
    将您的服务集成到由上游服务控制的“事务”中。在Web服务的上下文中,完整的ACID事务被认为通常不值得付出努力,但是通过在确认响应中提供取消和/或确认链接,您可以极大地帮助您的服务消费者,并因此实现补偿事务
    你的要求是基本的。不要让别人告诉你你的解决方案不正确。根据他们如何以及如何简单地解决你的问题来评估他们的架构。

    13
    如果你回顾一下这里的讨论,就会很清楚REST在许多API中并不适用,特别是当客户端与服务器交互具有天然状态时,例如处理复杂交易。为什么要跳过所有建议的步骤(对客户端和服务器),以便严格遵循某些不适合该问题的原则呢?更好的原则是给客户端提供最简单、最自然、最高效的方式来与应用程序进行组合。总之,如果您的应用程序真的需要大量事务处理,那么您真的不应该创建RESTful API。

    11
    好的,针对分布式微服务架构的情况下,应该采取什么样的替代方案呢? - Vitamon
    @Vitamon 确实是 gRPC。 - user3625699

    11

    你需要自己设计“事务ID”类型的事务管理。因此,需要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)
    

    如果需要负载平衡,您将需要处理将操作存储在数据库中或在内存中等的问题,然后处理提交、回滚和超时。

    这并不是一个轻松愉快的RESTful体验。


    4
    我认为这个例子并不特别好。你只需要两个步骤:创建事务(在“挂起”状态下创建事务),提交事务(如果未提交,则提交,并将资源移动到已提交或已回滚状态)。 - Jon Watte

    3
    首先,转账并不是一次性完成的操作。您需要进行的操作是发送资金。因此,您需要向发送者的账户中添加一个资金转移资源。
    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兼容 :)。


    更好的解决方案:

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

    这是自然的,但将协议暴露给客户端会使一切变得更加复杂。为了避免这种情况,只需想想我们在现实世界中进行复杂交互和事物时所做的事情…… 我们使用代理。

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

    一个复杂的例子:

    购买房屋:

    您需要证明自己的信誉(例如提供警察记录条目),确保财务细节,使用律师和可信第三方存储资金购买实际房屋,验证房屋现在属于您,并将购买事项添加到您的税收记录中等(仅作为示例,某些步骤可能不正确或其他)。

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

    为了做到这一点,您只需给代理任务购买房屋即可:

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

    完成了。机构会发回一个参考,你可以用它来查看和跟踪这个工作的状态,其他事情由机构代理自动完成。

    举例来说,想象一下一个故障跟踪器。基本上,你报告了故障并使用故障 ID 来查看进展情况。你甚至可以使用一个服务来监听此资源的变化。任务完成。


    2

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

    REST的一个约束条件是:

    无状态

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

    唯一符合REST标准的方法是创建交易重做日志并将其放入客户端状态。随着请求,客户端发送重做日志,服务器重新执行交易并

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

    但是,也许更简单的方法是使用支持服务器端事务的服务器会话技术。


    这句话来自维基百科的REST条目。这是真正的来源还是维基百科从其他地方获取的呢?谁能说出什么是客户端上下文,什么是服务器上下文呢? - bbsimonbb

    1
    在简单情况下(没有分布式资源),您可以将事务视为一种资源,其中创建它的行为达到了最终目标。
    因此,要在<url-base>/account/a<url-base>/account/b之间转移,您可以将以下内容发布到<url-base>/transfer
    <transfer>
        <from><url-base>/account/a</from>
        <to><url-base>/account/b</to>
        <amount>50</amount>
    </transfer>
    
    这将创建一个新的转账资源并返回转账的新URL - 例如<url-base>/transfer/256
    在成功发布时,服务器上会执行“真实”的交易,并从一个帐户中扣除金额并添加到另一个帐户中。
    然而,这并不涵盖分布式事务(例如,如果'a'在一个服务后面的一个银行持有,而'b'在另一个服务后面的另一个银行持有)- 除了说“尝试以不需要分布式事务的方式来表述所有操作”之外。

    2
    如果您无法“以不需要分布式事务的方式表达所有操作”,那么您确实需要一个两阶段提交。我能找到的在REST上实现两阶段提交的最好想法是http://rest.blueoxen.net/cgi-bin/wiki.pl?TwoPhaseCommit,它重要的是不会弄乱URL命名空间,并允许在干净的REST语义上叠加两阶段提交。 - Phasmal
    3
    这个建议的另一个问题是,如果缓存出现故障并且进行了两次POST请求,那么就会得到两次转移。 - Jon Watte
    如果是这样,您需要进行两个步骤的过程——创建一个具有唯一URL的“转移”资源,然后将转移详细信息作为提交的一部分添加到其中(如其他答案中所述)。当然,这可以被表述为创建一个“事务”资源,然后向其中添加一个“转移”操作。 - Phasmal

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接