外貿(mào)領(lǐng)航
首頁(yè)行業(yè)資訊 > 電商防止訂單重復(fù)提交「下單頻繁怎么破解」

電商防止訂單重復(fù)提交「下單頻繁怎么破解」

來(lái)源:互聯(lián)網(wǎng) 2024-07-17 13:04:02
用戶(hù)下單流程

我們從用戶(hù)瀏覽商品開(kāi)始,看看用戶(hù)下單的簡(jiǎn)要過(guò)程:

用戶(hù)下單簡(jiǎn)要過(guò)程

瀏覽商品:用戶(hù)查看商品詳情加購(gòu)/結(jié)算:用戶(hù)可以選擇直接購(gòu)買(mǎi)商品,也可以先加入購(gòu)物車(chē),用戶(hù)購(gòu)買(mǎi)的這一步就是結(jié)算確認(rèn)下單:結(jié)算完成,就進(jìn)入了下單頁(yè)面,提交訂單,這一步就會(huì)生成一個(gè)訂單,然后進(jìn)入付款頁(yè)面

我們可以看到,下單是發(fā)生在結(jié)算之后,下單之后,會(huì)生成唯一的訂單號(hào),接下來(lái),客戶(hù)端需要用這個(gè)訂單號(hào)去完成支付。

那接下來(lái)先看看,為什么發(fā)生重復(fù)下單?

為什么會(huì)重復(fù)下單

為什么會(huì)重復(fù)下單,對(duì)于訂單服務(wù)而言,就是接到了多個(gè)下單的請(qǐng)求,原因可能有很多,最常見(jiàn)的是這兩種:

用戶(hù)重復(fù)提交網(wǎng)絡(luò)原因?qū)е碌某瑫r(shí)重試

重復(fù)下單原因

如何防止重復(fù)下單

防止用戶(hù)提交,最常規(guī)的做法,就是客戶(hù)端點(diǎn)擊下單之后,在收到服務(wù)端響應(yīng)之前,按鈕置灰。

當(dāng)然,防止重復(fù)下單,肯定不能只依靠客戶(hù)端,可能會(huì)因?yàn)橐恍┚W(wǎng)絡(luò)的抖動(dòng),導(dǎo)致仍然有重復(fù)的請(qǐng)求到達(dá)服務(wù)端,所以還是要在服務(wù)端做防重/冪等的處理。

PS:這里額外插入一點(diǎn)我對(duì)防重和冪等的理解:防重指的是防止重復(fù)提交,冪等指的是多次請(qǐng)求如一次,簡(jiǎn)單說(shuō),就是防重可以給對(duì)重復(fù)請(qǐng)求拋異常,冪等是對(duì)重復(fù)的請(qǐng)求響應(yīng)第一次的結(jié)果,在我們討論的這個(gè)場(chǎng)景里,冪等就是響應(yīng)唯一的訂單號(hào)。

防重和冪等

防重第一步,需要識(shí)別請(qǐng)求是否重復(fù),這一步,需要客戶(hù)端配合實(shí)現(xiàn)。

為什么呢?大家想一下,下單的時(shí)候,服務(wù)端怎么去判斷這個(gè)下單請(qǐng)求是否唯一呢?金額?商品??jī)?yōu)惠券?……萬(wàn)一用戶(hù)就是喜歡,又下了一個(gè)一模一樣的單呢?

所以,需要客戶(hù)端在請(qǐng)求下單接口的時(shí)候,需要生成一個(gè)唯一的請(qǐng)求號(hào):requestId,服務(wù)端拿這個(gè)請(qǐng)求號(hào),判斷是否重復(fù)請(qǐng)求。

那么,接下來(lái),壓力就給到服務(wù)端了,看看服務(wù)端怎么實(shí)現(xiàn)防重/冪等吧!

利用數(shù)據(jù)庫(kù)實(shí)現(xiàn)冪等

可以在訂單表t_order里添加一個(gè)字段:requestId,添加唯一索引:

唯一請(qǐng)求字段

這樣一來(lái),如果是重復(fù)的請(qǐng)求,在落庫(kù)的時(shí)候就會(huì)報(bào)錯(cuò),為了保證冪等性,我們可以catch住這個(gè)異常,根據(jù)requestId獲取訂單號(hào),然后向客戶(hù)端響應(yīng)訂單號(hào)。

大概的代碼如下:

PlaceOrderResVO placeOrder(PlaceOrderReqVO reqVO) { try { //下單業(yè)務(wù)邏輯 …… //生成訂單號(hào) String oid=generateOid(); …… //訂單落庫(kù) Order order = orderMapper.saveOrder(orderDO); //響應(yīng)訂單 resVO.setOid(order.getOid()); return resVO; } catch(UniqueKeyViolationException e) { // 發(fā)生了重復(fù)異常 // 根據(jù)請(qǐng)求號(hào)獲取訂單 Order order = getOrderByRequestId(reqVO.getRequestId()); resVO.setOid(order.getOid()); return resVO; } catch (Exception e) { }}

當(dāng)然,這里不太好的地方是,拿異常來(lái)做業(yè)務(wù)判斷。

利用Redis防重

另外一個(gè)辦法,就是下單請(qǐng)求的時(shí)候要加鎖了,通常我們的服務(wù)都是集群部署,所以一般都是用Redis實(shí)現(xiàn)分布式鎖。

大概的邏輯:

就是以requestId為維度,進(jìn)行加鎖,如果獲取鎖失敗,就拋一個(gè)自定義的重復(fù)下單異常。如果獲取到鎖,先check一下,是否已經(jīng)下單,為了提高性能,下單完成后,也把下單的結(jié)果放在Redis緩存里。

redis防重邏輯

大概的代碼如下:

public PlaceOrderResVO placeOrder(PlaceOrderReqVO reqVO) { //加鎖 RLock orderLock = redissonClient.getLock(RedisConstant.PLACE_ORDER_LOCK_KEY reqVO.getRequestId()); //獲取鎖失敗,拋出重復(fù)下單異常 if(orderLock.isExistes){ throw new OrderRepeatException(); } // 加鎖 orderLock.lock(); try { //檢查是否已經(jīng)下單 RBucket<PlaceOrderResVO> orderCache = redissonClient.getBucket(RedisConstant.PLACE_ORDER_LOCK_KEY reqVO.getRequestId()); if(orderCache.isExistes){ return orderCache.get(); } //下單業(yè)務(wù)邏輯 …… //落庫(kù) //訂單落庫(kù) Order order = orderMapper.saveOrder(orderDO); …… //緩存結(jié)果 orderCache.put(resVO); return resVO; } } catch (Exception e) { //…… } finally { orderLock.unlock(); } return resVO; }

這里再說(shuō)明一下:

為什么獲取不到鎖的時(shí)候要拋異常呢?

因?yàn)橄聠卫锩嫫鋵?shí)還有一些其它的業(yè)務(wù)流程,比如鎖庫(kù)存、清優(yōu)惠券……而此時(shí),獲取到鎖的請(qǐng)求的下單流程還沒(méi)有結(jié)束,下單的結(jié)果還獲取不到,沒(méi)法完成響應(yīng),也就沒(méi)辦法做冪等。

客戶(hù)端,也可以根據(jù)響應(yīng)的狀態(tài)碼,進(jìn)行特殊處理,比如這個(gè)異常先不提示,但是允許用戶(hù)再次點(diǎn)擊下單按鈕,來(lái)提升用戶(hù)的體驗(yàn)。

原文鏈接:https://mp.weixin.qq.com/s/Dc_4taB6Boojdw_0mngroQ

作者:三分惡

鄭重聲明:本文版權(quán)歸原作者所有,轉(zhuǎn)載文章僅為傳播更多信息之目的,如有侵權(quán)行為,請(qǐng)第一時(shí)間聯(lián)系我們修改或刪除,多謝。

CopyRight ? 外貿(mào)領(lǐng)航 2023 All Rights Reserved.