【问题标题】:RESTful idempotenceRESTful 幂等性
【发布时间】:2013-05-21 00:26:31
【问题描述】:

我正在使用 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 并假设它是唯一值是公认的做法。这是因为可能值的数量如此之多,以至于随机生成两个相同数字的几率低到几乎不可能。

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

【问题讨论】:

    标签: rest idempotent


    【解决方案1】:

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

    第 9.5 段:

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

    第 9.6 段

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

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

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

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

    同时,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 中。特别是,您的方法中断的地方是您与 PUT 一起使用的 URL 是由 POST 创建的事务上下文的表示,而不是新资源本身的表示。换句话说,人员记录是更新交易记录的副作用,而不是它的直接结果(更新的交易记录)。

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

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

    【讨论】:

    • 如果您想让资源名称听起来更像一个名词,您可以将其重命名为 CREATE_PERSON_TRANSACTION。请求:PUT CREATE_PERSON_TRANSACTION/{transaction_id} 不违反资源必须保存在该 URL 的规则,因为:GET CREATE_PERSON_TRANSACTION/{transaction_id} 将返回有关事务的信息,例如事务是否正在进行或完成。它不会返回一个人。
    • 使用 POST PERSONS/ 创建新的 person 资源不是幂等的。幂等意味着请求可以执行一次或多次,结果是一样的。这很重要,因为如果客户端没有收到响应,客户端可以重新发送幂等请求。我发布这个问题的原因是为了获得关于幂等创建资源的想法。
    • @Franci 我同意所选择的资源名称令人困惑,但我不同意他正在执行的操作是一个问题。只需将名称更改为 POST /PersonIdentifiers PUT/Person/{Identifier} 我认为创建标识符作为单独的步骤没有问题。只要没有两个相同的标识符被创建,并且没有要求每个标识符都必须实际用于创建一个人,那么对我来说似乎没问题。
    • @Darrel - 这就是我试图让他做的事情。但是,他想用 POST 请求创建一个单独的事务 id,并使用 PUT 到一个 URL 来创建人员记录以进行事务,并取回实际的人员 id。
    • @Franci 好的,我错过了那个 PersonKey。是的,我同意这很奇怪。统一界面的全部意义在于您可以做人们期望的事情,而不仅仅是遵守法律条文。
    【解决方案2】:

    我刚刚看到这个帖子: Simple proof that GUID is not unique

    尽管这个问题被普遍讥讽,但其中一些答案进入了对 GUID 的更深层次的解释。似乎 GUID 是一个 2^128 大小的数字,并且随机生成两个相同大小的数字的几率很低,以至于对于所有实际目的都是不可能的。

    也许客户端可以只生成自己的事务 id,其大小与 GUID 一样大,而不是向服务器查询。如果有人可以诋毁这一点,请告诉我。

    【讨论】:

    • 在客户端上创建一个 GUID 然后执行 PUT /Person/{Guid} 绝对有效但是,我真的不明白“事务 id”这个概念的用途。
    • 在这种情况下,一个更好的术语可能是“request_id”。这个想法是,如果客户端第一次没有收到来自服务器的响应并且对幂等性有信心,它可以再次发出相同的请求。该请求将是幂等的,因为服务器可以查看 request_id,如果它匹配已经发出的请求,它将发送一个响应,指示该请求是重复的,而不是再次处理请求并将重复的人添加到数据库中。
    • @ChrisDutrow,我认为您的方法很到位,并且似乎符合 PUT 的设计意图。
    【解决方案3】:

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

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

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

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

    【讨论】:

    • 是的,你是对的,它应该是一个 POST,我改变了它。您的回复可能突出了我对这种架构的缺乏经验。我以前使用 ASP.NET 构建过这个应用程序,它非常非常慢。
    • @DutrowLLC 我非常有信心您的应用程序并不慢,因为当用户创建对象时您会进行两次往返。
    • 是的,ASP.NET 不适合该项目,而且开销似乎非常高,从而减慢了一切速度。此外,我使用的实体框架也可能非常低效。
    【解决方案4】:

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

    POST /persons first_name=foo

    回复是:

    HTTP 201 CREATED ... payload_containing_data_and_auto_generated_id

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

    【讨论】:

    • 这是正确的做法,但是POST请求不是幂等请求,这似乎是OP的错误。
    • 啊,我明白了……现在的问题是为什么它需要是幂等的?
    • @manuelaldana 它需要是幂等的,因为他只想创建一个人,而不是两个人。因此,如果第一个响应丢失,客户端无法关联其下一个请求以获取资源详细信息
    猜你喜欢
    • 2012-12-09
    • 2012-01-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-05-01
    • 1970-01-01
    • 2016-01-08
    • 1970-01-01
    相关资源
    最近更新 更多