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