用戶下單流程
我們從用戶瀏覽商品開始,看看用戶下單的簡要過程:
用戶下單簡要過程
- 瀏覽商品:用戶查看商品詳情
- 加購/結(jié)算:用戶可以選擇直接購買商品,也可以先加入購物車,用戶購買的這一步就是結(jié)算
- 確認(rèn)下單:結(jié)算完成,就進(jìn)入了下單頁面,提交訂單,這一步就會(huì)生成一個(gè)訂單,然后進(jìn)入付款頁面
我們可以看到,下單是發(fā)生在結(jié)算之后,下單之后,會(huì)生成唯一的訂單號(hào),接下來,客戶端需要用這個(gè)訂單號(hào)去完成支付。
那接下來先看看,為什么發(fā)生重復(fù)下單?
為什么會(huì)重復(fù)下單
為什么會(huì)重復(fù)下單,對(duì)于訂單服務(wù)而言,就是接到了多個(gè)下單的請求,原因可能有很多,最常見的是這兩種:
- 用戶重復(fù)提交
- 網(wǎng)絡(luò)原因?qū)е碌某瑫r(shí)重試
重復(fù)下單原因
如何防止重復(fù)下單
防止用戶提交,最常規(guī)的做法,就是客戶端點(diǎn)擊下單之后,在收到服務(wù)端響應(yīng)之前,按鈕置灰。
當(dāng)然,防止重復(fù)下單,肯定不能只依靠客戶端,可能會(huì)因?yàn)橐恍┚W(wǎng)絡(luò)的抖動(dòng),導(dǎo)致仍然有重復(fù)的請求到達(dá)服務(wù)端,所以還是要在服務(wù)端做防重/冪等的處理。
PS:這里額外插入一點(diǎn)我對(duì)防重和冪等的理解:防重指的是防止重復(fù)提交,冪等指的是多次請求如一次,簡單說,就是防重可以給對(duì)重復(fù)請求拋異常,冪等是對(duì)重復(fù)的請求響應(yīng)第一次的結(jié)果,在我們討論的這個(gè)場景里,冪等就是響應(yīng)唯一的訂單號(hào)。
防重和冪等
防重第一步,需要識(shí)別請求是否重復(fù),這一步,需要客戶端配合實(shí)現(xiàn)。
為什么呢?大家想一下,下單的時(shí)候,服務(wù)端怎么去判斷這個(gè)下單請求是否唯一呢?金額?商品?優(yōu)惠券?……萬一用戶就是喜歡,又下了一個(gè)一模一樣的單呢?
所以,需要客戶端在請求下單接口的時(shí)候,需要生成一個(gè)唯一的請求號(hào):requestId,服務(wù)端拿這個(gè)請求號(hào),判斷是否重復(fù)請求。
那么,接下來,壓力就給到服務(wù)端了,看看服務(wù)端怎么實(shí)現(xiàn)防重/冪等吧!
利用數(shù)據(jù)庫實(shí)現(xiàn)冪等
可以在訂單表t_order里添加一個(gè)字段:requestId,添加唯一索引:
唯一請求字段
這樣一來,如果是重復(fù)的請求,在落庫的時(shí)候就會(huì)報(bào)錯(cuò),為了保證冪等性,我們可以catch住這個(gè)異常,根據(jù)requestId獲取訂單號(hào),然后向客戶端響應(yīng)訂單號(hào)。
大概的代碼如下:
PlaceOrderResVO placeOrder(PlaceOrderReqVO reqVO) {
try {
//下單業(yè)務(wù)邏輯
……
//生成訂單號(hào)
String oid=generateOid();
……
//訂單落庫
Order order = orderMApper.saveOrder(orderDO);
//響應(yīng)訂單
resVO.setOid(order.getOid());
return resVO;
} catch(UniqueKeyViolationException e) {
// 發(fā)生了重復(fù)異常
// 根據(jù)請求號(hào)獲取訂單
Order order = getOrderByRequestId(reqVO.getRequestId());
resVO.setOid(order.getOid());
return resVO;
} catch (Exception e) {
}
}
當(dāng)然,這里不太好的地方是,拿異常來做業(yè)務(wù)判斷。
利用redis防重
另外一個(gè)辦法,就是下單請求的時(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ù)邏輯
……
//落庫
//訂單落庫
Order order = orderMapper.saveOrder(orderDO);
……
//緩存結(jié)果
orderCache.put(resVO);
return resVO;
}
} catch (Exception e) {
//……
} finally {
orderLock.unlock();
}
return resVO;
}
這里再說明一下:
- 為什么獲取不到鎖的時(shí)候要拋異常呢?
因?yàn)橄聠卫锩嫫鋵?shí)還有一些其它的業(yè)務(wù)流程,比如鎖庫存、清優(yōu)惠券……而此時(shí),獲取到鎖的請求的下單流程還沒有結(jié)束,下單的結(jié)果還獲取不到,沒法完成響應(yīng),也就沒辦法做冪等。
客戶端,也可以根據(jù)響應(yīng)的狀態(tài)碼,進(jìn)行特殊處理,比如這個(gè)異常先不提示,但是允許用戶再次點(diǎn)擊下單按鈕,來提升用戶的體驗(yàn)。
原文鏈接:
https://mp.weixin.qq.com/s/Dc_4taB6Boojdw_0mngroQ作者:三分惡






