RESTful幂等性

6
我正在设计一个利用ROA(资源导向架构)的RESTful网络服务。
我试图找出一种有效的方法来保证PUT请求在服务器指定资源键的情况下创建新资源时具有幂等性。
据我所知,传统方法是创建一种事务资源类型,例如/CREATE_PERSON。创建新人员资源的客户端-服务器交互将分为两个部分: 步骤1:获取创建新个人资源的唯一事务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
您的创建操作使用了错误的HTTP动词。RFC 2616指定了POSTPUT操作的语义。
第9.5段:

POST方法用于请求 原始服务器接受请求中封装的实体, 作为Request-Line中标识的资源的新下属

第9.6段:

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

有一些微妙的细节,例如如果不存在指定URL的资源,则可以使用PUT来创建新资源。但是,POST不应该将新实体放置在请求URL上,而PUT应该始终将任何新实体放置在请求URL上。这种与请求URL的关系将POST定义为CREATE,将PUT定义为UPDATE
根据该语义,如果您想使用PUT创建新人员,则应在/CREATE_PERSON/{transaction_id}中创建。换句话说,第一个请求返回的事务ID应该是以后用于获取该记录的人员键。 您不应该向不会成为该记录最终位置的URL发出PUT请求。 更好的方法是,通过使用POST/CREATE_PERSON来将其作为原子操作执行。这使您可以使用单个请求创建新的人员记录,并在响应中获取新ID(该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,你的API客户端必须知道并发出请求来操作相同的资源。
或者你可以在事务记录中复制完整的最后资源状态的表示,并且将人员记录更新也通过事务URL进行更新。但是此时,事务URL实际上就是人员记录,这意味着它是由第一个POST请求创建的。

1
@Franci 我同意所选资源名称令人困惑,但我不认为他执行的操作有问题。只需更改名称为POST /PersonIdentifiers PUT/Person/{Identifier},我认为将标识符创建为单独的步骤没有问题。只要永远不会创建两个相同的标识符,并且没有要求每个标识符都必须实际用于创建人员,那么这对我来说就没问题。 - Darrel Miller
1
@Darrel - 这就是我试图让他做的。但是,他想通过POST请求创建一个单独的事务ID,并通过PUT请求到事务URL来创建人员记录,并返回实际的人员ID。 - Franci Penov
1
@Franci 好的,我错过了那个PersonKey。是的,我同意这很奇怪。统一接口的整个重点在于你要做人们期望的事情,而不仅仅是遵守法律条文。 - Darrel Miller
@Henrik - 嗯?!根据定义,POST不是幂等的。两个相同的POST应该会创建两个新的从属资源。就像HTTP一样。如果你POST并且新资源成功创建,响应应该包含Location头部和新资源的URI。没有“丢失的消息”。 - Franci Penov
但问题特别询问的是当将真实网络因素考虑进去时会发生什么--我可以想象在POST请求中添加一个带有请求ID的头部,以便服务器进行去重。 - Henrik
显示剩余12条评论

2

我刚看到这篇文章:

GUID不唯一的简单证明

虽然该问题被普遍嘲笑,但其中一些答案对GUID进行了更深入的解释。似乎GUID是大小为2^128的数字,随机生成两个相同大小的数字的概率非常低,几乎可以说是不可能的。

也许客户端可以生成自己大小为GUID的事务ID,而不是向服务器查询一个。如果有人能反驳这点,请告诉我。


1
在客户端创建GUID,然后执行PUT /Person/{Guid}是完全有效的。但是,我真的不明白“事务ID”的概念是用来做什么的。 - Darrel Miller
也许在这种情况下更好的术语应该是“request_id”。这个想法是为了让客户端如果第一次没有收到服务器的响应,可以再次发出相同的请求,并且可以确保幂等性。因为服务器可以查看请求 ID,如果它与已经存在的请求匹配,它会发送一个表明请求是重复的响应,而不是再次处理请求并将重复的人添加到数据库中。因此,请求是幂等的。 - Chris Dutrow
1
@ChrisDutrow,我认为你的方法很好,与PUT的设计意图相符。 - Mahmoud Abdelkader

1

我不确定我有一个直接回答你问题的答案,但我看到了一些可能会导致答案的问题。

你的第一个操作是GET,但它不是一个安全的操作,因为它正在“创建”一个新的交易ID。我建议使用POST更合适。

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

如果两次往返对于响应用户请求创建对象来说不是一个合理的开销,那么我建议HTTP不是你的场景下正确的协议。然而,如果你的用户需要一次创建大量的对象,那么我们可能可以找到更好的方式来暴露资源以实现这一目的。


没错,应该是一个POST请求,我已经修改了。你的回复可能凸显了我在这个架构方面的不足。我之前用ASP.NET构建过这个应用程序,但是它非常慢。 - Chris Dutrow
1
@DutrowLLC 我非常有信心地认为你的应用程序不会慢,因为当用户创建一个对象时,你只进行了两次往返。 - Darrel Miller
是的,ASP.NET 对于这个项目来说不太合适,似乎有很高的开销,导致一切都变慢了。此外,我使用了 Entity Framework,它也可能非常低效。 - Chris Dutrow

0

为什么不直接使用简单的POST方法,在第一次调用时包含有效载荷。这样可以节省额外的调用,并且不必生成事务:


POST /persons
first_name=foo

响应将是:


HTTP 201 CREATED
...
包含数据和自动生成的ID的有效载荷

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


1
这是正确的做法,但那个POST请求不是幂等请求,这似乎困扰了OP。 - Franci Penov
啊,我明白了...现在问题是为什么它需要幂等性? - manuel aldana
@manuelaldana 它需要是幂等的,因为他只想创建一个人,而不是两个。因此,如果第一个响应丢失了,客户端无法将其下一个请求与获取资源详细信息相关联。 - Henrik

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