如何保證介面的冪等性?

  • 2021 年 8 月 22 日
  • 筆記

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

什麼是冪等性

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

在 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 事務