電子交易的一個(gè)很基本的問題囚戚,就是避免用戶下重復(fù)訂單。用戶明明想買一次轧简,結(jié)果一看下了兩個(gè)單驰坊。如果沒有及時(shí)發(fā)現(xiàn),就會帶來額外的物流成本和扯皮哮独。對商家的信譽(yù)也不好看拳芙。
從技術(shù)上看,這是一個(gè)分布式一致性問題皮璧;但實(shí)際上舟扎,技術(shù)無法100%解決這類問題,得結(jié)合多種手段綜合處理恶导。這里就來說道說道浆竭。
為啥會下重了呢?
- 原因1:客戶端bug
比如下單的按鍵在點(diǎn)按之后惨寿,在沒有收到服務(wù)器請求之前邦泄,按鍵的狀態(tài)沒有設(shè)為已禁用狀態(tài),還可以被按裂垦。又或者顺囊,在觸摸屏下,用戶手指的點(diǎn)按可能被手機(jī)操作系統(tǒng)識別為多次點(diǎn)擊蕉拢。
嗯特碳,誰能保證客戶端不偶爾出個(gè)什么bug 呢。
- 原因2: 超時(shí)
用戶的設(shè)備與服務(wù)器之間可能是不穩(wěn)定的網(wǎng)路晕换。這樣一個(gè)下單請求過去午乓,返回不一定回得來。超時(shí)最大的問題是: 從用戶的角度闸准,他無法確定下單的請求是還沒到服務(wù)器益愈,還是已經(jīng)到了服務(wù)器但是返回丟失了∫募遥——用戶無法區(qū)分到底這個(gè)單下了還是沒下蒸其。
這樣在等待一個(gè)超時(shí)后,UI可能會提示用戶下單超時(shí)库快,請重復(fù)再試摸袁。
- 原因3: 用戶的App閃退/人工強(qiáng)退,之后重新打開重新下單
也許可以使用一些技術(shù)手段避免用戶下重單义屏,但是心急的用戶可能會重啟流程/重啟App/重啟手機(jī)靠汁。在這種強(qiáng)制的手段下蜂大,任何技術(shù)手段都會失效——用戶壓根就不讓你的技術(shù)執(zhí)行,你怎么玩蝶怔?
在這些條件下县爬,如何避免用戶多下了一筆訂單呢?
用冪等防止重復(fù)訂單
在技術(shù)方面添谊,這是一個(gè)分布式一致性的問題,即客戶端和服務(wù)器端對某個(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ā)送下單請求——如果用戶一直不退出這個(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"語義的下單接口——簡單說就是
如果帶有指定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>。這樣前端可以有針對性的對這兩種情況提示不同的文案寡润。
技術(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ù)用表中對應(yīng)記錄的dedup key重新發(fā)起下單。
這樣不是絕對準(zhǔn)確的茧痒,僅僅是盡量的減少用戶誤操作的可能性肮韧。當(dāng)然,在產(chǎn)品設(shè)計(jì)上可以能出于用戶交互簡化旺订,不一定真的會這樣做弄企。這就需要其他機(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)過層層阻攔,可能還是會有用戶誤操作欠啤,直到收到兩份商品才發(fā)現(xiàn)下重了鳍怨。此時(shí)就得依靠運(yùn)營/客服的支持了。提供用戶申訴的手段跪妥,讓用戶提出哪些訂單是重復(fù)的,并且由銷售系統(tǒng)店家声滥、商品提供者和買家三方共同根據(jù)用戶操作的記錄來協(xié)商如何處理眉撵。我們需要讓技術(shù)幫助讓這種人工處理的幾率盡量小。因?yàn)槊看翁幚矶紩馁M(fèi)較大的人工成本落塑,和一些運(yùn)營費(fèi)用(比如賠款纽疟、小禮品等等)。
這么麻煩憾赁,有必要嗎污朽?
這要分業(yè)務(wù)場景,對于很多電商來講可能不是必要的龙考。因?yàn)閺挠脩粝聠蔚接唵伪粚徍颂幚磉M(jìn)入到發(fā)貨階段需要一定的時(shí)間(可能是半小時(shí)~1小時(shí))蟆肆,并且一定是支付成功后才會開始進(jìn)行下一步流程。在這個(gè)時(shí)間段晦款,用戶大概率能從網(wǎng)絡(luò)錯(cuò)誤中恢復(fù)過來炎功,自行區(qū)分是否下重了。配合客服主動(dòng)提示缓溅,會極大的降低出問題的概率蛇损。
但是對于理財(cái)服務(wù)來說,這種去重就非常必要了坛怪。因?yàn)?/p>
- “下單+支付”淤齐。用戶購買理財(cái)往往是“下單+支付”一起執(zhí)行,不可以單獨(dú)下單/單獨(dú)支付
- 用戶的入金可能很大袜匿。例如數(shù)萬更啄,數(shù)十萬
- 準(zhǔn)確性丟失。如果一旦下重了居灯,有可能影響用戶的投資資金配置的準(zhǔn)確性锈死。
- 撤銷難贫堰。部分理財(cái)產(chǎn)品存在下單不可撤銷的問題;或者即便撤銷待牵,資金也無法立刻回款其屏。等到回款,可能這個(gè)購入機(jī)會就錯(cuò)過去了缨该。例如對于基金交易偎行,錯(cuò)過1個(gè)交易日,價(jià)格就會發(fā)生變動(dòng)贰拿。
基于這些特性蛤袒,在理財(cái)產(chǎn)品中,就要竭盡全力的去重膨更。
結(jié)論
以上所講是處理重復(fù)訂單問題的一般方法妙真。你可以注意到,無論多么好的技術(shù)荚守,也不可能100%的攔截所有的可能性珍德,必須依靠技術(shù)+產(chǎn)品設(shè)計(jì)+運(yùn)營支持的綜合手段才能解決這類問題。
另外矗漾,本文還沒涉及到關(guān)于訂單支付(支付也可能重復(fù)哦)帶來的進(jìn)一步的復(fù)雜性锈候,也沒有討論在高并發(fā)情況下的性能優(yōu)化,僅僅討論下單本身的問題敞贡。所以可以想象一下現(xiàn)實(shí)中的交易業(yè)務(wù)比這里的說的要復(fù)雜得多泵琳。
本文介紹的原理也不僅僅適用于防止下重復(fù)訂單,而是可以應(yīng)用到任何需要“創(chuàng)建一個(gè)不應(yīng)該重復(fù)資源”的場景誊役,比如“向用戶發(fā)一條通知”获列,“觸發(fā)一次不能重復(fù)的批處理任務(wù)“……
希望今天你有g(shù)et到:)。