探討一下實現冪等性的幾種方式

什麼是冪等性?

對於同一筆業務操做,無論調用多少次,獲得的結果都是同樣的。java

冪等性設計

咱們以對接支付寶充值爲例,來分析支付回調接口如何設計?web

若是咱們系統中對接過支付寶充值功能的,咱們須要給支付寶提供一個回調接口,支付寶回調信息中會攜帶(out_trade_no【商戶訂單號】,trade_no【支付寶交易號】),trade_no在支付寶中是惟一的,out_trade_no在商戶系統中是惟一的。sql

回調接口實現有如下實現方式。數據庫

方式1(普通方式)

過程以下:多線程

1.接收到支付寶支付成功請求
2.根據trade_no查詢當前訂單是否處理過
3.若是訂單已處理直接返回,若未處理,繼續向下執行
4.開啓本地事務
5.本地系統給用戶加錢
6.將訂單狀態置爲成功
7.提交本地事務併發

上面的過程,對於同一筆訂單,若是支付寶同時通知屢次,會出現什麼問題?當屢次通知同時到達第2步時候,查詢訂單都是未處理的,會繼續向下執行,最終本地會給用戶加兩次錢。負載均衡

此方式適用於單機其,通知按順序執行的狀況,只能用於本身寫着玩玩。jvm

方式2(jvm加鎖方式)

方式1中因爲併發出現了問題,此時咱們使用java中的Lock加鎖,來防止併發操做,過程以下:分佈式

1.接收到支付寶支付成功請求
2.調用java中的Lock加鎖
3.根據trade_no查詢當前訂單是否處理過
4.若是訂單已處理直接返回,若未處理,繼續向下執行
5.開啓本地事務
6.本地系統給用戶加錢
7.將訂單狀態置爲成功
8.提交本地事務
9.釋放Lock鎖性能

分析問題:
Lock只能在一個jvm中起效,若是多個請求都被同一套系統處理,上面這種使用Lock的方式是沒有問題的,不過互聯網系統中,多數是採用集羣方式部署系統,同一套代碼後面會部署多套,若是支付寶同時發來多個通知通過負載均衡轉發到不一樣的機器,上面的鎖就不起效了。此時對於多個請求至關於無鎖處理了,又會出現方式1中的結果。此時咱們須要分佈式鎖來作處理。

方式3(悲觀鎖方式)

使用數據庫中悲觀鎖實現。悲觀鎖相似於方式二中的Lock,只不過是依靠數據庫來實現的。數據中悲觀鎖使用for update來實現,過程以下:

1.接收到支付寶支付成功請求
2.打開本地事物
3.查詢訂單信息並加悲觀鎖

select * from t_order where order_id = trade_no for update;

4.判斷訂單是已處理
5.若是訂單已處理直接返回,若未處理,繼續向下執行
6.給本地系統給用戶加錢
7.將訂單狀態置爲成功
8.提交本地事物

重點在於for update,對for update,作一下說明:
1.當線程A執行for update,數據會對當前記錄加鎖,其餘線程執行到此行代碼的時候,會等待線程A釋放鎖以後,才能夠獲取鎖,繼續後續操做。
2.事物提交時,for update獲取的鎖會自動釋放。

方式3能夠正常實現咱們須要的效果,能保證接口的冪等性,不過存在一些缺點:
1.若是業務處理比較耗時,併發狀況下,後面線程會長期處於等待狀態,佔用了不少線程,讓這些線程處於無效等待狀態,咱們的web服務中的線程數量通常都是有限的,若是大量線程因爲獲取for update鎖處於等待狀態,不利於系統併發操做。

方式4(樂觀鎖方式)

依靠數據庫中的樂觀鎖來實現。

1.接收到支付寶支付成功請求
2.查詢訂單信息

select * from t_order where order_id = trade_no;

3.判斷訂單是已處理
4.若是訂單已處理直接返回,若未處理,繼續向下執行
5.打開本地事物
6.給本地系統給用戶加錢
7.將訂單狀態置爲成功,注意這塊是重點,僞代碼:

update t_order set status = 1 where order_id = trade_no where status = 0;
//上面的update操做會返回影響的行數num
if(num==1){
 //表示更新成功
 提交事務;
}else{
 //表示更新失敗
 回滾事務;
}

注意:
update t_order set status = 1 where order_id = trade_no where status = 0; 是依靠樂觀鎖來實現的,status=0做爲條件去更新,相似於java中的cas操做;關於什麼是cas操做,能夠移步:什麼是 CAS 機制?
執行這條sql的時候,若是有多個線程同時到達這條代碼,數據內部會保證update同一條記錄會排隊執行,最終最有一條update會執行成功,其餘未成功的,他們的num爲0,而後根據num來進行提交或者回滾操做。

方式4(惟一約束方式)

依賴數據庫中惟一約束來實現。

咱們能夠建立一個表:

CREATE TABLE `t_uq_dipose` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `ref_type` varchar(32) NOT NULL DEFAULT '' COMMENT '關聯對象類型',
  `ref_id` varchar(64) NOT NULL DEFAULT '' COMMENT '關聯對象id',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uq_1` (`ref_type`,`ref_id`) COMMENT '保證業務惟一性'
) ENGINE=InnoDB;

對於任何一個業務,有一個業務類型(ref_type),業務有一個全局惟一的訂單號,業務來的時候,先查詢t_uq_dipose表中是否存在相關記錄,若不存在,繼續放行。

過程以下:

1.接收到支付寶支付成功請求
2.查詢t_uq_dipose(條件ref_id,ref_type),能夠判斷訂單是否已處理

select * from t_uq_dipose where ref_type = '充值訂單' and ref_id = trade_no;

3.判斷訂單是已處理
4.若是訂單已處理直接返回,若未處理,繼續向下執行
5.打開本地事物
6.給本地系統給用戶加錢
7.將訂單狀態置爲成功
8.向t_uq_dipose插入數據,插入成功,提交本地事務,插入失敗,回滾本地事務,僞代碼:

try{
    insert into t_uq_dipose (ref_type,ref_id) values ('充值訂單',trade_no);
    提交本地事務:
}catch(Exception e){
    回滾本地事務;
}

說明:
對於同一個業務,ref_type是同樣的,當併發時,插入數據只會有一條成功,其餘的會違法惟一約束,進入catch邏輯,當前事務會被回滾,最終最有一個操做會成功,從而保證了冪等性操做。
關於這種方式能夠寫成通用的方式,不過業務量大的狀況下,t_uq_dipose插入數據會成爲系統的瓶頸,須要考慮分表操做,解決性能問題。
上面的過程當中向t_uq_dipose插入記錄,最好放在最後執行,緣由:插入操做會鎖表,放在最後能讓鎖表的時間降到最低,提高系統的併發性。

關於消息服務中,消費者如何保證消息處理的冪等性?
每條消息都有一個惟一的消息id,相似於上面業務中的trade_no,使用上面的方式便可實現消息消費的冪等性。

總結

1.實現冪等性常見的方式有:悲觀鎖(for update)、樂觀鎖、惟一約束
2.幾種方式,按照最優排序:樂觀鎖 > 惟一約束 > 悲觀鎖
3.但願你們可以一塊兒探討,一塊兒成長,喜歡的關注一下(公衆號:javacode2018),能夠留言

相關文章
相關標籤/搜索