如何保證冪等性


冪等性最早是數學里面的一個概念,后來被用於計算機領域,用於表示任意多次請求執行的結果均與一次請求執行的結果相同,對於一個接口而言,即無論調用多少次,最終得到的結果都是一樣的,用數學語言表達就是f(x)=f(f(x))。

如何保證冪等性?

(1) 前端攔截

(2) 使用數據庫實現冪等性

(3) 使用 JVM 鎖實現冪等性

(4) 使用分布式鎖實現冪等性

(1) 前端攔截

前端攔截是指通過 Web 站點的頁面進行請求攔截,比如在用戶點擊完“提交”按鈕后,我們可以把按鈕設置為不可用或者隱藏狀態,避免用戶重復點擊。或者添加提交按鈕的事件處理,可以設置一個緩沖時間,例如2秒內的重復提交,只執行最后一次的請求。但前端攔截有一個致命的問題,如果是懂行的程序員或者黑客可以直接繞過頁面的 JS 執行,直接模擬請求后端的接口,這樣的話,我們前端的這些攔截就不能生效了。因此除了前端攔截一部分正常的誤操作之外,后端的驗證必不可少。

(2) 數據庫實現

① 通過悲觀鎖來實現冪等性

② 通過唯一索引來實現冪等性

③ 通過樂觀鎖來實現冪等性

① 通過悲觀鎖來實現冪等性

使用悲觀鎖實現冪等性,一般是配合事務一起來實現,在沒有使用悲觀鎖時,我們通常的執行過程是這樣的,首先來判斷數據的狀態,執行 SQL 如下:

select status from table_name where id='xxx';

然后再進行添加操作:

insert into table_name (id) values ('xxx');

最后再進行狀態的修改:

update table_name set status='xxx';

但這種情況因為是非原子操作,所以在高並發環境下可能會造成一個業務被執行兩次的問題,當一個程序在執行中時,而另一個程序也開始狀態判斷的操作。因為第一個程序還未來得及更改狀態,所以第二個程序也能執行成功,這就導致一個業務被執行了兩次。

在這種情況下我們就可以使用悲觀鎖來避免問題的產生,實現 SQL 如下所示:

begin;  # 1.開始事務
select * from table_name where id='xxx' for update; # 2.查詢狀態
insert into table_name (id) values ('xxx'); # 3.添加操作
update table_name set status='xxx'; # 4.更改操作
commit; # 5.提交事務

在實現的過程中需要注意以下兩個問題:

如果使用的是 MySQL 數據庫,必須選用 innodb 存儲引擎,因為 innodb 支持事務;

id 字段一定要是主鍵或者是唯一索引,不然會鎖表,影響其他業務執行。

② 通過唯一索引來實現冪等性

我們可以創建一個唯一索引的表來實現冪等性,在每次執行業務之前,先執行插入操作,因為唯一字段就是業務的 ID,因此如果重復插入的話會觸發唯一約束而導致插入失敗。在這種情況下(插入失敗)我們就可以判定它為重復提交的請求。

唯一索引表的創建示例如下:

CREATE TABLE `table_name` (
  `id` int NOT NULL AUTO_INCREMENT,
  `orderid` varchar(32) NOT NULL DEFAULT '' COMMENT '唯一id',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uq_orderid` (`orderid`) COMMENT '唯一約束'
) ENGINE=InnoDB;

③ 通過樂觀鎖來實現冪等性 

樂觀鎖是指在執行數據操作時(更改或添加)進行加鎖操作,其他時間不加鎖,因此相比於整個執行過程都加鎖的悲觀鎖來說,它的執行效率要高很多。

樂觀鎖可以通過版本號來實現,例如以下 SQL:

update table_name set version = version + 1 where version = 0;

(3) 使用 JVM 鎖實現冪等性

JVM 鎖實現是指通過 JVM 提供的內置鎖如 Lock 或者是 synchronized 來實現冪等性。使用 JVM 鎖來實現冪等性的一般流程為:首先通過 Lock 對代碼段進行加鎖操作,然后再判斷此訂單是否已經被處理過,如果未處理則開啟事務執行訂單處理,處理完成之后提交事務並釋放鎖。JVM 鎖存在的最大問題在於,它只能應用於單機環境,因為 Lock 本身為單機鎖,所以它就不適應於分布式多機環境。

(4) 使用分布式鎖實現冪等性

分布式鎖實現冪等性的邏輯是,在每次執行方法之前先判斷是否可以獲取到分布式鎖,如果可以,則表示為第一次執行方法,否則直接舍棄請求即可。

需要注意的是分布式鎖的 key 必須為業務的唯一標識,我們通常使用 Redis 或者 ZooKeeper 來實現分布式鎖;如果使用 Redis 的話,則用 set 命令來創建和獲取分布式鎖,執行示例如下:

127.0.0.1:6379> set lock true ex 30 nx
OK # 創建鎖成功

其中,ex 是用來設置超時時間的;而 nx 是 not exists 的意思,用來判斷鍵是否存在。如果返回的結果為“OK”,則表示創建鎖成功,否則表示重復請求,應該舍棄。

冪等性注意事項

冪等性的實現與判斷需要消耗一定的資源,因此不應該給每個接口都增加冪等性判斷,要根據實際的業務情況和操作類型來進行區分。例如,我們在進行查詢操作和刪除操作時就無須進行冪等性判斷。查詢操作查一次和查多次的結果都是一致的,因此我們無須進行冪等性判斷。刪除操作也是一樣,刪除一次和刪除多次都是把相關的數據進行刪除(這里的刪除指的是條件刪除而不是刪除所有數據),因此也無須進行冪等性判斷。

冪等性的關鍵步驟

實現冪等性的關鍵步驟分為以下三個:

(1)每個請求操作必須有唯一的 ID,而這個 ID 就是用來表示此業務是否被執行過的關鍵憑證,例如,訂單支付業務的請求,就要使用訂單的 ID 作為冪等性驗證的 Key;

(2)每次執行業務之前必須要先判斷此業務是否已經被處理過;

(3)第一次業務處理完成之后,要把此業務處理的狀態進行保存,比如存儲到 Redis 中或者是數據庫中,這樣才能防止業務被重復處理。

項目實戰

WKD項目中,有些三方系統流程(eg:采購出入庫)會通過MQ發送到我們中台庫存中心進行異步消費,為了防止MQ傳過來的訂單數據在我們消費端重復處理,需要做冪等性,解決方法就是對傳送消息體dto里中的全局唯一單號”orderNo”字段做唯一性校驗,先從緩存中校驗單號是否存在,不存在的話說明沒有消費過,我們處理完業務邏輯后,會把單號放入redis緩存中,如果單號重復的話就拋異常”單號重復”。

補充:方法2,還有就是我們有一張庫存變動流水表,記錄了庫存變更成功的詳細信息,我們可以拿全局唯一性的單號字段”orderNo”字段去數據庫里進行查詢,如果已經在日志表中,那么就不再處理這條消息。

Exchange中心

常見面試題

(1) 什么是冪等性?如何保證接口的冪等性?

(2) 冪等性需要注意什么問題?

(3) 實現冪等性的關鍵步驟有哪些?

(4) 說一說數據庫實現冪等性的執行細節?

(5) 項目用到冪等性得場景?怎么處理得?

(6) mq重復消費怎么處理?

參考/好文

拉鈎教育 -- 如何保證接口的冪等性?常見的實現方案有哪些?

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM