REST API設計:如何處理Http併發一致性事務更新? - mscharhag

banq發表於2021-02-25

併發控制可能是REST API的重要組成部分,尤其是當您期望對同一資源的併發更新請求時。在本文中,我們將介紹If-Unmodified-Since和If-Match標頭不同的選項,從而避免透過HTTP丟失更新。
讓我們從一個示例請求流開始,以瞭解問題:

REST API設計:如何處理Http併發一致性事務更新? - mscharhag
愛麗絲和鮑勃向伺服器請求資源/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(前提條件失敗)以外的狀態程式碼。換句話說,重定向和失敗優先於條件請求中前提條件的評估。

這通常導致更新請求的處理順序如下:

REST API設計:如何處理Http併發一致性事務更新? - mscharhag
在評估前提條件之前,我們會檢查請求是否滿足所有其他要求。如果不是這種情況,我們將使用標準的4xx狀態程式碼進行響應。這樣,我們確保412狀態程式碼不會抑制其他錯誤。


 

相關文章