溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

探討一下實(shí)現(xiàn)冪等性的幾種方式

發(fā)布時(shí)間:2020-07-18 09:35:01 來(lái)源:網(wǎng)絡(luò) 閱讀:359 作者:路人甲Java 欄目:編程語(yǔ)言

什么是冪等性?

對(duì)于同一筆業(yè)務(wù)操作,不管調(diào)用多少次,得到的結(jié)果都是一樣的。

冪等性設(shè)計(jì)

我們以對(duì)接支付寶充值為例,來(lái)分析支付回調(diào)接口如何設(shè)計(jì)?

如果我們系統(tǒng)中對(duì)接過(guò)支付寶充值功能的,我們需要給支付寶提供一個(gè)回調(diào)接口,支付寶回調(diào)信息中會(huì)攜帶(out_trade_no【商戶訂單號(hào)】,trade_no【支付寶交易號(hào)】),trade_no在支付寶中是唯一的,out_trade_no在商戶系統(tǒng)中是唯一的。

回調(diào)接口實(shí)現(xiàn)有以下實(shí)現(xiàn)方式。

方式1(普通方式)

過(guò)程如下:

1.接收到支付寶支付成功請(qǐng)求
2.根據(jù)trade_no查詢當(dāng)前訂單是否處理過(guò)
3.如果訂單已處理直接返回,若未處理,繼續(xù)向下執(zhí)行
4.開(kāi)啟本地事務(wù)
5.本地系統(tǒng)給用戶加錢(qián)
6.將訂單狀態(tài)置為成功
7.提交本地事務(wù)

上面的過(guò)程,對(duì)于同一筆訂單,如果支付寶同時(shí)通知多次,會(huì)出現(xiàn)什么問(wèn)題?當(dāng)多次通知同時(shí)到達(dá)第2步時(shí)候,查詢訂單都是未處理的,會(huì)繼續(xù)向下執(zhí)行,最終本地會(huì)給用戶加兩次錢(qián)。

此方式適用于單機(jī)其,通知按順序執(zhí)行的情況,只能用于自己寫(xiě)著玩玩。

方式2(jvm加鎖方式)

方式1中由于并發(fā)出現(xiàn)了問(wèn)題,此時(shí)我們使用java中的Lock加鎖,來(lái)防止并發(fā)操作,過(guò)程如下:

1.接收到支付寶支付成功請(qǐng)求
2.調(diào)用java中的Lock加鎖
3.根據(jù)trade_no查詢當(dāng)前訂單是否處理過(guò)
4.如果訂單已處理直接返回,若未處理,繼續(xù)向下執(zhí)行
5.開(kāi)啟本地事務(wù)
6.本地系統(tǒng)給用戶加錢(qián)
7.將訂單狀態(tài)置為成功
8.提交本地事務(wù)
9.釋放Lock鎖

分析問(wèn)題:
Lock只能在一個(gè)jvm中起效,如果多個(gè)請(qǐng)求都被同一套系統(tǒng)處理,上面這種使用Lock的方式是沒(méi)有問(wèn)題的,不過(guò)互聯(lián)網(wǎng)系統(tǒng)中,多數(shù)是采用集群方式部署系統(tǒng),同一套代碼后面會(huì)部署多套,如果支付寶同時(shí)發(fā)來(lái)多個(gè)通知經(jīng)過(guò)負(fù)載均衡轉(zhuǎn)發(fā)到不同的機(jī)器,上面的鎖就不起效了。此時(shí)對(duì)于多個(gè)請(qǐng)求相當(dāng)于無(wú)鎖處理了,又會(huì)出現(xiàn)方式1中的結(jié)果。此時(shí)我們需要分布式鎖來(lái)做處理。

方式3(悲觀鎖方式)

使用數(shù)據(jù)庫(kù)中悲觀鎖實(shí)現(xiàn)。悲觀鎖類(lèi)似于方式二中的Lock,只不過(guò)是依靠數(shù)據(jù)庫(kù)來(lái)實(shí)現(xiàn)的。數(shù)據(jù)中悲觀鎖使用for update來(lái)實(shí)現(xiàn),過(guò)程如下:

1.接收到支付寶支付成功請(qǐng)求
2.打開(kāi)本地事物
3.查詢訂單信息并加悲觀鎖

select * from t_order where order_id = trade_no for update;

4.判斷訂單是已處理
5.如果訂單已處理直接返回,若未處理,繼續(xù)向下執(zhí)行
6.給本地系統(tǒng)給用戶加錢(qián)
7.將訂單狀態(tài)置為成功
8.提交本地事物

重點(diǎn)在于for update,對(duì)for update,做一下說(shuō)明:
1.當(dāng)線程A執(zhí)行for update,數(shù)據(jù)會(huì)對(duì)當(dāng)前記錄加鎖,其他線程執(zhí)行到此行代碼的時(shí)候,會(huì)等待線程A釋放鎖之后,才可以獲取鎖,繼續(xù)后續(xù)操作。
2.事物提交時(shí),for update獲取的鎖會(huì)自動(dòng)釋放。

方式3可以正常實(shí)現(xiàn)我們需要的效果,能保證接口的冪等性,不過(guò)存在一些缺點(diǎn):
1.如果業(yè)務(wù)處理比較耗時(shí),并發(fā)情況下,后面線程會(huì)長(zhǎng)期處于等待狀態(tài),占用了很多線程,讓這些線程處于無(wú)效等待狀態(tài),我們的web服務(wù)中的線程數(shù)量一般都是有限的,如果大量線程由于獲取for update鎖處于等待狀態(tài),不利于系統(tǒng)并發(fā)操作。

方式4(樂(lè)觀鎖方式)

依靠數(shù)據(jù)庫(kù)中的樂(lè)觀鎖來(lái)實(shí)現(xiàn)。

1.接收到支付寶支付成功請(qǐng)求
2.查詢訂單信息

select * from t_order where order_id = trade_no;

3.判斷訂單是已處理
4.如果訂單已處理直接返回,若未處理,繼續(xù)向下執(zhí)行
5.打開(kāi)本地事物
6.給本地系統(tǒng)給用戶加錢(qián)
7.將訂單狀態(tài)置為成功,注意這塊是重點(diǎn),偽代碼:

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

注意:
update t_order set status = 1 where order_id = trade_no where status = 0; 是依靠樂(lè)觀鎖來(lái)實(shí)現(xiàn)的,status=0作為條件去更新,類(lèi)似于java中的cas操作;關(guān)于什么是cas操作,可以移步:什么是 CAS 機(jī)制?
執(zhí)行這條sql的時(shí)候,如果有多個(gè)線程同時(shí)到達(dá)這條代碼,數(shù)據(jù)內(nèi)部會(huì)保證update同一條記錄會(huì)排隊(duì)執(zhí)行,最終最有一條update會(huì)執(zhí)行成功,其他未成功的,他們的num為0,然后根據(jù)num來(lái)進(jìn)行提交或者回滾操作。

方式4(唯一約束方式)

依賴數(shù)據(jù)庫(kù)中唯一約束來(lái)實(shí)現(xiàn)。

我們可以創(chuàng)建一個(gè)表:

CREATE TABLE `t_uq_dipose` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `ref_type` varchar(32) NOT NULL DEFAULT '' COMMENT '關(guān)聯(lián)對(duì)象類(lèi)型',
  `ref_id` varchar(64) NOT NULL DEFAULT '' COMMENT '關(guān)聯(lián)對(duì)象id',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uq_1` (`ref_type`,`ref_id`) COMMENT '保證業(yè)務(wù)唯一性'
) ENGINE=InnoDB;

對(duì)于任何一個(gè)業(yè)務(wù),有一個(gè)業(yè)務(wù)類(lèi)型(ref_type),業(yè)務(wù)有一個(gè)全局唯一的訂單號(hào),業(yè)務(wù)來(lái)的時(shí)候,先查詢t_uq_dipose表中是否存在相關(guān)記錄,若不存在,繼續(xù)放行。

過(guò)程如下:

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

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

3.判斷訂單是已處理
4.如果訂單已處理直接返回,若未處理,繼續(xù)向下執(zhí)行
5.打開(kāi)本地事物
6.給本地系統(tǒng)給用戶加錢(qián)
7.將訂單狀態(tài)置為成功
8.向t_uq_dipose插入數(shù)據(jù),插入成功,提交本地事務(wù),插入失敗,回滾本地事務(wù),偽代碼:

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

說(shuō)明:
對(duì)于同一個(gè)業(yè)務(wù),ref_type是一樣的,當(dāng)并發(fā)時(shí),插入數(shù)據(jù)只會(huì)有一條成功,其他的會(huì)違法唯一約束,進(jìn)入catch邏輯,當(dāng)前事務(wù)會(huì)被回滾,最終最有一個(gè)操作會(huì)成功,從而保證了冪等性操作。
關(guān)于這種方式可以寫(xiě)成通用的方式,不過(guò)業(yè)務(wù)量大的情況下,t_uq_dipose插入數(shù)據(jù)會(huì)成為系統(tǒng)的瓶頸,需要考慮分表操作,解決性能問(wèn)題。
上面的過(guò)程中向t_uq_dipose插入記錄,最好放在最后執(zhí)行,原因:插入操作會(huì)鎖表,放在最后能讓鎖表的時(shí)間降到最低,提升系統(tǒng)的并發(fā)性。

關(guān)于消息服務(wù)中,消費(fèi)者如何保證消息處理的冪等性?
每條消息都有一個(gè)唯一的消息id,類(lèi)似于上面業(yè)務(wù)中的trade_no,使用上面的方式即可實(shí)現(xiàn)消息消費(fèi)的冪等性。

總結(jié)

1.實(shí)現(xiàn)冪等性常見(jiàn)的方式有:悲觀鎖(for update)、樂(lè)觀鎖、唯一約束
2.幾種方式,按照最優(yōu)排序:樂(lè)觀鎖 > 唯一約束 > 悲觀鎖
3.希望大家能夠一起探討,一起成長(zhǎng),喜歡的關(guān)注一下(公眾號(hào):javacode2018),可以留言

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI