冪等設計,都有哪些技術方案?

前言

大家好,我是 Tom 哥~    今天我們一起來聊聊冪等設計

  1. 什麼是冪等

  2. 爲什麼需要冪等

  3. 接口超時,如何處理呢?

  4. 如何設計冪等?

  5. 實現冪等的 8 種方案

  6. HTTP 的冪等

1. 什麼是冪等?

冪等是一個數學與計算機科學概念。

2. 爲什麼需要冪等

舉個例子:

我們開發一個轉賬功能,假設我們調用下游接口超時了。一般情況下,超時可能是網絡傳輸丟包的問題,也可能是請求時沒送到,還有可能是請求到了,返回結果卻丟了。這時候我們是否可以重試呢?如果重試的話,是否會多轉了一筆錢呢?

轉賬超時

當前互聯網的系統幾乎都是解耦隔離後,會存在各個不同系統的相互遠程調用。調用遠程服務會有三個狀態:成功,失敗,或者超時。前兩者都是明確的狀態,而超時則是未知狀態。我們轉賬超時的時候,如果下游轉賬系統做好冪等控制,我們發起重試,那即可以保證轉賬正常進行,又可以保證不會多轉一筆

其實除了轉賬這個例子,日常開發中,還有很多很多例子需要考慮冪等。比如:

3. 接口超時了,到底如何處理?

如果我們調用下游接口超時了,我們應該怎麼處理呢?

兩種方案處理:

拿我們的轉賬例子來說,轉賬系統提供一個查詢轉賬記錄的接口,如果渠道系統調用轉賬系統超時時,渠道系統先去查詢一下這筆記錄,看下這筆轉賬記錄成功還是失敗,如果成功就走成功流程,失敗再重試發起轉賬。

兩種方案都是挺不錯的,但是如果是 MQ 重複消費的場景,方案一處理並不是很妥,所以,我們還是要求下游系統對外接口支持冪等

4. 如何設計冪等

既然這麼多場景需要考慮冪等,那我們如何設計冪等呢?

冪等意味着一條請求的唯一性。不管是你哪個方案去設計冪等,都需要一個全局唯一的 ID,去標記這個請求是獨一無二的。

4.1 全局的唯一性 ID

全局唯一性 ID,我們怎麼去生成呢?你可以回想下,數據庫主鍵 Id 怎麼生成的呢?

是的,我們可以使用UUID,但是 UUID 的缺點比較明顯,它字符串佔用的空間比較大,生成的 ID 過於隨機,可讀性差,而且沒有遞增。

我們還可以使用雪花算法(Snowflake) 生成唯一性 ID。

雪花算法是一種生成分佈式全局唯一 ID 的算法,生成的 ID 稱爲Snowflake IDs。這種算法由 Twitter 創建,並用於推文的 ID。

一個 Snowflake ID 有 64 位。

雪花算法

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

4.2 冪等設計的基本流程

冪等處理的過程,說到底其實就是過濾一下已經收到的請求,當然,請求一定要有一個全局唯一的ID標記哈。然後,怎麼判斷請求是否之前收到過呢?把請求儲存起來,收到請求時,先查下存儲記錄,記錄存在就返回上次的結果,不存在就處理請求。

一般的冪等處理就是這樣啦,如下:

5. 實現冪等的 8 種方案

冪等設計的基本流程都是類似的,我們簡簡單單來過一下冪等實現的 8 中方案哈

5.1 select+insert + 主鍵 / 唯一索引衝突

日常開發中,爲了實現交易接口冪等,我是這樣實現的:

交易請求過來,我會先根據請求的唯一流水號 bizSeq字段,先select一下數據庫的流水錶

流程圖如下

僞代碼如下:

/**
 * 冪等處理
 */
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;
}

爲什麼前面已經select查詢了,還需要try...catch...捕獲重複異常呢?

是因爲高併發場景下,兩個請求去select的時候,可能都沒查到,然後都走到 insert 的地方啦。

當然,用唯一索引代替數據庫主鍵也是可以的哈,都是全局唯一的 ID 即可。

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 令牌方案一般包括兩個請求階段:

  1. 客戶端請求申請獲取 token,服務端生成 token 返回

  2. 客戶端帶着 token 請求,服務端校驗 token

流程圖如下:

  1. 客戶端發起請求,申請獲取 token。

  2. 服務端生成全局唯一的 token,保存到 redis 中(一般會設置一個過期時間),然後返回給客戶端。

  3. 客戶端帶着 token,發起請求。

  4. 服務端去 redis 確認 token 是否存在,一般用 redis.del(token)的方式,如果存在會刪除成功,即處理業務邏輯,如果刪除失敗不處理業務邏輯,直接返回結果。

5.6 悲觀鎖 (如 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.提交事務

這種場景是非原子操作的,在高併發環境下,可能會造成一個業務被執行兩次的問題:

當一個請求 A 在執行中時,而另一個請求 B 也開始狀態判斷的操作。因爲請求 A 還未來得及更改狀態,所以請求 B 也能執行成功,這就導致一個業務被執行了兩次。

可以使用數據庫悲觀鎖(select ...for update)解決這個問題.

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

5.7 樂觀鎖

悲觀鎖有性能問題,可以試下樂觀鎖

什麼是樂觀鎖

樂觀鎖在操作數據時, 則非常樂觀,認爲別人不會同時在修改數據,因此樂觀鎖不會上鎖。只是在執行更新的時候判斷一下,在此期間別人是否修改了數據。

怎樣實現樂觀鎖呢?

就是給表的加多一列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 的情況啦。

5.8 分佈式鎖

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

6. HTTP 的冪等

我們的接口,一般都是基於 http 的,所以我們再來聊聊 Http 的冪等吧。HTTP 請求方法主要有以下這幾種,我們看下各個接口是否都是冪等的。

6.1 GET 方法

HTTP 的 GET 方法用於獲取資源,可以類比於數據庫的select查詢,不應該有副作用,所以是冪等的。它不會改變資源的狀態,不論你調用一次還是調用多次,效果一樣的,都沒有副作用。

如果你的 GET 方法是獲取最近最新的新聞,不同時間點調用,返回的資源內容雖然不一樣,但是最終對資源本質是沒有影響的哈,所以還是冪等的。

6.2 HEAD 方法

HTTP HEAD 和 GET 有點像,主要區別是HEAD不含有呈現數據,而僅僅是 HTTP 的頭信息,所以它也是冪等的。如果想判斷某個資源是否存在,很多人會使用GET,實際上用HEAD則更加恰當。即HEAD方法通常用來做探活使用。

6.3 OPTIONS 方法

HTTP OPTIONS 主要用於獲取當前 URL 所支持的方法,也是有點像查詢,因此也是冪等的。

6.4 DELETE 方法

HTTP DELETE 方法用於刪除資源,它是的冪等的。比如我們要刪除id=666的帖子,一次執行和多次執行,影響的效果是一樣的呢。

6.5 POST 方法

HTTP POST 方法用於創建資源,可以類比於提交信息,顯然一次和多次提交是有副作用,執行效果是不一樣的,不滿足冪等性

比如:POST http://www.tianluo.com/articles 的語義是在 http://www.tianluo.com/articles 下創建一篇帖子,HTTP 響應中應包含帖子的創建狀態以及帖子的 URI。兩次相同的 POST 請求會在服務器端創建兩份資源,它們具有不同的 URI;所以,POST 方法不具備冪等性

6.6 PUT 方法

HTTP PUT 方法用於創建或更新操作,所對應的 URI 是要創建或更新的資源本身,有副作用,它應該滿足冪等性。

比如:PUT http://www.tianluo.com/articles/666 的語義是創建或更新 ID 爲 666 的帖子。對同一 URI 進行多次 PUT 的副作用和一次 PUT 是相同的;因此,PUT 方法具有冪等性。

參考資料

[1] 彈力設計篇之 “冪等性設計”: https://time.geekbang.org/column/article/4050

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/qezCgEqtaQg_mcLsPycO0w