1. 程式人生 > >探討一下實現冪等性的幾種方式

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

什麼是冪等性?

對於同一筆業務操作,不管呼叫多少次,得到的結果都是一樣的。

冪等性設計

我們以對接支付寶充值為例,來分析支付回撥介面如何設計?

如果我們系統中對接過支付寶充值功能的,我們需要給支付寶提供一個回撥介面,支付寶回撥資訊中會攜帶(out_trade_no【商戶訂單號】,trade_no【支付寶交易號】),trade_no在支付寶中是唯一的,out_trade_no在商戶系統中是唯一的。

回撥介面實現有以下實現方式。

方式1(普通方式)

過程如下:

1.接收到支付寶支付成功請求

2.根據trade_no查詢當前訂單是否處理過

3.如果訂單已處理直接返回,若未處理,繼續向下執行

4.開啟本地事務

5.本地系統給使用者加錢

6.將訂單狀態置為成功

7.提交本地事務

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

此方式適用於單機其,通知按順序執行的情況,只能用於自己寫著玩玩。

方式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.幾種方式,按照最優排序:樂觀鎖 > 唯一約束 > 悲觀鎖

歡迎工作一到五年的Java工程師朋友們加入Java程式設計師開發: 721575865

群內提供免費的Java架構學習資料(裡面有高可用、高併發、高效能及分散式、Jvm效能調優、Spring原始碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的