電子交易的一個(gè)很基本的問題,就是避免用戶下重復(fù)訂單。用戶明明想買一次,結(jié)果一看下了兩個(gè)單。如果沒有及時(shí)發(fā)現(xiàn),就會(huì)帶來額外的物流成本和扯皮。對(duì)商家的信譽(yù)也不好看。
從技術(shù)上看,這是一個(gè)分布式一致性問題;但實(shí)際上,技術(shù)無法100%解決這類問題,得結(jié)合多種手段綜合處理。這里就來說道說道。
為啥會(huì)下重了呢?
原因1:客戶端bug
比如下單的按鍵在點(diǎn)按之后,在沒有收到服務(wù)器請(qǐng)求之前,按鍵的狀態(tài)沒有設(shè)為已禁用狀態(tài),還可以被按。又或者,在觸摸屏下,用戶手指的點(diǎn)按可能被手機(jī)操作系統(tǒng)識(shí)別為多次點(diǎn)擊。
嗯,誰能保證客戶端不偶爾出個(gè)什么bug 呢。
原因2: 超時(shí)
用戶的設(shè)備與服務(wù)器之間可能是不穩(wěn)定的網(wǎng)路。這樣一個(gè)下單請(qǐng)求過去,返回不一定回得來。超時(shí)最大的問題是: 從用戶的角度,他無法確定下單的請(qǐng)求是還沒到服務(wù)器,還是已經(jīng)到了服務(wù)器但是返回丟失了。——用戶無法區(qū)分到底這個(gè)單下了還是沒下。
這樣在等待一個(gè)超時(shí)后,UI可能會(huì)提示用戶下單超時(shí),請(qǐng)重復(fù)再試。
原因3: 用戶的App閃退/人工強(qiáng)退,之后重新打開重新下單
也許可以使用一些技術(shù)手段避免用戶下重單,但是心急的用戶可能會(huì)重啟流程/重啟App/重啟手機(jī)。在這種強(qiáng)制的手段下,任何技術(shù)手段都會(huì)失效——用戶壓根就不讓你的技術(shù)執(zhí)行,你怎么玩?
在這些條件下,如何避免用戶多下了一筆訂單呢?
用冪等防止重復(fù)訂單
在技術(shù)方面,這是一個(gè)分布式一致性的問題,即客戶端和服務(wù)器端對(duì)某個(gè)訂單是否成功/失敗達(dá)成一致。防止重單的關(guān)鍵是使用一個(gè)由客戶端生成的,可用于避免重復(fù)的key,俗稱dedup key(deduplicate key之意)。這個(gè)key可以用任意可以保證全局唯一性的方式生成,比如uuid。客戶端和服務(wù)器需要使用這個(gè)dedup key作為串聯(lián)條件,一起解決去重問題。
客戶端的流程
客戶端需要實(shí)現(xiàn)這樣一個(gè)下單界面。用戶點(diǎn)擊【確認(rèn)下單】時(shí),應(yīng)該產(chǎn)生一個(gè)獨(dú)一無二的dedup key,連定訂單數(shù)據(jù)發(fā)送給服務(wù)器端。在服務(wù)器返回之前,該界面應(yīng)該一直等待,直到服務(wù)器響應(yīng)成功/失敗或者超時(shí)發(fā)生(比如15秒后,收不到服務(wù)器響應(yīng))。如果超時(shí)發(fā)生,應(yīng)該向用戶提示是否重試下單或者退出該界面。當(dāng)用戶點(diǎn)擊【重試】時(shí),應(yīng)該用剛剛生成的dedup key來再次發(fā)送下單請(qǐng)求——如果用戶一直不退出這個(gè)流程,每次用戶點(diǎn)擊重試,都應(yīng)該用這個(gè)dedup key來重試下單,直到服務(wù)器正常返回,或者用戶放棄返回。
下單的客戶端流程
后端數(shù)據(jù)表設(shè)計(jì)
后端在訂單數(shù)據(jù)表中,需要增加dedup_key這列,并設(shè)置唯一約束。
create table order(
# ...
dedup_key varchar(60) not null comment 'key to pretend order duplication',
# ...
unique uniq_dedup_key(dedup_key)
);
下單的實(shí)現(xiàn)
在實(shí)現(xiàn)下單邏輯時(shí),基于該dedup_key實(shí)現(xiàn)一個(gè)"create-or-get"語義的下單接口——簡(jiǎn)單說就是
如果帶有指定dedup_key的訂單已經(jīng)存在,則直接返回;否則,用該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 ...
}
}
這時(shí),這段下單代碼總是能返回一個(gè)訂單(除非發(fā)生一些DB掛了之類的錯(cuò)誤),要么是新創(chuàng)建的,要么就是一個(gè)已經(jīng)存在的單。注意,最好在訂單里增加一個(gè)屬性(比如例子中用“duplicated”)來表示這個(gè)訂單是這次新生成的,還是因?yàn)閮绲榷苯臃祷氐?/strong>。這樣前端可以有針對(duì)性的對(duì)這兩種情況提示不同的文案。
技術(shù)搞定冪等就足夠了嗎?
上面的流程沒有考慮一種情況,就是用戶中途強(qiáng)制退出客戶端,或者直接點(diǎn)擊【返回】回到產(chǎn)品頁,重新走下單流程。這個(gè)時(shí)候客戶端就無法判斷用戶到底是想重新下單,還是想第二次下單。此時(shí),可以從產(chǎn)品設(shè)計(jì)上考慮一下。
比如,在客戶端緩存一個(gè)表,記錄所有沒有確認(rèn)結(jié)果的訂單。
產(chǎn)品代碼 產(chǎn)品數(shù)量 金額 dedup key 未確認(rèn)訂單1 AAA 1 1000 xxx-yyy-zzz 未確認(rèn)訂單2 BBB 2 500.00 Aaa-bbb-ccc ... 通過這個(gè)表,我們可以猜一下用戶的意圖。比如,如果用戶重新提交了一筆訂單,其產(chǎn)品代碼、金額與表中記錄的某條完全一致,就可以提示一下用戶:
提示一下用戶是不是下重了
如果用戶想重試,可以繼續(xù)用表中對(duì)應(yīng)記錄的dedup key重新發(fā)起下單。
這樣不是絕對(duì)準(zhǔn)確的,僅僅是盡量的減少用戶誤操作的可能性。當(dāng)然,在產(chǎn)品設(shè)計(jì)上可以能出于用戶交互簡(jiǎn)化,不一定真的會(huì)這樣做。這就需要其他機(jī)制來配合,比如“通知”。
通知
一旦服務(wù)器下單成功,可以通過某種通知機(jī)制(如APNS、Websocket)主動(dòng)將訂單推送至客戶端,強(qiáng)行讓客戶端重新拉取最新的訂單信息,并配合“未確認(rèn)訂單”表,以通知Badge/彈框等方式提示用戶剛剛一筆狀態(tài)未知的訂單成功/失敗了。
另外一種手段就是,服務(wù)器端實(shí)時(shí)掃描用戶的下單數(shù)據(jù),一旦發(fā)現(xiàn)可能的重單,就立刻通知客服主動(dòng)聯(lián)系用戶,及時(shí)處理問題。
如果還攔不住……
經(jīng)過層層阻攔,可能還是會(huì)有用戶誤操作,直到收到兩份商品才發(fā)現(xiàn)下重了。此時(shí)就得依靠運(yùn)營(yíng)/客服的支持了。提供用戶申訴的手段,讓用戶提出哪些訂單是重復(fù)的,并且由銷售系統(tǒng)店家、商品提供者和買家三方共同根據(jù)用戶操作的記錄來協(xié)商如何處理。我們需要讓技術(shù)幫助讓這種人工處理的幾率盡量小。因?yàn)槊看翁幚矶紩?huì)耗費(fèi)較大的人工成本,和一些運(yùn)營(yíng)費(fèi)用(比如賠款、小禮品等等)。
這么麻煩,有必要嗎?
這要分業(yè)務(wù)場(chǎng)景,對(duì)于很多電商來講可能不是必要的。因?yàn)閺挠脩粝聠蔚接唵伪粚徍颂幚磉M(jìn)入到發(fā)貨階段需要一定的時(shí)間(可能是半小時(shí)~1小時(shí)),并且一定是支付成功后才會(huì)開始進(jìn)行下一步流程。在這個(gè)時(shí)間段,用戶大概率能從網(wǎng)絡(luò)錯(cuò)誤中恢復(fù)過來,自行區(qū)分是否下重了。配合客服主動(dòng)提示,會(huì)極大的降低出問題的概率。
但是對(duì)于理財(cái)服務(wù)來說,這種去重就非常必要了。因?yàn)?/p>
- “下單+支付”。用戶購(gòu)買理財(cái)往往是“下單+支付”一起執(zhí)行,不可以單獨(dú)下單/單獨(dú)支付
- 用戶的入金可能很大。例如數(shù)萬,數(shù)十萬
- 準(zhǔn)確性丟失。如果一旦下重了,有可能影響用戶的投資資金配置的準(zhǔn)確性。
- 撤銷難。部分理財(cái)產(chǎn)品存在下單不可撤銷的問題;或者即便撤銷,資金也無法立刻回款。等到回款,可能這個(gè)購(gòu)入機(jī)會(huì)就錯(cuò)過去了。例如對(duì)于基金交易,錯(cuò)過1個(gè)交易日,價(jià)格就會(huì)發(fā)生變動(dòng)。
基于這些特性,在理財(cái)產(chǎn)品中,就要竭盡全力的去重。
結(jié)論
以上所講是處理重復(fù)訂單問題的一般方法。你可以注意到,無論多么好的技術(shù),也不可能100%的攔截所有的可能性,必須依靠技術(shù)+產(chǎn)品設(shè)計(jì)+運(yùn)營(yíng)支持的綜合手段才能解決這類問題。
另外,本文還沒涉及到關(guān)于訂單支付(支付也可能重復(fù)哦)帶來的進(jìn)一步的復(fù)雜性,也沒有討論在高并發(fā)情況下的性能優(yōu)化,僅僅討論下單本身的問題。所以可以想象一下現(xiàn)實(shí)中的交易業(yè)務(wù)比這里的說的要復(fù)雜得多。
本文介紹的原理也不僅僅適用于防止下重復(fù)訂單,而是可以應(yīng)用到任何需要“創(chuàng)建一個(gè)不應(yīng)該重復(fù)資源”的場(chǎng)景,比如“向用戶發(fā)一條通知”,“觸發(fā)一次不能重復(fù)的批處理任務(wù)!
本文為企業(yè)推廣,本網(wǎng)站不做任何建議,僅提供參考,作為信息展示!
推薦閱讀:資訊中國(guó)
網(wǎng)友評(píng)論
請(qǐng)登錄后進(jìn)行評(píng)論|
0條評(píng)論
請(qǐng)文明發(fā)言,還可以輸入140字
您的評(píng)論已經(jīng)發(fā)表成功,請(qǐng)等候?qū)徍?/p>
小提示:您要為您發(fā)表的言論后果負(fù)責(zé),請(qǐng)各位遵守法紀(jì)注意語言文明