在工作中,接口冪等該如何做?
前言
大家好,我是路由器沒有路。
隨著互聯(lián)網(wǎng)的發(fā)展,Web API 已成為現(xiàn)代應(yīng)用程序的重要組成部分,它允許不同的應(yīng)用程序之間進(jìn)行通信和數(shù)據(jù)交換。
那么今天就來講下關(guān)于 Web API 中接口冪等性的一些內(nèi)容,希望對(duì)大家有所幫助。
背景
在 Web API 中,接口冪等性是一種重要的概念,或者說是一種機(jī)制,它可以確保同一個(gè)請(qǐng)求多次執(zhí)行時(shí),不會(huì)對(duì)系統(tǒng)造成任何負(fù)面影響。
在 Web 應(yīng)用程序中,由于網(wǎng)絡(luò)延遲、請(qǐng)求重試等因素,可能會(huì)導(dǎo)致同一個(gè)請(qǐng)求被執(zhí)行多次。如果接口不具備冪等性,這樣的重復(fù)請(qǐng)求可能會(huì)導(dǎo)致系統(tǒng)狀態(tài)的不一致性,數(shù)據(jù)的重復(fù)提交等問題。
因此,接口冪等性已成為 Web API 設(shè)計(jì)中的重要考慮因素。
接口冪等的使用場(chǎng)景
我們?cè)谄匠5臉I(yè)務(wù)開發(fā)中會(huì)經(jīng)常接觸到接口需要實(shí)現(xiàn)冪等的場(chǎng)景,但冪等往往又是開發(fā)最容易忽略的一個(gè)點(diǎn)之一,由于冪等實(shí)現(xiàn)不當(dāng)或缺少冪等實(shí)現(xiàn)導(dǎo)致嚴(yán)重的系統(tǒng)故障,如:重復(fù)下單、重復(fù)發(fā)貨、庫存重復(fù)扣減等。
接口冪等性在許多場(chǎng)景下都是非常重要的。以下是一些常見的使用場(chǎng)景:
前端重復(fù)提交表單:用戶填寫完成表單提交,很多時(shí)候會(huì)因網(wǎng)絡(luò)波動(dòng)沒有及時(shí)對(duì)用戶做出提交成功響應(yīng),致使用戶認(rèn)為沒有成功提交,然后一直點(diǎn)提交按鈕,這時(shí)就會(huì)發(fā)生重復(fù)提交表單請(qǐng)求。
支付系統(tǒng):在支付系統(tǒng)中,重復(fù)的支付請(qǐng)求可能會(huì)導(dǎo)致用戶的賬戶被重復(fù)扣款。因此,支付接口必須具備冪等性,以確保同一個(gè)請(qǐng)求多次執(zhí)行時(shí),不會(huì)對(duì)用戶的賬戶造成任何影響。
訂單系統(tǒng):在訂單系統(tǒng)中,重復(fù)的訂單請(qǐng)求可能會(huì)導(dǎo)致系統(tǒng)中出現(xiàn)重復(fù)的訂單。因此,訂單接口必須具備冪等性,以確保同一個(gè)請(qǐng)求多次執(zhí)行時(shí),不會(huì)對(duì)系統(tǒng)中的訂單數(shù)據(jù)造成任何影響。
數(shù)據(jù)庫操作:在數(shù)據(jù)庫操作中,重復(fù)的插入、更新、刪除請(qǐng)求可能會(huì)導(dǎo)致數(shù)據(jù)的重復(fù)提交或丟失。因此,數(shù)據(jù)庫接口必須具備冪等性,以確保同一個(gè)請(qǐng)求多次執(zhí)行時(shí),不會(huì)對(duì)數(shù)據(jù)庫中的數(shù)據(jù)造成任何影響。
用戶惡意刷單:例如在實(shí)現(xiàn)用戶投票這種功能時(shí),如果用戶針對(duì)一個(gè)用戶進(jìn)行重復(fù)提交投票,這樣會(huì)導(dǎo)致接口接收到用戶重復(fù)提交的投票信息,這樣會(huì)使投票結(jié)果與事實(shí)嚴(yán)重不符。
接口超時(shí)重復(fù)提交: 很多時(shí)候 HTTP 客戶端工具(如 OpenFeign)都默認(rèn)開啟超時(shí)重試的機(jī)制,尤其是第三方調(diào)用接口時(shí)候,為了防止網(wǎng)絡(luò)波動(dòng)超時(shí)等造成的請(qǐng)求失敗,都會(huì)添加重試機(jī)制,導(dǎo)致一個(gè)請(qǐng)求提交多次。
消息進(jìn)行重復(fù)消費(fèi):當(dāng)使用 MQ 消息中間件時(shí)候,如果發(fā)生消息中間件出現(xiàn)錯(cuò)誤未及時(shí)提交消費(fèi)信息,導(dǎo)致發(fā)生重復(fù)消費(fèi)。
Restful API 接口的冪等性是怎么樣的
在 Restful API
中,接口冪等性是一種非常重要的概念。Restful API
的冪等性可以通過 HTTP
方法來實(shí)現(xiàn)。
根據(jù) HTTP
規(guī)范,GET 和 HEAD 方法是冪等的,因?yàn)樗鼈冎皇亲x取資源,不會(huì)對(duì)資源進(jìn)行修改。
而 POST、PUT 和 DELETE 方法則不是冪等的,因?yàn)樗鼈儠?huì)對(duì)資源進(jìn)行修改。
方法是否冪等描述GET?GET 方法用于獲取資源,其一般不會(huì)對(duì)系統(tǒng)資源進(jìn)行改變,所以是冪等的HEAD?Head 與 get 請(qǐng)求類似,返回的響應(yīng)中沒有具體內(nèi)容,用于獲取報(bào)頭,所以也是冪等的POST×POST 方法一般用于創(chuàng)建新的資源。其每次執(zhí)行都會(huì)新增數(shù)據(jù),所以不是冪等的PUT×PUT 方法一般用于修改資源,所以也是非冪等的DELETE×DELETE 方法一般用戶刪除資源,所以也是非冪等的
冪等的不足之處
雖然接口冪等性可以確保同一個(gè)請(qǐng)求多次執(zhí)行時(shí),不會(huì)對(duì)系統(tǒng)造成任何負(fù)面影響,但是它也存在一些不足之處。
以下是一些常見的問題:
增加了額外的控制冪等業(yè)務(wù)邏輯,復(fù)雜了業(yè)務(wù)邏輯處理
把本可以并行執(zhí)行的操作變成了串行執(zhí)行,降低了執(zhí)行效率
實(shí)現(xiàn)復(fù)雜:實(shí)現(xiàn)接口冪等性可能需要使用一些復(fù)雜的技術(shù),如分布式鎖、樂觀鎖等,這可能會(huì)增加系統(tǒng)的復(fù)雜度
性能影響:實(shí)現(xiàn)接口冪等性可能會(huì)對(duì)系統(tǒng)的性能產(chǎn)生影響,如分布式鎖可能會(huì)降低系統(tǒng)的吞吐量
業(yè)務(wù)復(fù)雜度:有些業(yè)務(wù)場(chǎng)景下,可能無法實(shí)現(xiàn)接口冪等性,如在某些業(yè)務(wù)場(chǎng)景下,重復(fù)的請(qǐng)求可能會(huì)對(duì)系統(tǒng)產(chǎn)生影響,這時(shí)就需要在業(yè)務(wù)層面上進(jìn)行處理
冪等實(shí)現(xiàn)的關(guān)鍵點(diǎn)
仔細(xì)分析冪等的定義,發(fā)現(xiàn)冪等在實(shí)現(xiàn)上關(guān)注的重點(diǎn)是辨別重復(fù)請(qǐng)求和重復(fù)請(qǐng)求對(duì)系統(tǒng)不會(huì)多次造成不良影響。
那如何判斷相同請(qǐng)求呢?
請(qǐng)求方生成唯一請(qǐng)求 Id,服務(wù)提供方通過請(qǐng)求 Id 辨別請(qǐng)求是否重復(fù),如果請(qǐng)求 Id 相同則判定為重復(fù)請(qǐng)求;
服務(wù)提供方根據(jù)請(qǐng)求參數(shù)經(jīng)過一系列的 Hash 算法生成對(duì)應(yīng)的 Hash 值,若 Hash 值相同則判定為重復(fù)請(qǐng)求;
實(shí)現(xiàn)接口冪等性需要注意以下幾個(gè)關(guān)鍵點(diǎn):
確定請(qǐng)求的唯一性:需要確定每個(gè)請(qǐng)求的唯一性,可以使用請(qǐng)求頭、請(qǐng)求參數(shù)等方式來標(biāo)識(shí)每個(gè)請(qǐng)求的唯一性。
避免重復(fù)執(zhí)行:需要避免同一個(gè)請(qǐng)求多次執(zhí)行的情況發(fā)生,可以使用分布式鎖、樂觀鎖等方式來實(shí)現(xiàn)。
處理異常情況:需要處理異常情況,如網(wǎng)絡(luò)異常、系統(tǒng)故障等情況,可以使用重試機(jī)制來處理異常情況。
記錄請(qǐng)求日志:需要記錄每個(gè)請(qǐng)求的日志,以便于進(jìn)行排查和追蹤。
冪等實(shí)現(xiàn)的方式
實(shí)現(xiàn)接口冪等性的方式有很多,以下是一些常見的方式。
去重表
利用數(shù)據(jù)庫的特性來實(shí)現(xiàn)冪等。通常是在表上構(gòu)建一個(gè)唯一索引,那么只要某一個(gè)數(shù)據(jù)構(gòu)建完畢,后面再次操作也無法成功寫入。
常見的業(yè)務(wù)就是博客系統(tǒng)點(diǎn)贊功能,一個(gè)用戶對(duì)一個(gè)博文點(diǎn)贊后,就把用戶 id 與 博文 id 綁定,后續(xù)該用戶點(diǎn)贊同一個(gè)博文就無法插入了。
或是在金融系統(tǒng)中,給用戶創(chuàng)建金融賬戶,一個(gè)用戶肯定不能有多個(gè)賬戶,就在賬戶表中增加唯一索引來存儲(chǔ)用戶 id,這樣即使重復(fù)操作用戶也只能擁有一個(gè)賬戶。
狀態(tài)標(biāo)識(shí)
狀態(tài)標(biāo)識(shí)是很常見的冪等設(shè)計(jì)方式,主要思路就是通過狀態(tài)標(biāo)識(shí)的變更,保證業(yè)務(wù)中每個(gè)流程只會(huì)在對(duì)應(yīng)的狀態(tài)下執(zhí)行,如果標(biāo)識(shí)已經(jīng)進(jìn)入下一個(gè)狀態(tài),這時(shí)候來了上一個(gè)狀態(tài)的操作就不允許變更狀態(tài),保證了業(yè)務(wù)的冪等性。
狀態(tài)標(biāo)識(shí)經(jīng)常用在業(yè)務(wù)流程較長,修改數(shù)據(jù)較多的場(chǎng)景里。最經(jīng)典的例子就是訂單系統(tǒng),假如一個(gè)訂單要經(jīng)歷 創(chuàng)建訂單 -> 訂單支付/取消-> 發(fā)貨-> 確認(rèn)收貨->關(guān)閉訂單 這幾個(gè)步驟。
那么就有可能一筆待支付的訂單去支付,需要去賬戶里扣除對(duì)應(yīng)的余額,消耗對(duì)應(yīng)的優(yōu)惠卷,但是由于支付完成后網(wǎng)絡(luò)等原因返回了錯(cuò)誤信息,這時(shí)候就會(huì)重試再次去進(jìn)行賬戶余額扣減步驟造成數(shù)據(jù)錯(cuò)誤。
所以為了保證整個(gè)訂單流程的冪等性,可以在訂單信息中增加一個(gè)狀態(tài)標(biāo)識(shí),一旦完成了一個(gè)步驟就修改對(duì)應(yīng)的狀態(tài)標(biāo)識(shí)。
比如訂單支付成功后,就把訂單標(biāo)識(shí)為修改為支付完成待發(fā)貨,現(xiàn)在再次調(diào)用訂單支付或者取消接口,會(huì)先判斷訂單狀態(tài)標(biāo)識(shí),如果是已經(jīng)支付過或者取消訂單,就不會(huì)再次支付了。
Token 機(jī)制(冪等標(biāo)識(shí))
Token 機(jī)制應(yīng)該是適用范圍最廣泛的一種冪等設(shè)計(jì)方案了,具體實(shí)現(xiàn)方式也很多樣化。但是核心思想就是每次操作都生成一個(gè)唯一 Token 憑證,服務(wù)器通過這個(gè)唯一憑證保證同樣的操作不會(huì)被執(zhí)行兩次。
這個(gè) Token 除了字面形式上的唯一字符串,也可以是多個(gè)標(biāo)志的組合(比如上面提到的狀態(tài)標(biāo)志),甚至可以是時(shí)間段標(biāo)識(shí)等等。
換句話說,冪等標(biāo)識(shí)可以在請(qǐng)求頭或響應(yīng)頭中添加一個(gè)唯一的標(biāo)識(shí)符,來標(biāo)識(shí)每個(gè)請(qǐng)求的唯一性。
舉個(gè)例子,比如下單,這是一個(gè)典型的 Post 新增操作,要怎樣防止用戶多次點(diǎn)擊提交導(dǎo)致產(chǎn)生多個(gè)同樣的訂單呢??梢宰層脩籼峤坏臅r(shí)候帶一個(gè)唯一 Token,服務(wù)器只要判斷該 Token 存在了就不允許提交(提示用戶已重復(fù)下單),便能保證冪等性。
上面這個(gè)例子比較容易理解,但是業(yè)務(wù)比較簡(jiǎn)單。由于 Token 機(jī)制適用較廣,所以其設(shè)計(jì)中要注意的要求也會(huì)根據(jù)業(yè)務(wù)不同而不同。
那么 Token 在何時(shí)生成,怎么生成?
這是該機(jī)制的核心,就拿下單服務(wù)來說,如果在用戶提交訂單的時(shí)候才生成 Token,那用戶每次點(diǎn)提交都會(huì)生成新的 Token 然后都能提交成功,就不是冪等的了。必須在用戶提交內(nèi)容之前,比如進(jìn)入 checkout 頁的時(shí)候生成 Token,用戶在提交的時(shí)候內(nèi)容帶著 Token 一起提交,對(duì)于同一個(gè)頁面無論用戶提交多少次,就至多能成功一次。
所以 Token 生成的時(shí)機(jī)必須保證能夠使該操作具多次執(zhí)行都是相同的效果才行。使用 Token 機(jī)制就要求開發(fā)者對(duì)業(yè)務(wù)流程有較好的理解。
案例說明
下面以“數(shù)據(jù)庫樂觀鎖 + 冪等性 + Go 偽代碼”的案例來說明。
在數(shù)據(jù)庫中,樂觀鎖是通過在數(shù)據(jù)表中增加一個(gè)版本號(hào)(或者時(shí)間戳)字段來實(shí)現(xiàn)的。
在更新數(shù)據(jù)時(shí),先查詢當(dāng)前數(shù)據(jù)的版本號(hào),然后將要更新的數(shù)據(jù)的版本號(hào)設(shè)置為當(dāng)前版本號(hào)+1,然后執(zhí)行更新操作。如果更新成功,則說明當(dāng)前數(shù)據(jù)沒有被其他線程修改,否則說明當(dāng)前數(shù)據(jù)已經(jīng)被其他線程修改過,更新失敗。
樂觀鎖的實(shí)現(xiàn)可以很好地解決并發(fā)更新數(shù)據(jù)時(shí)的沖突問題,但是在某些情況下,可能會(huì)出現(xiàn)冪等性問題。例如,在某個(gè)接口中,多個(gè)請(qǐng)求同時(shí)對(duì)同一條數(shù)據(jù)進(jìn)行更新,如果使用樂觀鎖來實(shí)現(xiàn)冪等,那么可能會(huì)導(dǎo)致多次更新操作,最終數(shù)據(jù)的結(jié)果可能并不是我們期望的結(jié)果。
為了解決這個(gè)問題,我們可以在接口層面增加一個(gè)冪等性校驗(yàn),通過校驗(yàn)請(qǐng)求的唯一標(biāo)識(shí)符(如請(qǐng)求 ID)來判斷當(dāng)前請(qǐng)求是否已經(jīng)處理過。如果當(dāng)前請(qǐng)求已經(jīng)處理過,則直接返回結(jié)果,否則執(zhí)行更新操作,并將請(qǐng)求的唯一標(biāo)識(shí)符記錄到數(shù)據(jù)庫中,以便下次校驗(yàn)。這樣就可以保證同一個(gè)請(qǐng)求只會(huì)被處理一次,從而實(shí)現(xiàn)冪等性。
下面是一個(gè)用 Golang 偽代碼實(shí)現(xiàn)樂觀鎖和冪等性的示例:
go復(fù)制代碼//?定義數(shù)據(jù)結(jié)構(gòu) type?User?struct?{ ????ID??????int ????Name????string ????Version?int } ?//?更新用戶信息 func?updateUser(db?*sql.DB,?user?*User,?requestId?string)?error?{ ????//?查詢當(dāng)前版本號(hào) ????var?currentVersion?int ????err?:=?db.QueryRow("SELECT?version?FROM?user?WHERE?id?=??",?user.ID).Scan(¤tVersion) ????if?err?!=?nil?{ ????????return?err ????} ?????//?設(shè)置新版本號(hào) ????user.Version?=?currentVersion?+?1 ?????//?執(zhí)行更新操作 ????result,?err?:=?db.Exec("UPDATE?user?SET?name?=??,?version?=???WHERE?id?=???AND?version?=??",?user.Name,?user.Version,?user.ID,?currentVersion) ????if?err?!=?nil?{ ????????return?err ????} ?????//?判斷更新是否成功 ????rowsAffected,?err?:=?result.RowsAffected() ????if?err?!=?nil?{ ????????return?err ????} ????if?rowsAffected?==?0?{ ????????return?errors.New("update?failed") ????} ?????//?記錄請(qǐng)求ID,用于冪等性校驗(yàn) ????_,?err?=?db.Exec("INSERT?INTO?request_log(request_id)?VALUES(?)",?requestId) ????if?err?!=?nil?{ ????????return?err ????} ?????return?nil } ?//?冪等性校驗(yàn) func?checkRequestId(db?*sql.DB,?requestId?string)?bool?{ ????var?count?int ????err?:=?db.QueryRow("SELECT?COUNT(*)?FROM?request_log?WHERE?request_id?=??",?requestId).Scan(&count) ????if?err?!=?nil?{ ????????return?false ????} ????return?count?>?0 } ?//?處理請(qǐng)求 func?handleRequest(db?*sql.DB,?user?*User,?requestId?string)?error?{ ????//?冪等性校驗(yàn) ????if?checkRequestId(db,?requestId)?{ ????????return?nil ????} ?????//?更新用戶信息 ????err?:=?updateUser(db,?user,?requestId) ????if?err?!=?nil?{ ????????return?err ????} ?????return?nil }
在這個(gè)示例中,我們定義了一個(gè) User
結(jié)構(gòu)體,其中包含了用戶的 ID、姓名和版本號(hào)。
在更新用戶信息時(shí),我們首先查詢當(dāng)前版本號(hào),然后將要更新的數(shù)據(jù)的版本號(hào)設(shè)置為當(dāng)前版本號(hào)+1,然后執(zhí)行更新操作。如果更新成功,則說明當(dāng)前數(shù)據(jù)沒有被其他線程修改,否則說明當(dāng)前數(shù)據(jù)已經(jīng)被其他線程修改過,更新失敗。
為了保證冪等性,我們?cè)?handleRequest()
函數(shù)中增加了一個(gè)冪等性校驗(yàn),通過查詢請(qǐng)求日志表來判斷當(dāng)前請(qǐng)求是否已經(jīng)處理過。
如果當(dāng)前請(qǐng)求已經(jīng)處理過,則直接返回結(jié)果,否則執(zhí)行更新操作,并將請(qǐng)求的唯一標(biāo)識(shí)符記錄到數(shù)據(jù)庫中,以便下次校驗(yàn)。這樣就可以保證同一個(gè)請(qǐng)求只會(huì)被處理一次,從而實(shí)現(xiàn)冪等性。
方案比較
方案優(yōu)點(diǎn)缺點(diǎn)去重表實(shí)現(xiàn)簡(jiǎn)單,易于理解和維護(hù);可以避免重復(fù)提交和重復(fù)處理的問題1)需要占用額外的存儲(chǔ)空間;2) 只能用于插入和刪除操作;3)只能存在于唯一鍵場(chǎng)景狀態(tài)標(biāo)識(shí)實(shí)現(xiàn)簡(jiǎn)單,查詢效率高1)只適用于更新操作;2)表中需要增加額外的狀態(tài)標(biāo)識(shí)Token 機(jī)制實(shí)現(xiàn)相對(duì)復(fù)雜,但安全性高,可以避免重放攻擊和惡意請(qǐng)求1)需要生成唯一 Token;2)獲取 Token 可能需要與服務(wù)提供方交互;3)需要借助第三方存儲(chǔ)(比如 Redis)
綜上所述,選擇哪種冪等實(shí)現(xiàn)方案取決于具體的業(yè)務(wù)需求和實(shí)現(xiàn)環(huán)境。
如果對(duì)存儲(chǔ)空間和查詢效率要求較高,可以選擇狀態(tài)標(biāo)識(shí);如果對(duì)安全性要求較高,可以選擇 Token 機(jī)制;如果對(duì)實(shí)現(xiàn)簡(jiǎn)單和易于維護(hù)要求較高,可以選擇去重表。
總結(jié)
總之,接口冪等性是 Web API 設(shè)計(jì)中的重要考慮因素,可以確保同一個(gè)請(qǐng)求多次執(zhí)行時(shí),不會(huì)對(duì)系統(tǒng)造成任何負(fù)面影響。
實(shí)現(xiàn)接口冪等性需要注意請(qǐng)求的唯一性、重復(fù)執(zhí)行的處理、異常情況的處理以及請(qǐng)求日志的記錄等關(guān)鍵點(diǎn)。
通??梢允褂萌ブ乇怼顟B(tài)標(biāo)識(shí)、token 機(jī)制等方式來實(shí)現(xiàn)接口冪等性。