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