我正在使用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冲突。
答案 0 :(得分:4)
您正在使用错误的HTTP谓词进行创建操作。 RFC 2616指定POST
和PUT
的操作的语义。
第9.5段:
POST
方法用于请求 原始服务器接受 请求中包含的实体 资源的新下属 由请求行中的Request-URI标识
第9.6段
PUT
方法请求 封闭的实体存储在 提供了Request-URI。
该行为有细微的细节,例如PUT
可用于在指定的URL上创建新资源(如果尚不存在)。但是,POST
永远不应将新实体放在请求URL中,而PUT
应始终将任何新实体放在请求URL中。与请求网址的此关系将POST
定义为CREATE
,将PUT
定义为UPDATE
。
根据该语义,如果您想使用PUT
创建新人,则应在/CREATE_PERSON/{transaction_id}
中创建。换句话说,第一个请求返回的事务ID应该是稍后用于获取该记录的人员密钥。 您不应向{1}}请求不会是该记录的最终位置的网址。
但是,更好的是,您可以使用PUT
到POST
作为原子操作执行此操作。这允许您使用单个请求创建新人员记录,并在响应中获取新ID(也应在HTTP /CREATE_PERSON
标头中引用)。
同时,REST准则指定动词不应该是资源URL的一部分。因此,创建新人的URL应该与获取所有人员列表的位置相同 - Location
(我更喜欢复数形式: - ))。
因此,您的REST API变为:
/PERSONS
GET /PERSONS
GET /PERSONS/{id}
,其中正文包含新记录的数据POST /PERSONS
的新人。PUT /PERSONS/{id}
注意:我个人不希望因为两个原因而不使用PUT来创建记录,除非我需要创建一个与来自不同数据集的已存在记录具有相同id的子记录(也称为'穷人的外国人) key': - ))。
更新:你是对的,DELETE /PERSONS/{id}
不是幂等的,而是按照HTTP规范。 POST
将始终返回新资源。在上面的示例中,新资源将是事务上下文。
但是,我的观点是,您希望POST
用于创建新资源(人员记录),并根据HTTP规范,新资源本身应位于URL。特别是,您的方法中断的地方是您使用PUT
的URL是POST创建的事务上下文的表示,而不是新资源本身的表示。换句话说,人员记录是更新交易记录的副作用,而不是它的直接结果(更新的交易记录)。
当然,使用这种方法,PUT
请求将是幂等的,因为一旦创建了人员记录并且事务被“最终确定”,后续PUT
请求将不执行任何操作。但是现在你有一个不同的问题 - 要真正更新那个人的记录,你需要向另一个URL发出PUT
请求 - 一个代表人员记录的URL,而不是创建它的事务。因此,现在您的API客户端必须知道两个单独的URL,并请求操作相同的资源。
或者您也可以完整地表示在交易记录中复制的最后一个资源状态,并且人员记录更新也会通过事务URL进行更新。但此时,事务URL 用于人员记录的意图和目的,这意味着它首先由PUT
请求创建。
答案 1 :(得分:1)
我不确定我对您的问题有直接的答案,但我看到一些可能导致答案的问题。
您的第一个操作是GET,但它不是一个安全的操作,因为它正在“创建”新的事务ID。我建议POST是一个更合适的动词。
您提到您担心两次往返会导致用户感知到的性能问题。这是因为您的用户将同时创建500个对象,还是因为您处于具有大量延迟问题的网络上?
如果两次往返不是创建对象以响应用户请求的合理费用,那么我建议HTTP不适合您的方案。但是,如果您的用户需要同时创建大量对象,那么我们可能会找到一种更好的方式来公开资源以实现这一目标。
答案 2 :(得分:1)
我刚看到这篇文章: Simple proof that GUID is not unique
虽然这个问题受到普遍嘲笑,但有些答案会对GUID进行更深入的解释。似乎GUID的大小为2 ^ 128,并且随机生成两个相同数量的这个大小的几率非常低,以至于不可能用于所有实际目的。
也许客户端只能生成自己的GUID大小的事务ID,而不是查询服务器的一个。如果有人可以诋毁这个,请告诉我。
答案 3 :(得分:0)
为什么不使用简单的POST,也包括首次呼叫时的有效负载。这样你就节省了额外的电话而不必产生交易:
POST /persons
first_name=foo
响应将是:
HTTP 201 CREATED
...
payload_containing_data_and_auto_generated_id
server-internal会生成一个id。为简单起见,我会选择一个人工主键(例如,从数据库中自动增加id)。