實戰,實現冪等的8種方案!

1 什么是冪等

冪等的概念來自數學和計算機科學

數學概念:冪等函數 f(x) = f (f(x)) ,如絕對值函數abs(-5) = abs(abs(-5))

計算機科學:多次請求同同一資源與一次請求的影響是一致的

2 為什么要冪等設計

為什么要冪等設計呢?

我們舉個例子:


除了轉賬的例子,我們日常開發中還會遇到許多其他考慮冪等的例子

A.MQ下游消費系統重復消費的問題

B.前端form表單重復提交的問題

3 接口超時了如何設計

我們在調用下游接口超時的時候該如何處理呢?一般來講有兩種方案:方案一:下游系統提供一個查詢接口,上游調用超時,調用查詢接口,根據查詢接口返回的結果做相應的處理

方案二:下游支付中心支持冪等,超時之后,發起重試

4 如何設計冪等

這么多場景需要考慮冪等,那如何設計冪等呢冪等的核心問題是保持請求的唯一性,那首先要解決的問題是全局唯一性ID

如果使用唯一索引控制冪等,那么唯一索引就是唯一的;

如果使用數據庫主鍵控制冪等,那么主鍵就是唯一的;

如果使用悲觀鎖控制冪等,那么底層標志還是全局唯一ID

4.1 全局唯一ID

如UUID、雪花算法(snowflake IDs),UUID的方式簡單,但是可讀性差,

雪花算法是一種生成分布式全局唯一ID的算法,生成的ID稱為Snowflake IDs。這種算法由Twitter創建,并用于推文的ID。

一個snowflake是64位

第一位代表正負,1代表負數、0代表整數,一般ID都是整數,所以默認是0;

接下來的41位是時間戳,自選定日期以來的毫秒數;

接下來的10位代表計算機ID;

最后12位代表這個機器生成的ID序列碼,允許在同一毫秒內創建多少個snowflake ID

當然,全局唯一ID還可以使用美團的Leaf,百度的Uidgenerator.

4.2 冪等設計的基本流程

冪等設計歸根結底是過濾一下請求,要過濾請求,必須滿足兩個條件,全局唯一ID,請求記錄被保存,當收到請求時,優先查詢請求記錄,記錄存在則返回結果,不存在則處理請求并記錄,然后返回結果。

5 實現冪等設計的8中方案冪等設計的基本流程都一致,簡單概括起來有8中實現方案

5.1 select + Insert + 主鍵/唯一索引沖突當請求過來,根據唯一流水號查詢請求記錄記錄存在直接返回結果記錄不存在,插入記錄,記錄插入成功,返回結果,插入報主鍵沖突,根據已經插入的記錄返回結果。

偽代碼如下:

/**

* 冪等處理

*/

Rsp idempotent(Request req){

Object requestRecord =selectByBizSeq(bizSeq);


if(requestRecord !=null){

//攔截是重復請求

log.info("重復請求,直接返回成功,流水號:{}",bizSeq);

return rsp;

}


try{

insert(req);

}catch(DuplicateKeyException e){

//攔截是重復請求,直接返回成功

log.info("主鍵沖突,是重復請求,直接返回成功,流水號:{}",bizSeq);

return rsp;

}


//正常處理請求

dealRequest(req);


return rsp;

}


5.2 直接Insert + 主鍵/唯一索引沖突

5.1 的方案中先判斷了請求是否存在,如果并發度不是很高的時候,可以跳過查詢,根據主鍵沖突判重。

偽代碼如下:

/**冪等處理 */*/Rsp idempotent(Request req){? ? try{? ? ? ? insert(req);? ? }catch(DuplicateKeyException e){? ? //攔截是重復請求,直接返回成功? ? ? ? log.info("主鍵沖突,是重復請求,直接返回成功,流水號:{}",bizSeq);? ? ? return rsp;? ? }? ? //正常處理請求? ? dealRequest(req);? ? return rsp;}

5.3 狀態機

很多業務表都是有狀態,如轉賬流水表,狀態一般有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:

}

5.4 抽取防重表

5.1和5.2的方案,都是建立在業務流水表上bizSeq的唯一性上。很多時候,我們業務表唯一流水號希望后端系統生成,又或者我們希望防重功能與業務表分隔開來,這時候我們可以單獨搞個防重表。當然防重表也是利用主鍵/索引的唯一性,如果插入防重表沖突即直接返回成功,如果插入成功,即去處理請求。

5.5 token令牌

為每個請求生成一個唯一的Token,并在服務端進行校驗,一旦處理了對應的請求,就丟棄該Token,避免重復處理。具體步驟:

1、服務端提供了發送 token 的接口。我們在分析業務的時候,哪些業務是存在冪等問題的, 就必須在執行業務前,先去獲取 token,服務器會把 token 保存到 redis 中。

2、然后調用業務接口請求時,把 token 攜帶過去,一般放在請求頭部。

3、服務器判斷 token 是否存在 redis 中,存在表示第一次請求,然后刪除 token,繼續執行業務。

4、如果判斷token不存在redis中,就表示是重復操作,直接返回重復標記給 client,這樣就保證了業務代碼,不被重復執行。

核心邏輯:

// 服務端接口,接收請求并處理token

void do(String token) {

? ? if (Redis.exists(token)) {

? ? ? // 刪除token,確保不會重復處理

? ? ? ? Redis.del(token);

? ? ? ? // 執行具體的業務操作

? ? ? ? doSometing();

? ? } else {

? ? ? ? log.info(token);

? ? }

}

注意:最好設計為先刪除 token,如果業務調用失敗,就重新獲取 token 再次請求。可以在 redis 使用 lua 腳本完成這個操作

if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end

5.6 悲觀鎖

通俗點講就是很悲觀,每次去操作數據時,都覺得別人中途會修改,所以每次在拿數據的時候都會上鎖。官方點講就是,共享資源每次只給一個線程使用,其它線程阻塞,用完后再把資源轉讓給其它線程。

悲觀鎖如何控制冪等的呢?就是加鎖呀,一般配合事務來實現。

偽代碼如下:

begin;? # 1.開始事務select * from order where order_id='666' # 查詢訂單,判斷狀態if(status !=處理中){//非處理中狀態,直接返回;return ;}## 處理業務邏輯update order set status='完成' where order_id='666' # 更新完成commit; # 5.提交事務

這里面order_id需要是索引或主鍵哈,要鎖住這條記錄就好,如果不是索引或者主鍵,會鎖表的!

悲觀鎖在同一事務操作過程中,鎖住了一行數據。別的請求過來只能等待,如果當前事務耗時比較長,就很影響接口性能。所以一般不建議用悲觀鎖做這個事情。

5.7 樂觀鎖

通過記錄數據的版本號或時間戳,僅當數據未被其他事務修改時,才允許更新操作執行。每次更新數據時,版本號都會遞增。

UPDATE orders

SET

? quantity = 1,

? order_status = 1,

? pay_time = '2024-04-30 10:20:00',

? version = version + 1

WHERE

? order_id = 'ORD-20231023-0001' AND

? version = 1;

效果演示:

如果 Session-01 已經提交了事務,Session-02 的更新操作將不會影響任何行,因為 version 已經從 1 增加到了 2。


5.8 分布式鎖

分布式鎖實現冪等性的邏輯就是,請求過來時,先去嘗試獲得分布式鎖,如果獲得成功,就執行業務邏輯,反之獲取失敗的話,就舍棄請求直接返回成功。執行流程如下圖所示:

分布式鎖可以使用Redis,也可以使用ZooKeeper,不過還是Redis相對好點,因為較輕量級。

Redis分布式鎖,可以使用命令SET EX PX NX + 唯一流水號實現,分布式鎖的key必須為業務的唯一標識哈

Redis執行設置key的動作時,要設置過期時間哈,這個過期時間不能太短,太短攔截不了重復請求,也不能設置太長,會占存儲空間,處理完成,要在finally中刪除鎖。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372

推薦閱讀更多精彩內容