在REST中进行交易?


147

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

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

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

Answers:


91

考虑一个RESTful购物篮方案。从概念上讲,购物篮是您的事务包装器。可以将多个项目添加到购物篮中,然后提交该购物篮以处理订单,您可以将Bob的帐户条目添加到事务包装器中,然后将Bill的帐户条目添加到包装器中。当所有零件都放置到位后,您可以将所有组件零件进行POST / PUT事务包装。


18
为什么TransferMoneyTransaction不是可行的银行资源?
Darrel Miller's

8
如果您确保端点引用了名词,那么通常可以很直观地了解标准GET,PUT,POST,DELETE动词将如何处理该名词。RPC允许端点本身就是动词,因此它们可能与HTTP动词发生冲突,并且意图变得混乱。
Darrel Miller's

10
例如,如果在端点UpdateXYZ上执行HTTP DELETE,会发生什么情况?它会删除XYZ吗?它是删除Update还是只是执行Update而忽略HTTP动词删除。通过将动词放在端点之外,可以消除混乱。
Darrel Miller's

5
跨多个服务的交易又如何呢?以及当您要进行一组“不相关”更改而该服务不公开任何隐式事务容器时如何处理呢?此外,为什么当我们转移到与您的实际数据完全不相关的通用事务时具有特定的事务类型变化。事务可能不匹配静态,但似乎事务应该放在顶层,与其余调用无关,除了请求标头包含事务引用这一事实。
meandmycode

4
@meandmycode数据库事务应位于REST接口后面。或者,您可以将业务事务(而不是数据库事务)本身作为资源公开,然后在发生故障时需要采取补偿措施。
Darrel Miller'2

60

有几个重要的问题没有被这个问题回答,我认为这太糟糕了,因为它在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", ...}

注意,在这一点上,多次看跌并不重要。甚至在txn上的GET都将返回当前状态。具体来说,第二个PUT会检测到第一个PUT已经处于适当的状态,然后将其返回-或者,如果在它已经处于“提交”状态之后尝试将其置于“回滚”状态,则会得到一个错误,并将实际的已提交事务退回。

只要您与单个数据库或具有集成事务监控器的数据库进行对话,该机制实际上就可以正常工作。您可能还会引入事务超时,如果愿意,您甚至可以使用Expires标头来表示。


有趣的讨论!我想补充一点,初始发布必须一步完成。以后不能再添加它了(那时我们处于购物车区域,购物车有许多制衡手段,以防止它们对最终用户造成伤害,即使是立法,银行转账也不会)……
Erk

33

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

POST /transaction

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

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

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

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

如果之前已对ID为“ 1234”的事务进行了PUT,则服务器将给出错误响应,否则将给出OK响应以及用于查看已完成事务的URL。

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


4
将REST与CRUD等同是一个严重的错误。POST不必表示CREATE。

12
严重的错误?我知道PUT和POST之间有区别,但是对CRUD的映射很松散。“认真”?
泰德·约翰逊

3
是的,认真。CRUD是一种结构化数据存储的方式。REST是一种构造应用程序数据流的方法。您可以在REST上执行CRUD,但不能在CRUD上执行REST。它们不相等。
乔恩·瓦特

20

很好的问题,REST主要通过类似于数据库的示例进行解释,其中存储,更新,检索,删除了某些内容。很少有这样的示例,其中服务器应该以某种方式处理数据。我认为罗伊·菲尔丁(Roy Fielding)毕竟没有以http为基础的论文。

但是他确实谈到了“代表性状态转移”作为状态机,并将链接移至下一个状态。这样,文档(表示形式)就可以跟踪客户端状态,而不必由服务器来执行。这样,就没有客户端状态,只有关于您所处链接的状态。

我一直在考虑这个问题,在我看来,让服务器为您处理某些事情是合理的,当您上传时,服务器会自动创建相关资源,并为您提供链接到它们的资源(实际上,无需自动创建它们:它可以告诉您链接,并且仅当您关注它们时才创建它们-惰性创建)。并为您提供创建新的相关资源的链接-相关资源具有相同的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调用)”。即名词“ committed transaction resource”而不是动词“ commit this transaction”。我想标称化的一个优点是您可以按名称引用资源,而不需要以其他方式指定资源(例如维护会话状态,因此您知道“此”事务是什么...)

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


您能否解释一下“不需要保持客户端状态”如何有助于可扩展性?什么样的可扩展性?可扩展性是什么意思?
jhegedus

11

如果您退一步来总结这里的讨论,那么很显然REST不适合许多API,特别是当客户端与服务器之间的交互本质上是有状态的时,尤其是在非平凡事务中。为什么要为客户端和服务器都跳过建议的所有步骤,以认真地遵循一些不适合该问题的原则?更好的原则是为客户提供最简单,最自然,高效的方式来编写应用程序。

总而言之,如果您确实在应用程序中执行大量事务(类型,而不是实例),则实际上不应该创建RESTful API。


9
是的,但是在分布式微服务架构的情况下应该有什么替代方案?
Vitamon'4

11

我离开这个话题已经有十年了。回来时,我不敢相信当您Google休息+可靠时,您会迷恋成科学的宗教。混乱是神话。

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

  • 下游服务。您开发的任何Web服务都将具有您使用的下游服务,并且您必须遵循其事务语法。您应该尝试对服务的用户隐藏所有这些内容,并确保操作的所有部分作为一个组成功或失败,然后将此结果返回给用户。
  • 您的服务。客户希望Web服务调用的结果明确无误,而通常直接在实质性资源上进行POST,PUT或DELETE请求的REST模式让我感到不满意,并且很容易改进这种确定性的方式。如果您关心可靠性,则需要确定操作请求。该ID可以是在客户端上创建的GUID,也可以是服务器上关系DB的种子值。对于服务器生成的ID,请使用“预检”请求-响应来交换操作的ID。如果此请求失败或成功一半,则没问题,客户端将重复该请求。未使用的ID无害。

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

您的要求是基本要求。不要让别人告诉您您的解决方案不是犹太教。根据解决您的问题的能力和简便程度来判断他们的体系结构。


9

您必须推出自己的“交易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)

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

在公园里,这并不是真正令人愉快的一天。


4
我认为这不是一个很好的例证。您仅需要两个步骤:创建事务(在“待处理”状态下创建事务)和提交事务(在未提交时提交,并将资源移到已提交或回滚状态)。
乔恩·瓦特

2

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

我真的认为,当您只需要利用数据库来执行此操作时,创建一个自定义事务管理器就不会花额外的钱。


2

首先,转移资金是您在单个资源调用中无法做到的。您要执行的操作是汇款。因此,您可以将汇款资源添加到发送者的帐户中。

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

做完了 您无需知道这是必须是原子性的交易。您只需再汇款即可。从A到B汇款。


但在极少数情况下,这里是一个通用解决方案:

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

如果您转移状态,则需要向客户端隐藏内部信息。客户不应只知道实施所需的内部信息,而不能携带与业务相关的信息。如果这些信息没有商业价值,则应加密状态,并需要使用诸如令牌,通行证之类的隐喻。

这样,就可以传递内部状态,并使用加密和签名系统仍然是安全可靠的。为客户找到正确的抽象为什么他会传递状态信息,这取决于设计和体系结构。


真正的解决方案:

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

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

Cookies被设计为网站记住状态信息(例如购物车中的物品)或记录用户的浏览活动(包括单击特定按钮,登录或记录用户迄今为止访问过的页面)的可靠机制。追溯到几个月或几年前)。

因此,基本上,如果您需要传递状态,请使用Cookie。出于完全相同的原因设计它,它是HTTP,因此从设计上就与REST兼容:)。


更好的解决方案:

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

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

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

一个复杂的例子:

买房子:

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

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

为此,您只需给代理商任务购买房屋即可,例如:

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

做完了 代理商会将您的参考发回给您,您可以使用该参考来查看和跟踪此工作的状态,其余的则由代理商的代理商自动完成。

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


1

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

REST约束之一:

无状态

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

唯一的RESTful方法是创建事务重做日志并将其置于客户端状态。客户端根据请求发送重做日志,服务器重做事务,

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

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


引用来自Wikipedia REST条目。这是真正的来源,还是维基百科从某个地方得到的?谁说什么是客户端上下文,什么是服务器上下文?
bbsimonbb

1

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

我认为将客户端生成的GUID字段与转账对象一起使用,并确保不再再次插入相同的GUID,这将是解决银行转账事宜的更简单解决方案。

不知道更复杂的情况,例如多张机票预订或微型架构。

我找到了一篇有关该主题的论文,涉及在RESTful服务处理事务原子性的经验。


0

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

因此,要在<url-base>/account/a和之间转移<url-base>/account/b,您可以将以下内容发布到<url-base>/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上实现两阶段提交的最佳想法是rest.blueoxen.net/cgi-bin/wiki.pl?TwoPhaseCommit,重要的是不会弄乱URL名称空间,并且允许将两阶段提交分层干净的REST语义。
Phasmal

3
这个建议的另一个问题是,如果两次缓存打扰和POST,您将获得两次传输。
乔恩·瓦特

没错,在这种情况下,您需要执行两个步骤-创建具有唯一URL的“传输”资源,然后将传输详细信息添加到该资源中,作为提交的一部分(其他答案中提到了两部分)。当然,然后可以将其表述为创建“事务”资源,然后向其添加“传输”操作。
Phasmal

-3

我想您可以在网址/资源中包含TAN:

  1. 进行交易/交易以获取ID(例如“ 1”)
  2. [PUT,GET,POST,等等] / 1 / account / bob
  3. [PUT,GET,POST,等等] / 1 /帐户/帐单
  4. ID为1的删除/交易

只是个主意。


我看到这种方法有两个问题:1)这意味着您不能访问事务外部的资源(尽管这可能不大)。2)到目前为止,没有任何答案能解决服务器不再是无状态的事实,尽管我怀疑对此无能为力。
吉利

好吧,/ 1 / account / bob和/ account / bob只是两个不同的资源。:)和RE:无状态,这意味着资源始终可用,并且不依赖于先前的请求。由于您要求交易,是的,事实并非如此。但是话又说回来,您想要交易。
直到

1
如果客户端必须汇编URI,则您的API不是RESTful的。
aehlke

1
我真不明白你们!如果您将事务视为资源(如上例中所示),则只需停止传统意义上的事务处理,并以“适当的REST方式”使用该事务,从而进一步简化了对事务处理的编程。例如,您可以在响应中包含对事务的href,以解决分布式服务器端环境中的替换问题,它仍然是无状态的(只是资源,不是吗?),您仍然可以实施实际的事务机制想要(如果后面没有DB会怎样?)
Matthias Hryniszak 2010年

1
如果您只是简单地停止思考SQL / SOAP而开始思考HTTP(就像浏览器一样),那么一切都变得很简单
Matthias Hryniszak,2010年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.