用戶下單流程
我們從用戶瀏覽商品開始,看看用戶下單的簡要過程:

用戶下單簡要過程
- 瀏覽商品:用戶查看商品詳情
- 加購/結算:用戶可以選擇直接購買商品,也可以先加入購物車,用戶購買的這一步就是結算
- 確認下單:結算完成,就進入了下單頁面,提交訂單,這一步就會生成一個訂單,然后進入付款頁面
我們可以看到,下單是發生在結算之后,下單之后,會生成唯一的訂單號,接下來,客戶端需要用這個訂單號去完成支付。
那接下來先看看,為什么發生重復下單?
為什么會重復下單
為什么會重復下單,對于訂單服務而言,就是接到了多個下單的請求,原因可能有很多,最常見的是這兩種:
- 用戶重復提交
- 網絡原因導致的超時重試

重復下單原因
如何防止重復下單
防止用戶提交,最常規的做法,就是客戶端點擊下單之后,在收到服務端響應之前,按鈕置灰。
當然,防止重復下單,肯定不能只依靠客戶端,可能會因為一些網絡的抖動,導致仍然有重復的請求到達服務端,所以還是要在服務端做防重/冪等的處理。
PS:這里額外插入一點我對防重和冪等的理解:防重指的是防止重復提交,冪等指的是多次請求如一次,簡單說,就是防重可以給對重復請求拋異常,冪等是對重復的請求響應第一次的結果,在我們討論的這個場景里,冪等就是響應唯一的訂單號。

防重和冪等
防重第一步,需要識別請求是否重復,這一步,需要客戶端配合實現。
為什么呢?大家想一下,下單的時候,服務端怎么去判斷這個下單請求是否唯一呢?金額?商品?優惠券?……萬一用戶就是喜歡,又下了一個一模一樣的單呢?
所以,需要客戶端在請求下單接口的時候,需要生成一個唯一的請求號:requestId,服務端拿這個請求號,判斷是否重復請求。
那么,接下來,壓力就給到服務端了,看看服務端怎么實現防重/冪等吧!
利用數據庫實現冪等
可以在訂單表t_order里添加一個字段:requestId,添加唯一索引:

唯一請求字段
這樣一來,如果是重復的請求,在落庫的時候就會報錯,為了保證冪等性,我們可以catch住這個異常,根據requestId獲取訂單號,然后向客戶端響應訂單號。
大概的代碼如下:
PlaceOrderResVO placeOrder(PlaceOrderReqVO reqVO) {
try {
//下單業務邏輯
……
//生成訂單號
String oid=generateOid();
……
//訂單落庫
Order order = orderMApper.saveOrder(orderDO);
//響應訂單
resVO.setOid(order.getOid());
return resVO;
} catch(UniqueKeyViolationException e) {
// 發生了重復異常
// 根據請求號獲取訂單
Order order = getOrderByRequestId(reqVO.getRequestId());
resVO.setOid(order.getOid());
return resVO;
} catch (Exception e) {
}
}
當然,這里不太好的地方是,拿異常來做業務判斷。
利用redis防重
另外一個辦法,就是下單請求的時候要加鎖了,通常我們的服務都是集群部署,所以一般都是用Redis實現分布式鎖。
大概的邏輯:
- 就是以requestId為維度,進行加鎖,如果獲取鎖失敗,就拋一個自定義的重復下單異常。
- 如果獲取到鎖,先check一下,是否已經下單,為了提高性能,下單完成后,也把下單的結果放在Redis緩存里。

redis防重邏輯
大概的代碼如下:
public PlaceOrderResVO placeOrder(PlaceOrderReqVO reqVO) {
//加鎖
RLock orderLock = redissonClient.getLock(RedisConstant.PLACE_ORDER_LOCK_KEY + reqVO.getRequestId());
//獲取鎖失敗,拋出重復下單異常
if(orderLock.isExistes){
throw new OrderRepeatException();
}
// 加鎖
orderLock.lock();
try {
//檢查是否已經下單
RBucket<PlaceOrderResVO> orderCache = redissonClient.getBucket(RedisConstant.PLACE_ORDER_LOCK_KEY+reqVO.getRequestId());
if(orderCache.isExistes){
return orderCache.get();
}
//下單業務邏輯
……
//落庫
//訂單落庫
Order order = orderMapper.saveOrder(orderDO);
……
//緩存結果
orderCache.put(resVO);
return resVO;
}
} catch (Exception e) {
//……
} finally {
orderLock.unlock();
}
return resVO;
}
這里再說明一下:
- 為什么獲取不到鎖的時候要拋異常呢?
因為下單里面其實還有一些其它的業務流程,比如鎖庫存、清優惠券……而此時,獲取到鎖的請求的下單流程還沒有結束,下單的結果還獲取不到,沒法完成響應,也就沒辦法做冪等。
客戶端,也可以根據響應的狀態碼,進行特殊處理,比如這個異常先不提示,但是允許用戶再次點擊下單按鈕,來提升用戶的體驗。
原文鏈接:
https://mp.weixin.qq.com/s/Dc_4taB6Boojdw_0mngroQ作者:三分惡