電子交易的一個很基本的問題,就是避免用戶下重複訂單。用戶明明想買一次,結果一看下了兩個單。如果沒有及時發現,就會帶來額外的物流成本和扯皮。對商家的信譽也不好看。

從技術上看,這是一個分散式一致性問題;但實際上,技術無法100%解決這類問題,得結合多種手段綜合處理。這裡就來說道說道。

為啥會下重了呢?

原因1:客戶端bug

比如下單的按鍵在點按之後,在沒有收到伺服器請求之前,按鍵的狀態沒有設為已禁用狀態,還可以被按。又或者,在觸摸屏下,用戶手指的點按可能被手機操作系統識別為多次點擊。

嗯,誰能保證客戶端不偶爾出個什麼bug 呢。

原因2: 超時

用戶的設備與伺服器之間可能是不穩定的網路。這樣一個下單請求過去,返回不一定回得來。超時最大的問題是: 從用戶的角度,他無法確定下單的請求是還沒到伺服器,還是已經到了伺服器但是返回丟失了。——用戶無法區分到底這個單下了還是沒下

這樣在等待一個超時後,UI可能會提示用戶下單超時,請重複再試。

原因3: 用戶的App閃退/人工強退,之後重新打開重新下單

也許可以使用一些技術手段避免用戶下重單,但是心急的用戶可能會重啟流程/重啟App/重啟手機。在這種強制的手段下,任何技術手段都會失效——用戶壓根就不讓你的技術執行,你怎麼玩?

在這些條件下,如何避免用戶多下了一筆訂單呢?

用冪等防止重複訂單

在技術方面,這是一個分散式一致性的問題,即客戶端和伺服器端對某個訂單是否成功/失敗達成一致。防止重單的關鍵是使用一個由客戶端生成的,可用於避免重複的key,俗稱dedup key(deduplicate key之意)。這個key可以用任意可以保證全局唯一性的方式生成,比如uuid。客戶端和伺服器需要使用這個dedup key作為串聯條件,一起解決去重問題。

客戶端的流程

客戶端需要實現這樣一個下單界面。用戶點擊【確認下單】時,應該產生一個獨一無二的dedup key,連定訂單數據發送給伺服器端。在伺服器返回之前,該界面應該一直等待,直到伺服器響應成功/失敗或者超時發生(比如15秒後,收不到伺服器響應)。如果超時發生,應該向用戶提示是否重試下單或者退出該界面。當用戶點擊【重試】時,應該用剛剛生成的dedup key來再次發送下單請求——如果用戶一直不退出這個流程,每次用戶點擊重試,都應該用這個dedup key來重試下單,直到伺服器正常返回,或者用戶放棄返回。

下單的客戶端流程

後端數據表設計

後端在訂單數據表中,需要增加dedup_key這列,並設置唯一約束

create table order(
# ...
dedup_key varchar(60) not null comment key to pretend order duplication,
# ...
unique uniq_dedup_key(dedup_key)
);

下單的實現

在實現下單邏輯時,基於該dedup_key實現一個"create-or-get"語義的下單介面——簡單說就是

如果帶有指定dedup_key的訂單已經存在,則直接返回;否則,用該dedup_key下單。

用偽代碼表示大概是:

@Transactional
Order createOrder(Integer userId, String prodCode, Decimal amount, String dedupKey) {
try {
String orderId = createOrder(userId, prodCode, amount, deupKey); // insert a new order
Order order = getOrderById(orderId); // read order from db
order.setDuplicated(false);
return order;
} catch(UniqueKeyViolationException e) {
// if duplicated order has existed
Order order = getOrderByDedupKey(dedupKey);
order.setDuplicated(true);
return order;
} catch (Exception e) {
// hanlde other errors and rollback transaction ...
}
}

這時,這段下單代碼總是能返回一個訂單(除非發生一些DB掛了之類的錯誤),要麼是新創建的,要麼就是一個已經存在的單。注意,最好在訂單里增加一個屬性(比如例子中用「duplicated」)來表示這個訂單是這次新生成的,還是因為冪等而直接返回的。這樣前端可以有針對性的對這兩種情況提示不同的文案。

技術搞定冪等就足夠了嗎?

上面的流程沒有考慮一種情況,就是用戶中途強制退出客戶端,或者直接點擊【返回】回到產品頁,重新走下單流程。這個時候客戶端就無法判斷用戶到底是想重新下單,還是想第二次下單。此時,可以從產品設計上考慮一下。

比如,在客戶端緩存一個表,記錄所有沒有確認結果的訂單。

產品代碼 產品數量 金額 dedup key 未確認訂單1 AAA 1 1000 xxx-yyy-zzz 未確認訂單2 BBB 2 500.00 Aaa-bbb-ccc ... 通過這個表,我們可以一下用戶的意圖。比如,如果用戶重新提交了一筆訂單,其產品代碼、金額與表中記錄的某條完全一致,就可以提示一下用戶:

提示一下用戶是不是下重了

如果用戶想重試,可以繼續用表中對應記錄的dedup key重新發起下單。

這樣不是絕對準確的,僅僅是盡量的減少用戶誤操作的可能性。當然,在產品設計上可以能出於用戶交互簡化,不一定真的會這樣做。這就需要其他機制來配合,比如「通知」。

通知

一旦伺服器下單成功,可以通過某種通知機制(如APNS、Websocket)主動將訂單推送至客戶端,強行讓客戶端重新拉取最新的訂單信息,並配合「未確認訂單」表,以通知Badge/彈框等方式提示用戶剛剛一筆狀態未知的訂單成功/失敗了。

另外一種手段就是,伺服器端實時掃描用戶的下單數據,一旦發現可能的重單,就立刻通知客服主動聯繫用戶,及時處理問題。

如果還攔不住……

經過層層阻攔,可能還是會有用戶誤操作,直到收到兩份商品才發現下重了。此時就得依靠運營/客服的支持了。提供用戶申訴的手段,讓用戶提出哪些訂單是重複的,並且由銷售系統店家、商品提供者和買家三方共同根據用戶操作的記錄來協商如何處理。我們需要讓技術幫助讓這種人工處理的幾率盡量小。因為每次處理都會耗費較大的人工成本,和一些運營費用(比如賠款、小禮品等等)。

這麼麻煩,有必要嗎?

這要分業務場景,對於很多電商來講可能不是必要的。因為從用戶下單到訂單被審核處理進入到發貨階段需要一定的時間(可能是半小時~1小時),並且一定是支付成功後才會開始進行下一步流程。在這個時間段,用戶大概率能從網路錯誤中恢復過來,自行區分是否下重了。配合客服主動提示,會極大的降低出問題的概率。

但是對於理財服務來說,這種去重就非常必要了。因為

  • 「下單+支付」。用戶購買理財往往是「下單+支付」一起執行,不可以單獨下單/單獨支付
  • 用戶的入金可能很大。例如數萬,數十萬
  • 準確性丟失。如果一旦下重了,有可能影響用戶的投資資金配置的準確性。
  • 撤銷難。部分理財產品存在下單不可撤銷的問題;或者即便撤銷,資金也無法立刻回款。等到回款,可能這個購入機會就錯過去了。例如對於基金交易,錯過1個交易日,價格就會發生變動。

基於這些特性,在理財產品中,就要竭盡全力的去重。

結論

以上所講是處理重複訂單問題的一般方法。你可以注意到,無論多麼好的技術,也不可能100%的攔截所有的可能性,必須依靠技術+產品設計+運營支持的綜合手段才能解決這類問題。

另外,本文還沒涉及到關於訂單支付(支付也可能重複哦)帶來的進一步的複雜性,也沒有討論在高並發情況下的性能優化,僅僅討論下單本身的問題。所以可以想像一下現實中的交易業務比這裡的說的要複雜得多。

本文介紹的原理也不僅僅適用於防止下重複訂單,而是可以應用到任何需要「創建一個不應該重複資源」的場景,比如「向用戶發一條通知」,「觸發一次不能重複的批處理任務!

最後

希望你們看了能夠有所收穫,同時覺得不錯的朋友可以點贊和關注下我,以後還會有更多精選文章分享給大家!

同時可以這個知乎專欄,裡面是更多Java進階技術交流和學習資源分享以及大佬面經分享!

java架構技術交流?

zhuanlan.zhihu.com
圖標

推薦閱讀:
相关文章