天天看點

接口的幂等性如何保證接口的幂等性?

如何保證接口的幂等性?

今天我們來聊聊關于接口的幂等性問題。

什麼是幂等性

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

在 restful 規範中,常見的請求方式和接口幂等性關系如下:

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

為什麼會産生接口幂等性問題

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

對于業務中需要考慮幂等性的地方一般都是接口的重複請求,重複請求是指同一個請求因為某些原因被多次送出。導緻這種情況的發生有以下幾種常見的場景:

  1. 前端重複送出:使用者在送出表單的時候,可能會因網絡波動沒有及時做出送出成功響應,緻使使用者認為沒有成功送出,然後一直點送出按鈕,這時就會發生重複送出表單請求。
  2. 接口逾時重試:第三方調用接口時候,為了逾時等異常情況造成的請求失敗,都會添加重試機制,導緻一個請求送出多次。
  3. 消息重複消費:當使用 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

往期推薦

你必須了解的分布式事務解決方案

就這?分布式 ID 發号器實戰

略懂設計模式之工廠模式

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

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

本文來自部落格園,作者:靓仔聊程式設計,轉載請注明原文連結:https://www.cnblogs.com/liangzaiit/p/15171618.html

今天我們來聊聊關于接口的幂等性問題。

什麼是幂等性

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

在 restful 規範中,常見的請求方式和接口幂等性關系如下:

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

為什麼會産生接口幂等性問題

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

對于業務中需要考慮幂等性的地方一般都是接口的重複請求,重複請求是指同一個請求因為某些原因被多次送出。導緻這種情況的發生有以下幾種常見的場景:

  1. 前端重複送出:使用者在送出表單的時候,可能會因網絡波動沒有及時做出送出成功響應,緻使使用者認為沒有成功送出,然後一直點送出按鈕,這時就會發生重複送出表單請求。
  2. 接口逾時重試:第三方調用接口時候,為了逾時等異常情況造成的請求失敗,都會添加重試機制,導緻一個請求送出多次。
  3. 消息重複消費:當使用 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

往期推薦

你必須了解的分布式事務解決方案

就這?分布式 ID 發号器實戰

略懂設計模式之工廠模式

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

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