- 一、概述
- 1.1、什麼是冪等
- 1.2、為什麼需要冪等?
- 二、冪等如何設計
- 實現冪等的8 種方案
- select+insert+主鍵/唯一索引衝突
- 狀態機冪等
- 抽取防重表
- token令牌
- 悲觀鎖(如select for update)
- 樂觀鎖
- 分散式鎖
- 三、HTTP的冪等
一、概述
在分散式系統和微服務架構中,確保操作的安全性和可靠性至關重要。冪等性(Idempotency)是實現這一目標的重要概念。本文將探討冪等性的基本原理、應用場景以及設計冪等操作的方法。
在工作中,做冪等的場景蠻多的,恰逢阿里雲網盤事件爆發,能夠訪問到陌生人的圖片,那麼在冪等上肯定就存在著問題。
所以針對冪等性針對要來做一個總結說明。
1.1、什麼是冪等
冪等性是指一個操作無論執行多少次,其結果都是相同的。在數學上,函式 f(x)f(x)f(x) 滿足冪等性是指 f(f(x))=f(x)f(f(x)) = f(x)f(f(x))=f(x)。在電腦科學中,冪等操作的一個典型例子是HTTP GET請求,不管你傳送多少次GET請求,伺服器返回的資源狀態都是一致的。
1.2、為什麼需要冪等?
在某些提交表單的場景,如果使用者一直重複點選,可能會產生兩條一樣的資料,這種是前端重複提交的場景。還有就是轉賬場景,出現網路超時,有可能請求到,或者出現網路丟包的情況,這種情況導致重複請求的情況,這種情況如果重試,那麼就是需要做好冪等,否則的話就會出現轉賬多轉的情況。
二、冪等如何設計
唯一請求識別符號:使用唯一請求ID(如UUID)來標識每個操作。伺服器可以使用這個ID來跟蹤和記錄請求,確保每個請求只執行一次。
一般會生成一個全域性性的唯一 id,全域性唯一 id 有許多種生成方式,如以下方式
-
UUID:UUID的缺點比較明顯,它字串佔用的空間比較大,生成的ID過於隨機,可讀性差,而且沒有遞增。
-
分散式 id:經典分散式 id 就是使用雪花演算法來生成唯一 id。
分散式 id 生成演算法雪花演算法是一種生成分散式全域性唯一ID的演算法,生成的ID稱為Snowflake IDs。這種演算法由Twitter建立,並用於推文的ID。一個Snowflake ID有64位。
第1位:Java中long的最高位是符號位代表正負,正數是0,負數是1,一般生成ID都為正數,所以預設為0。
接下來前41位是時間戳,表示了自選定的時期以來的毫秒數。接下來的10位代表計算機ID,防止衝突。
其餘12位代表每臺機器上生成ID的序列號,這允許在同一毫秒內建立多個Snowflake ID。
當然,全域性唯一性的ID,還可以使用百度的Uidgenerator,或者美團的Leaf。
實現冪等的8 種方案
如果你呼叫下游介面超時了,是不是要考慮重試,如果重試的話,下游介面就需要支援冪等。
實現冪等一般有以下方案:
- select+insert+主鍵/唯一索引衝突
- 直接insert + 主鍵/唯一索引衝突
- 狀態機冪等
- 抽取防重表
- token令牌
- 悲觀鎖(如select for update,很少用)
- 樂觀鎖
- 分散式鎖
select+insert+主鍵/唯一索引衝突
日常開發中,為了實現交易介面冪等,我是這樣實現的:
交易請求過來,我會先根據請求的唯一流水號 reqNo
欄位+userId欄位,先select
一下資料庫的流水錶
-
如果資料已經存在,就攔截是重複請求,直接返回成功;
-
如果資料不存在,就執行
insert
插入,如果insert
成功,則直接返回成功,如果insert
產生主鍵衝突異常,則捕獲異常,開始來做一些補償處理。
狀態機冪等
很多業務表,都是有狀態的,比如轉賬流水錶,就會有0-待處理,1-處理中、2-成功、3-失敗狀態
。轉賬流水更新的時候,都會涉及流水狀態更新,即涉及狀態機 (即狀態變更圖)。我們可以利用狀態機實現冪等,一起來看下它是怎麼實現的。
比如轉賬成功後,把處理中的轉賬流水更新為成功狀態,SQL這麼寫:
很多業務表,都是有狀態的,比如轉賬流水錶,就會有0-待處理,1-處理中、2-成功、3-失敗狀態
。轉賬流水更新的時候,都會涉及流水狀態更新,即涉及狀態機 (即狀態變更圖)。我們可以利用狀態機實現冪等,一起來看下它是怎麼實現的。
比如轉賬成功後,把處理中的轉賬流水更新為成功狀態,SQL這麼寫:
update transfr_flow set status=2 where biz_seq=‘666’ and status=1;
如下是虛擬碼
Rsp idempotentTransfer(Request req){
String bizSeq = req.getBizSeq();
int rows= "update transfr_flow set status=2 where biz_seq=#{bizSeq} and status=1;"
if(rows==1){
log.info(“更新成功,可以處理該請求”);
//其他業務邏輯處理
return rsp;
}else if(rows==0){
log.info(“更新不成功,不處理該請求”);
//不處理,直接返回
return rsp;
}
log.warn("資料異常")
return rsp:
}
Java
Copy
抽取防重表
很多時候,我們業務表唯一流水號希望後端系統生成,又或者我們希望防重功能與業務表分隔開來,這時候我們可以單獨搞個防重表。當然防重表也是利用主鍵/索引的唯一性,如果插入防重表衝突即直接返回成功,如果插入成功,即去處理請求。
token令牌
token 令牌方案一般包括兩個請求階段:
-
客戶端請求申請獲取token,服務端生成token返回
-
客戶端帶著token請求,服務端校驗token
- 客戶端發起請求,申請獲取token。
- 服務端生成全域性唯一的token,儲存到redis中(一般會設定一個過期時間),然後返回給客戶端。
- 客戶端帶著token,發起請求。
- 服務端去redis確認token是否存在,一般用
redis.del(token)
的方式,如果存在會刪除成功,即處理業務邏輯,如果刪除失敗不處理業務邏輯,直接返回結果。
悲觀鎖(如select for update)
什麼是悲觀鎖?
通俗點講就是很悲觀,每次去運算元據時,都覺得別人中途會修改,所以每次在拿資料的時候都會上鎖。官方點講就是,共享資源每次只給一個執行緒使用,其它執行緒阻塞,用完後再把資源轉讓給其它執行緒。
悲觀鎖如何控制冪等的呢?就是加鎖呀,一般配合事務來實現。
舉個更新訂單的業務場景:
假設先查出訂單,如果查到的是處理中狀態,就處理完業務,再然後更新訂單狀態為完成。如果查到訂單,並且不是處理中的狀態,則直接返回
整體的虛擬碼如下:
begin; # 1.開始事務
select * from order where order_id='666' # 查詢訂單,判斷狀態
if(status !=處理中){
//非處理中狀態,直接返回;
return ;
}
## 處理業務邏輯
update order set status='完成' where order_id='666' # 更新完成
commit; # 5.提交事務
-
這裡面order_id需要是索引或主鍵哈,要鎖住這條記錄就好,如果不是索引或者主鍵,會鎖表的!
-
悲觀鎖在同一事務操作過程中,鎖住了一行資料。別的請求過來只能等待,如果當前事務耗時比較長,就很影響介面效能。所以一般不建議用悲觀鎖做這個事情。
除了會導致事務過長之外,還會導致大量的鎖迴圈檢測,導致CPU飆升,一般來說不會利用資料庫來熱點資料更新。
可以採用的方案有:
- 如果使用的是阿里雲的資料庫,可以使用到hit機制;
- Redis+MQ,利用Redis來抗住壓力,然後將壓力減小,利用MQ來進行消費,然後來對其進行更新;
樂觀鎖
悲觀鎖有效能問題,可以試下樂觀鎖。
什麼是樂觀鎖?
樂觀鎖在運算元據時,則非常樂觀,認為別人不會同時在修改資料,因此樂觀鎖不會上鎖。只是在執行更新的時候判斷一下,在此期間別人是否修改了資料。
怎樣實現樂觀鎖呢?
就是給表的加多一列version版本號,每次更新記錄version都升級一下(version=version+1)。具體流程就是先查出當前的版本號version,然後去更新修改資料時,確認下是不是剛剛查出的版本號,如果是才執行更新
比如,我們更新前,先查下資料,查出的版本號是version =1
select order_id,version from order where order_id='666';
然後使用version =1
和訂單Id
一起作為條件,再去更新
update order set version = version +1,status='P' where order_id='666' and version =1
最後更新成功,才可以處理業務邏輯,如果更新失敗,預設為重複請求,直接返回。
流程圖如下:
為什麼版本號建議自增的呢?
因為樂觀鎖存在ABA的問題,如果version版本一直是自增的就不會出現ABA的情況啦。
分散式鎖
分散式鎖實現冪等性的邏輯就是,請求過來時,先去嘗試獲得分散式鎖,如果獲得成功,就執行業務邏輯,反之獲取失敗的話,就捨棄請求直接返回成功。執行流程如下圖所示:
三、HTTP的冪等
我們的介面,一般都是基於http的,所以我們再來聊聊Http的冪等吧。HTTP 請求方法主要有以下這幾種,我們看下各個介面是否都是冪等的。
-
GET方法
-
HEAD方法
-
OPTIONS方法
-
DELETE方法
-
POST 方法
-
PUT方法
GET 方法
HTTP 的GET方法用於獲取資源,可以類比於資料庫的select
查詢,不應該有副作用,所以是冪等的。它不會改變資源的狀態,不論你呼叫一次還是呼叫多次,效果一樣的,都沒有副作用。
如果你的GET方法是獲取最近最新的新聞,不同時間點呼叫,返回的資源內容雖然不一樣,但是最終對資源本質是沒有影響的哈,所以還是冪等的。
HEAD 方法
HTTP HEAD和GET有點像,主要區別是HEAD
不含有呈現資料,而僅僅是HTTP的頭資訊,所以它也是冪等的。如果想判斷某個資源是否存在,很多人會使用GET
,實際上用HEAD
則更加恰當。即HEAD
方法通常用來做探活使用。
OPTIONS方法
HTTP OPTIONS 主要用於獲取當前URL所支援的方法,也是有點像查詢,因此也是冪等的。
DELETE方法
HTTP DELETE 方法用於刪除資源,它是的冪等的。比如我們要刪除id=666
的帖子,一次執行和多次執行,影響的效果是一樣的呢。
POST 方法
HTTP POST 方法用於建立資源,可以類比於提交資訊
,顯然一次和多次提交是有副作用,執行效果是不一樣的,不滿足冪等性。
比如:POST 的語義是建立一篇帖子,HTTP 響應中應包含帖子的建立狀態以及帖子的 URI。兩次相同的POST請求會在伺服器端建立兩份資源,它們具有不同的 URI;所以,POST方法不具備冪等性。
PUT 方法
HTTP PUT 方法用於建立或更新操作,所對應的URI是要建立或更新的資源本身,有副作用,它應該滿足冪等性。
比如:PUT的語義是建立或更新 ID 為666的帖子。對同一 URI 進行多次 PUT 的副作用和一次 PUT 是相同的;因此,PUT 方法具有冪等性。