今天我們來聊聊關於介面的冪等性問題。
什麼是冪等性
所謂冪等,就是任意多次執行所產生的影響均與一次執行的影響相同。
在 restful 規範中,常見的請求方式和介面冪等性關係如下:
請求方式 | 操作 | 是否冪等 |
---|---|---|
GET | 查詢資料 | 是 |
POST | 新增資料 | 否 |
PUT | 更新資料 | 直接更新為某個值,滿足冪等,如:set a = 1;累加操作的更新,不滿足,如:set a = a+1 |
DELETE | 刪除資料 | 根據唯一條件刪除,滿足冪等;否則,不滿足,冪等,比如:根據某一條件刪除一批資料後,又新增了一條滿足該條件的資料,又執行了一次刪除,那麼就會刪除掉新增的這條資料 |
為什麼會產生介面冪等性問題
在計算機應用中,可能遇到網路抖動,臨時故障,或者服務呼叫失敗,尤其是分散式系統中,介面呼叫失敗更為常見。為了保證服務的完整性,我們可能會發起介面的重試呼叫,如果介面不處理冪等,可能對系統造成很大的影響,因此介面的冪等設計尤其更為重要。
對於業務中需要考慮冪等性的地方一般都是介面的重複請求,重複請求是指同一個請求因為某些原因被多次提交。導致這種情況的發生有以下幾種常見的場景:
-
前端重複提交:使用者在提交表單的時候,可能會因網路波動沒有及時做出提交成功響應,致使使用者認為沒有成功提交,然後一直點提交按鈕,這時就會發生重複提交表單請求。
-
介面超時重試:第三方呼叫介面時候,為了超時等異常情況造成的請求失敗,都會新增重試機制,導致一個請求提交多次。
-
訊息重複消費:當使用 MQ 訊息中介軟體時候,如果發生訊息中介軟體出現錯誤未及時提交消費資訊,導致發生重複消費。
冪等性解決方案
那我們應該能怎樣保證介面的冪等性呢?
可以思考一下,第一種場景下,既然是使用者重複提交導致的,那我們可以想辦法讓使用者沒辦法重複提交。
方案一:前端控制
在前端做攔截,比如按鈕點選一次之後就置灰或者隱藏。但是往往前端並不可靠,還是得後端處理才更放心。
方案二:Token機制
使用者進入表單頁面首先呼叫後臺介面獲取 token 並存入 redis,當使用者提交表單時將 token 也作為入參,後端先刪除 redis 中的 token,刪除成功則儲存表單資料,失敗則提示使用者重複提交。
這裡為什麼不先判斷 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
往期推薦