如何保證介面的冪等性?

小牛呼嚕嚕發表於2023-10-16

今天我們來聊聊關於介面的冪等性問題。

什麼是冪等性

所謂冪等,就是任意多次執行所產生的影響均與一次執行的影響相同。

在 restful 規範中,常見的請求方式和介面冪等性關係如下:

請求方式 操作 是否冪等
GET 查詢資料
POST 新增資料
PUT 更新資料 直接更新為某個值,滿足冪等,如:set a = 1;累加操作的更新,不滿足,如:set a = a+1
DELETE 刪除資料 根據唯一條件刪除,滿足冪等;否則,不滿足,冪等,比如:根據某一條件刪除一批資料後,又新增了一條滿足該條件的資料,又執行了一次刪除,那麼就會刪除掉新增的這條資料

為什麼會產生介面冪等性問題

在計算機應用中,可能遇到網路抖動,臨時故障,或者服務呼叫失敗,尤其是分散式系統中,介面呼叫失敗更為常見。為了保證服務的完整性,我們可能會發起介面的重試呼叫,如果介面不處理冪等,可能對系統造成很大的影響,因此介面的冪等設計尤其更為重要。

對於業務中需要考慮冪等性的地方一般都是介面的重複請求,重複請求是指同一個請求因為某些原因被多次提交。導致這種情況的發生有以下幾種常見的場景:

  1. 前端重複提交:使用者在提交表單的時候,可能會因網路波動沒有及時做出提交成功響應,致使使用者認為沒有成功提交,然後一直點提交按鈕,這時就會發生重複提交表單請求。

  2. 介面超時重試:第三方呼叫介面時候,為了超時等異常情況造成的請求失敗,都會新增重試機制,導致一個請求提交多次。

  3. 訊息重複消費:當使用 MQ 訊息中介軟體時候,如果發生訊息中介軟體出現錯誤未及時提交消費資訊,導致發生重複消費。

冪等性解決方案

那我們應該能怎樣保證介面的冪等性呢?

可以思考一下,第一種場景下,既然是使用者重複提交導致的,那我們可以想辦法讓使用者沒辦法重複提交。

方案一:前端控制

在前端做攔截,比如按鈕點選一次之後就置灰或者隱藏。但是往往前端並不可靠,還是得後端處理才更放心。

方案二:Token機制

使用者進入表單頁面首先呼叫後臺介面獲取 token 並存入 redis,當使用者提交表單時將 token 也作為入參,後端先刪除 redis 中的 token,刪除成功則儲存表單資料,失敗則提示使用者重複提交。

img

這裡為什麼不先判斷 redis 是否存在這個 token 再刪除,是因為要保證操作的原子性,極端情況下,第一個請求查詢到 redis 中存在這個 token,還沒來得及刪除,第二個請求進來,也查詢到 redis 中存在這個 token,那麼還是會造成重複提交的問題。

token 機制需要先請求獲取 token 的介面,在有些情況下很明顯並不合適。我們大部分請求都是要落到資料庫的,所以我們可以從資料庫著手。

方案三、唯一索引

這種方案就比較好理解了,使用唯一索引可以避免髒資料的新增,當插入重複資料時資料庫會拋異常,保證了資料的唯一性。唯一索引可以支援插入、更新、刪除業務操作。

方案四、悲觀鎖

這裡所說的悲觀鎖是基於資料庫層面的,在獲取資料時進行加鎖,當同時有多個重複請求時,其他請求都無法進行操作。悲觀鎖只適用於更新操作。

// 例如
select name from t_goods where id=1 for update;

注意:id 欄位一定要是主鍵或者唯一索引,不然會鎖住整張表,這是會死人的。悲觀鎖使用時一般伴隨事務一起使用,資料鎖定時間可能會很長,根據實際情況選用。

在請求量比較大的情況下,使用悲觀鎖明顯不合適,這時候就到樂觀鎖上場了。

方案五、樂觀鎖

可以通過版本號實現,為表增加一個 version 欄位,當資料需要更新時,先去資料庫裡獲取此時的version版本號。

select version from t_goods where id=1

更新資料時首先要對比版本號,如果不相等說明已經有其他的請求去更新資料了,提示更新失敗。

update t_goods set count=count+1,version=version+1 where version=#{version}

還有一種是通過狀態機實現的,其實也是樂觀鎖的原理。這種方法適合在有狀態流轉的情況下,比如訂單的建立和付款,訂單的建立肯定是在付款之前,這時我們可以通過在設計狀態欄位時,使用 int 型別,並且通過值型別的大小來實現冪等性。

update t_goods set status=#{status} where id=1 and status<#{status}

同樣,樂觀鎖也只適用於更新操作。

方案六、分散式鎖

有時候我們的業務不僅僅是運算元據庫,也可能是傳送簡訊、訊息等等,那資料庫層面的鎖就不適合了。這種情況下就要考慮程式碼層面的鎖了,而 java 的自帶的鎖在分散式叢集部署的場景下並不適用,那麼就可以採用分散式鎖來實現(Redis 或 Zookeeper)。

拿 Redis 分散式鎖舉例,比如一個訂單發起支付請求,支付系統會去 Redis 快取中查詢是否存在該訂單號的 Key,如果不存在,則以 Key 為訂單號向 Redis 插入。查詢訂單是否已經支付,如果沒有則進行支付,支付完成後刪除該訂單號的Key。通過 Redis 做到了分散式鎖,只有這次訂單支付請求完成,下次請求才能進來。當然這裡需要設定一個Key 的過期時間,在發生異常的時候還要注意刪除 Redis 的 Key。

總結

介面的冪等性是一個很常見的問題,需要根據具體業務場景的不同,選擇合適的解決方案。

END

往期推薦

你必須瞭解的分散式事務解決方案

就這?分散式 ID 發號器實戰

略懂設計模式之工廠模式

就這?Spring 事務失效場景及解決方案

就這?一篇文章讓你讀懂 Spring 事務

相關文章