一對一聊天平臺原始碼,實現冪等的8種方案

zhibo系統開發發表於2024-01-06

一對一聊天平臺原始碼,實現冪等的8種方案

在一對一聊天平臺原始碼開發時,冪等設計的基本流程都是類似的,我們簡簡單單來過一下冪等實現的8中方案

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

在一對一聊天平臺原始碼開發中,為了實現交易介面冪等,我是這樣實現的:
交易請求過來,我會先根據請求的唯 一 流水號 bizSeq欄位,先select一下資料庫的流水錶

如果資料已經存在,就攔截是重複請求,直接返回成功;
如果資料不存在,就執行insert插入,如果insert成功,則直接返回成功,如果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;
}

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

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

當然,用唯 一索引代替資料庫主鍵也是可以的哈,都是全域性唯 一的ID即可。

二、直接insert + 主鍵/唯 一索引衝突

在方案一中,都會先查一下流水錶的交易請求,判斷是否存在,然後不存在再插入請求記錄。如果一對一聊天平臺原始碼中重複請求的機率比較低的話,我們可以直接插入請求,利用主鍵/唯 一索引衝突,去判斷是重複請求。
流程圖如下:
在這裡插入圖片描述

虛擬碼如下:

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

溫馨提示 :

大家別搞混哈,防重和冪等設計其實是有區別的。防重主要為了避免產生重複資料,把重複請求攔截下來即可。而冪等設計除了攔截已經處理的請求,還要求每次相同的請求都返回一樣的結果。不過呢,很多時候,它們的處理可以是類似,只是返回響應不一樣。

三、狀態機冪等

一對一聊天平臺原始碼的很多業務表,都是有狀態的,比如轉賬流水錶,就會有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=2;"
   if(rows==1){
      log.info(“更新成功,可以處理該請求”);
      //其他業務邏輯處理
      return rsp;
   }else if(rows==0){
      log.info(“更新不成功,不處理該請求”);
      //不處理,直接返回
      return rsp;
   }
   
   log.warn("資料異常")
   return rsp:
}

狀態機是怎麼實現冪等的呢?

第1次請求來時,bizSeq流水號是 666 ,該流水的狀態是處理中,值是 1 ,要更新為2-成功的狀態 ,所以該update語句可以正常更新資料,sql執行結果的影響行數是1,流水狀態最後變成了2。
第2請求也過來了,如果它的流水號還是 666 ,因為該流水狀態已經2-成功的狀態 了,所以更新結果是0,不會再處理業務邏輯,介面直接返回成功。

四、抽取防重表

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

五、 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.提交事務

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

當一個請求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.提交事務

這裡面order_id需要是索引或主鍵哈,要鎖住這條記錄就好,如果不是索引或者主鍵,會鎖表的!
悲觀鎖在同一事務操作過程中,鎖住了一行資料。別的請求過來只能等待,如果當前事務耗時比較長,就很影響介面效能。所以一般不建議用悲觀鎖做這個事情。

七、樂觀鎖

悲觀鎖有效能問題,可以試下樂觀鎖。
什麼是樂觀鎖?

樂觀鎖在操作一對一聊天平臺原始碼中的資料時,則非常樂觀,認為別人不會同時在修改資料,因此樂觀鎖不會上鎖。只是在執行更新的時候判斷一下,在此期間別人是否修改了資料。

怎樣實現樂觀鎖呢?

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

八、分散式鎖

在一對一聊天平臺原始碼中分散式鎖實現冪等性的邏輯就是,請求過來時,先去嘗試獲得分散式鎖,如果獲得成功,就執行業務邏輯,反之獲取失敗的話,就捨棄請求直接返回成功。執行流程如下圖所示:
在這裡插入圖片描述

在一對一聊天平臺原始碼中分散式鎖可以使用Redis,也可以使用ZooKeeper,不過還是Redis相對好點,因為較輕量級。
Redis分散式鎖,可以使用命令SET EX PX NX + 唯 一 流水號實現,分散式鎖的key必須為業務的唯 一 標識哈
Redis執行設定key的動作時,要設定過期時間哈,這個過期時間不能太短,太短攔截不了重複請求,也不能設定太長,會佔儲存空間。

以上是一對一聊天平臺原始碼,實現冪等的8種方案, 更多內容歡迎關注之後的文章


來自 “ ITPUB部落格 ” ,連結:https://blog.itpub.net/69978258/viewspace-3002920/,如需轉載,請註明出處,否則將追究法律責任。

相關文章