Go 實現接口冪等方案設計

    冪等(idempotent)這個詞來源於數學運算,其中的含義是做一次和做多次的效果是一樣的,即該操作可以重複進行,卻不會改變系統的狀態。

在計算機科學中,它們的含義一樣:某個接口或函數,無論調用一次還是調用多次,產生的副作用都是相同的。例如,HTTP 的 GET、PUT、DELETE、HEAD 和 OPTIONS 這些方法都是冪等的,當你向這個 URL 發出一個請求,無論做一次或者做多次,結果都是一樣的。

這個概念在分佈式事務,微服務架構中特別重要,例如在處理網絡請求時,由於網絡不穩定,可能會導致請求重複,如果接口設計爲冪等的,那後端接收到重複的請求後,處理的結果依然保持一致。

Go 語言實現接口冪等性的幾種常用方案如下:

方案一:業務邏輯的唯一性

例如下單,每次操作都會生成一份唯一的訂單號,並存儲發起的操作。如果有重複發起請求,則查找已經發起過的請求。如果未找到說明指定操作尚未發起,則進入正常的業務處理,否則直接輸出(或讀取返回)返回結果。

var (
  orderMap = make(map[string]*Order)
)
type Order struct {
  OrderID string
  Amount  float64
  Status  string
}
func CreateOrder(orderID string, amount float64) (*Order, error) {
  // 檢查是否已存在相同的訂單號,有則直接返回
  if order, exists := orderMap[orderID]; exists {
    return order, nil
  }
  // 不存在,則創建新的訂單
  order := &Order{
    OrderID: orderID,
    Amount:  amount,
    Status:  "created",
  }
  // 將新訂單存儲到map中
  orderMap[orderID] = order
  return order, nil
}

每次創建訂單前,先到內存的 map 中查找是否已經存在相同訂單號的訂單,如果存在,說明之前已經創建過,直接返回數據庫中的結果即可。否則,就正常處理訂單創建邏輯。

這樣處理的效果就是,無論這個函數被調用一次還是多次,只要傳入的訂單號相同,得到的結果(系統中的訂單狀態)總是一樣的。這就實現了接口的冪等性。

注意:該方法的缺點在於,如果服務器重啓,之前的內存數據會丟失。因此在 實際的生產環境中,需要把訂單的信息持久化存儲,比如保存在數據庫中。

方案二:Token 機制

    Token 機制:每次請求前,先向服務器申請一個 Token,並以此 Token 作爲後續操作的唯一憑證。執行操作時,服務器將驗證這個 Token,如果此 Token 不在服務器的緩存中,則進入正常的業務處理。否則說明這個請求已經操作過,此次操作將被認爲是重複操作。

import (
    "github.com/google/uuid"
)
var (
  tokenStore = make(map[string]bool)
)
func createToken() string {
    return uuid.New().String()
}
func Handler(w http.ResponseWriter, r *http.Request){
    token := r.Header.Get("Token")
    if _, exists := tokenStore[token]; exists {
        //如果Token在存儲中,則說明請求已經完成,直接返回
        w.Write([]byte("This request has been processed"))
        return
    }
    //否則,處理請求,認爲是有效的新請求
    //操作完成後,將Token添加到存儲中
    tokenStore[token] = true
    //其他操作...
    w.Write([]byte("Processing request"))
}

    在客戶端每次請求時,都需要先調用一個函數獲取 Token,然後再使用這個 Token 進行請求。

    這個方法的缺點在於 Token 只能用一次,對於相同的一種操作,每次都要先向服務器獲取 Token。並且如果服務器重啓,未處理完的 Token 會失效。在實際應用中,還需要加入 Token 超時清理機制,避免 Token 無限增加導致內存耗盡。

方案三:使用分佈式鎖

func processWithLock(id string, rdb *redis.Client) string {
    // 爲了防止死鎖,設置一個過期時間,這裏假設爲1分鐘
    ok, err := rdb.SetNX(context.Background(), id, id, 60*time.Second).Result()
    if err != nil || !ok {
        return ""
    }
    // 進行任務處理
    result := "result"
    // 任務完成後,需釋放鎖
    _, err = rdb.Del(context.Background(), id).Result()
    if err != nil {
        return ""
    }
    return result
}

   這段代碼中,SetNX 方法會給id上鎖,如果鎖已經被別的線程拿走,它會返回 false,這時候就會進行等待;如果能夠成功上鎖,則進行任務處理,最後再釋放鎖,保證了任務處理的原子性。

通過這種方式,無論請求是否重複,由於分佈式鎖的保護,系統都會按照預期的流程順序執行,從而實現了接口的冪等性。

儲存包含 id 和鎖信息,當請求進來時,先判斷該請求的 id 是否已經存在,如果不存在,設置鎖並處理請求;如果已存在,直接拒絕或者等待鎖釋放。這樣無論有多少相同 id 的請求同時到來,系統都只會處理一次,達到接口冪等性的目。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/o5zRh_G8g42KmdNNAknryw