5

我正在使用 ROA(面向资源的架构)设计一个 RESTful Web 服务。

我试图找出一种有效的方法来保证在服务器指定资源密钥的情况下创建新资源的 PUT 请求的幂等性。

据我了解,传统的做法是创建一种事务资源,例如 /CREATE_PERSON。用于创建新人员资源的客户端-服务器交互将分为两部分:

第 1 步:获取用于创建新 PERSON 资源的唯一事务 ID:::

**Client request:**
POST /CREATE_PERSON

**Server response:**
200 OK
transaction-id:"as8yfasiob"

第 2 步:使用事务 id::: 在保证唯一的请求中创建新人员资源

**Client request**
PUT /CREATE_PERSON/{transaction_id}
first_name="Big bubba"

**Server response**
201 Created             // (If the request is a duplicate, it would send this
PersonKey="398u4nsdf"   // same response without creating a new resource.  It
                        // would perhaps send an error response if the was used
                        // on a transaction id non-duplicate request, but I have
                        // control over the client, so I can guarantee that this
                        // won't happen)

我用这种方法看到的问题是它需要向服务器发送两个请求才能执行创建新 PERSON 资源的单个操作。这会产生性能问题,增加用户等待客户端完成其请求的机会。

我一直在尝试消除第一步的想法,例如在每个请求中预先发送事务 ID,但我的大多数想法都有其他问题或涉及牺牲应用程序的无状态性。

有没有办法做到这一点?

编辑::::::

我们最终采用的解决方案是让客户端获取 UUID 并将其与请求一起发送。UUID 是一个非常大的数字,占用 16 个字节 (2^128) 的空间。与具有编程头脑的人可能直观地认为的相反,随机生成 UUID 并假设它是唯一值是公认的做法。这是因为可能值的数量如此之大,以至于随机生成两个相同数字的几率低到几乎不可能。

一个警告是我们让我们的客户从服务器请求一个 UUID ( GET uuid/)。这是因为我们无法保证客户端运行的环境。如果出现问题,例如在客户端上播种随机数生成器,那么很可能会发生 UUID 冲突。

4

4 回答 4

4

您在创建操作中使用了错误的 HTTP 动词。RFC 2616指定了 和 的操作POST语义PUT

第 9.5 段:

POST方法用于请求源服务器接受包含在请求中的实体作为由 Request-Line 中的 Request-URI 标识的资源的新下级

第 9.6 段

PUT方法请求将封闭的实体存储在提供的 Request-URI 下。

该行为有一些微妙的细节,例如PUT,可以用于在指定的 URL 上创建新资源(如果尚不存在)。但是,POST永远不应将新实体放在请求 URL 处,而PUT应始终将任何新实体放在请求 URL 处。这种与请求 URL 的关系定义POST为 asCREATEPUTas UPDATE

按照这个语义,如果你想用来PUT创建一个新人,它应该在/CREATE_PERSON/{transaction_id}. 换句话说,您的第一个请求返回的事务 ID 应该是稍后用于获取该记录的人员键。您不应向不会PUT成为该记录最终位置的 URL 发出请求。

不过,更好的是,您可以通过使用POSTto 将其作为原子操作来执行/CREATE_PERSON。这允许您通过一个请求来创建新的人员记录并在响应中获取新的 ID(也应该在 HTTPLocation标头中引用)。

同时,REST 指南指定动词不应成为资源 URL 的一部分。因此,创建新人的 URL 应该与获取所有人列表的位置相同 - /PERSONS(我更喜欢复数形式 :-))。

因此,您的 REST API 变为:

  • 让所有人 -GET /PERSONS
  • 获得单身人士 -GET /PERSONS/{id}
  • 创建新人 -POST /PERSONS正文包含新记录的数据
  • 更新现有人员或创建具有已知 id 的新人员 -PUT /PERSONS/{id}正文包含更新记录的数据。
  • 删除现有的人 -DELETE /PERSONS/{id}

注意:我个人不喜欢使用 PUT 来创建记录,原因有两个,除非我需要创建一个与来自不同数据集的现有记录具有相同 id 的子记录(也称为“穷人的外键”: -))。

更新:你说得对,这POST不是幂等的,而且符合 HTTP 规范。POST始终返回新资源。在上面的示例中,新资源将是事务上下文。

但是,我的观点是,您希望PUT用于创建新资源(人员记录),并且根据 HTTP 规范,新资源本身应该位于 URL 中。特别是,您的方法中断的地方是您使用的 URLPUT是由 POST 创建的事务上下文的表示,而不是新资源本身的表示。换句话说,人员记录是更新交易记录的副作用,而不是它的直接结果(更新的交易记录)。

当然,使用这种方法,PUT请求将是幂等的,因为一旦创建了人员记录并且事务“完成”,后续PUT请求将什么也不做。但是现在您遇到了一个不同的问题 - 要实际更新该人员记录,您需要向PUT不同的 URL 发出请求 - 一个代表人员记录的 URL,而不是创建它的事务。因此,现在您有两个单独的 URL,您的 API 客户端必须知道并发出请求以操作相同的资源。

或者,您也可以在事务记录中复制最后一个资源状态的完整表示,并让人员记录更新也通过事务 URL 进行更新。但是此时,事务 URL用于人员记录的意图和目的,这意味着它首先是由POST请求创建的。

于 2010-06-02T03:56:28.593 回答
2

我刚看到这篇文章: GUID 不是唯一的简单证明

Although the question is universally ridiculed, some of the answers go into deeper explanation of GUIDs. It seems that a GUID is a number of 2^128 in size and that the odds of randomly generating two of the same numbers of this size so low as to be impossible for all practical purposes.

Perhaps the client could just generate its own transaction id the size of a GUID instead of querying the server for one. If anyone can discredit this, please let me know.

于 2010-06-05T01:39:40.623 回答
1

我不确定我是否能直接回答您的问题,但我看到了一些可能导致答案的问题。

您的第一个操作是 GET,但它不是安全操作,因为它正在“创建”一个新的事务 ID。我建议 POST 是一个更合适的动词。

您提到您担心由两次往返引起的用户感知的性能问题。这是因为您的用户要一次创建 500 个对象,还是因为您的网络存在大量延迟问题?

如果两次往返不是为响应用户请求而创建对象的合理费用,那么我建议 HTTP 不是适合您的方案的协议。但是,如果您的用户需要一次创建大量对象,那么我们可能会找到一种更好的方法来公开资源以实现它。

于 2010-06-02T03:42:28.267 回答
0

你为什么不只使用一个简单的 POST,还包括你第一次调用的有效负载。这样您就可以节省额外的通话费用并且不必产生交易:


POST /persons

first_name=foo

回应是:


HTTP 201 CREATED
...
payload_containing_data_and_auto_generated_id

服务器内部会生成一个 id。为简单起见,我会选择人工主键(例如,数据库中的自动增量 ID)。

于 2010-06-02T20:16:06.303 回答