REST API设计:如何处理Http并发一致性事务更新? - mscharhag


并发控制可能是REST API的重要组成部分,尤其是当您期望对同一资源的并发更新请求时。在本文中,我们将介绍If-Unmodified-Since和If-Match标头不同的选项,从而避免通过HTTP丢失更新。
让我们从一个示例请求流开始,以了解问题:

爱丽丝和鲍勃向服务器请求资源/articles/123,服务器以当前资源状态做出响应。然后,鲍勃基于先前接收的数据执行更新请求。此后不久,爱丽丝也执行了更新请求。爱丽丝的请求也基于先前接收的资源,并且不包括鲍勃所做的更改。服务器完成对爱丽丝的更新的处理后,鲍勃的更改已丢失。
HTTP提供了针对此问题的解决方案:条件请求,在RFC 7232中定义
条件请求使用在特定标头中定义的验证器和前提条件。验证器是服务器生成的元数据,可用于定义前提条件。例如,最后修改日期或ETag是可用于前提条件的验证器。根据这些前提条件,服务器可以决定是否应执行更新请求。
对于状态更改请求,If-Unmodified-Since和If-Match标头特别有趣。在下一部分中,我们将学习如何使用这些标头避免并发更新。
 
If-Unmodified-Since和If-Match标头一起使用
避免更新丢失的最简单方法是使用上次修改日期,保存资源的上次修改日期通常是个好主意,因此很可能我们的数据库中已经具有此值。如果不是这种情况,通常很容易添加。
现在,当将响应返回给客户端时,我们可以在Last-Modified响应标头中添加上次修改日期。上次修改标头使用的格式如下:

<day-name>, <day> <month-name> <year> <hour>:<minute>:<second> GMT

请求:
GET /articles/123

响应:
HTTP/1.1 200 OK
Last-Modified: Sat, 13 Feb 2021 12:34:56 GMT

{
    "title": "Sunny summer",
   
"text": "bla bla ..."
}

为了更新此资源,客户端现在必须将If-Unmodified-Since标头添加到请求中。此标头的值设置为从上一个GET请求检索到的最后修改日期。
示例更新请求:

PUT /articles/123
If-Unmodified-Since: Sat, 13 Feb 2021 12:34:56 GMT

{
    "title": "Sunny winter",
   
"text": "bla bla ..."
}

在执行更新之前,服务器必须将资源的最后修改日期与If-Unmodified-Since标头中的值进行比较。仅当两个值相同时才执行更新。
有人可能会说,检查资源的最后修改日期是否比If-Unmodified-Since标头的值新就足够了。但是,这使客户可以选择发送已修改的上次修改日期(例如,将来的日期)来否决其他并发请求。
这种方法的问题在于,Last-Modified标头的精度限制为秒。如果在同一秒内执行多个并发更新请求,我们仍然会遇到丢失更新的问题。
 
将ETag与If-Match标头一起使用
另一种方法是使用实​​体标签(ETag)。ETag是服务器为请求的资源表示形式生成的不透明字符串。例如,资源表示的哈希可以用作ETag。
ETag使用ETag标头发送到客户端。例如:
GET /articles/123

响应:
HTTP/1.1 200 OK
ETag: "a915ecb02a9136f8cfc0c2c5b2129c4b"

{
   
"title": "Sunny summer",
   
"text": "bla bla ..."
}

更新资源时,客户端将ETag标头发送回服务器:

PUT /articles/123
ETag: "a915ecb02a9136f8cfc0c2c5b2129c4b"

{
   
"title": "Sunny winter",
   
"text": "bla bla ..."
}

现在,服务器将验证ETag标头是否与资源的当前表示形式匹配。如果ETag不匹配,则服务器上的资源状态已在GET和PUT请求之间更改。
 
强弱验证
RFC 7232区分弱验证和强验证:
弱验证器易于生成,但对比较却没有多大用处。强大的验证器是比较的理想选择,但要高效生成可能非常困难(有时甚至不可能)。
只要资源表示形式发生变化,强验证器就会发生变化。相反,弱验证器不会在每次资源表示更改时都更改。
ETag可以生成弱变体和强变体。弱ETag必须以W /为前缀。
以下是一些示例ETag:
弱ETag:
ETag: W/"abcd"
ETag: W/
"123"

强ETag:
ETag: "a915ecb02a9136f8cfc0c2c5b2129c4b"
ETag:
"ngl7Kfe73Mta"

除了并发控制外,前提条件通常还用于缓存和带宽减少。在这些情况下,弱验证者可能就足够了。对于REST API中的并发控制,通常最好使用强验证器。
请注意,由于精度有限,使用Last-Modified和If-Unmodified-Since标头被认为是较弱的。我们不能确定服务器状态是否已在同一秒内被另一个请求更改。但是,如果这是一个实际问题,则取决于您期望的并发更新请求的数量。
 
计算Etags
对于特定资源的所有表示形式的所有版本,强ETag必须是唯一的。例如,同一资源的JSON和XML表示应具有不同的ETag。
生成和验证强大的ETag可能会有些棘手。例如,假设我们在将资源发送给客户端之前通过对资源的JSON表示进行散列来生成ETag。为了验证更新请求的ETag,我们现在必须加载资源,将其转换为JSON,然后对JSON表示进行哈希处理。
在最佳情况下,资源包含跟踪更改的特定于实现的字段。这可以是确切的上次修改日期,也可以是某种形式的内部修订号。例如,当使用带有乐观锁定的数据库框架(如Java Persistence API(JPA))时,我们可能已经拥有一个版本字段,该字段随每次更改而增加。
然后,我们可以通过对资源ID,媒体类型(例如application / json)以及上次修改日期或修订号进行哈希处理来计算ETag 。
 
HTTP状态码和执行顺序
使用前提条件时,两个HTTP状态代码是相关的:
  • 412-前提条件失败表示服务器上一个或多个前提条件评估为假(例如,因为服务器上的资源状态已更改)
  • - 428所需的先决条件在已添加RFC 6585和指示服务器需要请求是有条件的。如果更新请求不包含预期的前提条件,则服务器应返回此状态代码

RFC 7232还定义了HTTP 412的评估顺序(前提条件失败):
  • [..]接收者缓存或原始服务器必须在成功执行其正常请求检查之后并且即将执行与请求方法关联的操作之前,评估接收到的请求前提条件。如果服务器对同一请求的响应没有其他条件,则它必须忽略所有接收到的前提条件,而不是2xx(成功)或412(前提条件失败)以外的状态代码。换句话说,重定向和失败优先于条件请求中前提条件的评估。

这通常导致更新请求的处理顺序如下:

在评估前提条件之前,我们会检查请求是否满足所有其他要求。如果不是这种情况,我们将使用标准的4xx状态代码进行响应。这样,我们确保412状态代码不会抑制其他错误。