Skip to content
ADP
API Design PrincipleBETA

[ADP-362] 並發控制

reviewing phase 1

增加參考資料

概述

本規範涵蓋了兩種主要的並發控制機制: 樂觀鎖定和悲觀鎖定。

指導原則

  • 如有必要,必須(MUST)確保並發操作期間的數據一致性和完整性。
  • 應該(SHOULD)根據應用程序的需求和特性使用適當的並發控制機制。
  • 必須(MUST)優雅地處理衝突並適當地通知客戶端。

樂觀鎖定

概念: 樂觀鎖定假設衝突很少發生,只在提交事務時檢查衝突。它使用 ETag 來檢測衝突。

實施指南:

  1. 使用 ETag 進行資源版本控制:

    • 必須在資源表示中包含 ETag 標頭作為版本標識符。
    • 對於需要精確控制的資源,應該使用強 ETag。

    資源表示示例:

    http
    GET /users/123 HTTP/1.1
    Host: example.com
    
    HTTP/1.1 200 OK
    Content-Type: application/json
    ETag: "abc123"
    
    {
      "id": 123,
      "email": "user@example.com",
      "phone": "+1234567890"
    }
  2. 在更新請求中包含 ETag:

    • 必須要求客戶端在更新請求中使用 If-Match 標頭包含 ETag。
    • 如果缺少 If-Match 標頭,應該拒絕更新。

    包含 ETag 的更新請求示例:

    http
    PATCH /users/123 HTTP/1.1
    Host: example.com
    Content-Type: application/json-patch+json
    If-Match: "abc123"
    
    [
      { "op": "replace", "path": "/email", "value": "newemail@example.com" }
    ]
  3. 衝突檢測:

    • 在應用更新之前,必須比較提供的 ETag 與資源的當前 ETag。
    • 如果 ETag 不匹配,表示資源已被另一個客戶端修改,必須拒絕更新。
  4. 錯誤回應:

    • 當檢測到衝突時,必須返回 412 Precondition Failed 狀態碼。
    • 應該使用 HTTP Problem Details 格式進行錯誤回應。

    衝突回應示例:

    http
    HTTP/1.1 412 Precondition Failed
    Content-Type: application/problem+json
    
    {
      "type": "https://example.com/problems/concurrent-update",
      "title": "前提條件失敗",
      "status": 412,
      "detail": "該資源已被另一個客戶端修改。",
      "instance": "/users/123",
      "currentETag": "def456"
    }
  5. 更新 ETag:

    • 每次成功更新資源時,必須生成新的 ETag。
    • 應該在回應標頭中包含更新後的 ETag。

    成功更新回應示例:

    http
    HTTP/1.1 200 OK
    Content-Type: application/json
    ETag: "def456"
    
    {
      "id": 123,
      "email": "newemail@example.com",
      "phone": "+1234567890"
    }

悲觀鎖定

概念: 悲觀鎖定假設衝突經常發生,並在操作期間鎖定資源以防止其他客戶端修改它。

實施指南:

  1. 鎖定獲取:

    • 在進行任何修改之前必須鎖定資源。
    • 應該使用鎖定機制;我們在此引入類似 WebDAV 的機制。
    • 當鎖定請求成功時,應該在回應標頭中返回 Lock-Token
    • 應該在回應標頭中指定 Timeout
  2. 鎖定期間的操作:

    • 應該在回應標頭中指定 Lock-Token
  3. 鎖定釋放:

    • 操作完成後必須釋放鎖定。
    • 應該確保即使操作失敗也釋放鎖定,通常使用 try-finally 塊或服務器超時機制。
    • 應該在請求標頭中指定 Lock-Token
  4. 處理鎖定失敗:

    • 如果資源已被另一個客戶端鎖定,必須返回 423 Locked 狀態碼。
    • 應該使用 HTTP Problem Details 格式進行錯誤回應。

    鎖定失敗回應示例:

    http
    HTTP/1.1 423 Locked
    Content-Type: application/problem+json
    
    {
      "type": "https://example.com/problems/locked",
      "title": "已鎖定",
      "status": 423,
      "detail": "該資源當前被另一個操作鎖定。",
      "instance": "/users/123"
    }
  5. 長時間運行的操作:

    • 應該最小化鎖定持續時間,以避免其他客戶端長時間等待。
    • 可以使用鎖定超時機制來釋放持有時間過長的鎖定。

最佳實踐

  1. 清晰的文件:
    • 應該清楚地記錄所使用的並發控制機制,包括如何在請求中包含 ETag 以及如何處理衝突。
  2. 客戶端處理:
    • 應該建議客戶端如何處理 412 Precondition Failed423 Locked 回應,包括重試策略。
  3. 一致性:
    • 必須確保在資源的所有實例中一致地更新和檢查 ETag。
  4. 冪等操作:
    • 應該設計更新操作為冪等的,以便使用相同的 ETag 重試操作不會導致不一致的狀態。

樂觀鎖定的示例工作流程

  1. 客戶端獲取資源:

    • 客戶端檢索資源並獲取當前的 ETag。
    http
    GET /users/123 HTTP/1.1
    Host: example.com

    回應:

    http
    HTTP/1.1 200 OK
    Content-Type: application/json
    ETag: "abc123"
    
    {
      "id": 123,
      "email": "user@example.com",
      "phone": "+1234567890"
    }
  2. 客戶端發送更新請求:

    • 客戶端使用 If-Match 標頭在更新請求中包含 ETag。
    http
    PATCH /users/123 HTTP/1.1
    Host: example.com
    Content-Type: application/json-patch+json
    If-Match: "abc123"
    
    [
      { "op": "replace", "path": "/email", "value": "newemail@example.com" }
    ]
  3. 服務器檢查 ETag:

    • 服務器檢查提供的 ETag 是否與資源的當前 ETag 匹配。
    • 如果 ETag 匹配,服務器應用更新並生成新的 ETag。
    • 如果 ETag 不匹配,服務器返回 412 Precondition Failed 回應。
  4. 成功更新回應:

    • 如果更新成功,服務器回應更新後的資源和新的 ETag。
    http
    HTTP/1.1 200 OK
    Content-Type: application/json
    ETag: "def456"
    
    {
      "id": 123,
      "email": "newemail@example.com",
      "phone": "+1234567890"
    }
  5. 衝突回應:

    • 如果檢測到衝突,服務器回應 412 Precondition Failed 狀態碼和資源的當前 ETag。
    http
    HTTP/1.1 412 Precondition Failed
    Content-Type: application/problem+json
    
    {
      "type": "https://example.com/problems/concurrent-update",
      "title": "前提條件失敗",
      "status": 412,
      "detail": "該資源已被另一個客戶端修改。",
      "instance": "/users/123",
      "currentETag": "def456"
    }

悲觀鎖定的示例工作流程

  1. 客戶端請求鎖定:

    • 客戶端在執行更新之前請求對資源進行鎖定。
    http
    POST /users/123/lock HTTP/1.1
    Host: example.com
    Timeout: Second-60

    回應:

    http
    HTTP/1.1 200 OK
    Content-Type: application/json
    Lock-Token: abc123
    
    {
      "lockId": "abc123",
      "resource": "/users/123",
      "locked": true
    }
  2. 客戶端發送更新請求:

    • 客戶端發送更新請求,包括鎖定標識符。
    http
    PATCH /users/123 HTTP/1.1
    Host: example.com
    Content-Type: application/json-patch+json