如何防止訂單重復(fù)支付?
大家好,我是磊哥,想必大家對(duì)在線支付都不陌生,今天和大家聊聊如何防止訂單重復(fù)支付。
看看訂單支付流程
我們來(lái)看看,電商訂單支付的簡(jiǎn)要流程:

從下單/計(jì)算開(kāi)始:
下單/結(jié)算:這一步雖然不是直接的支付起點(diǎn),但是支付相關(guān)的金額等等信息都來(lái)自結(jié)算,此時(shí)訂單的狀態(tài)是未支付
申請(qǐng)支付:用戶選擇申請(qǐng)支付,客戶端調(diào)用支付服務(wù),此時(shí)在系統(tǒng)內(nèi)產(chǎn)生一筆支付流水,這筆流水的狀態(tài)是未支付
發(fā)起支付:支付服務(wù)調(diào)用三方支付,通常這種錢包類的支付,在發(fā)起支付這一步,會(huì)響應(yīng)一些支付的鏈接,客戶端會(huì)對(duì)鏈接進(jìn)行對(duì)應(yīng)的處理。
錢包支付:用戶進(jìn)行支付,通常是通過(guò)對(duì)應(yīng)的錢包進(jìn)行的,大家可以回憶一下自己在購(gòu)物中,支付的過(guò)程,不同的端,對(duì)錢包支付的處理是不太一樣的:

京東PC端支付頁(yè) APP端: 在國(guó)內(nèi),購(gòu)物大部分都是在APP端,產(chǎn)品經(jīng)理會(huì)想法設(shè)法把用戶帶到APP,為什么我的示例圖都用京東,不用淘寶呢?因?yàn)槲夷肬C打開(kāi)淘寶,會(huì)直接跳轉(zhuǎn)APP。APP端的錢包支付,我們應(yīng)該都非常熟悉,一般是拉起錢包,支付。

APP支付 WAP端:手機(jī)的網(wǎng)頁(yè)站,WAP端的支付一般是直接拉起對(duì)應(yīng)的錢包,如果拉起錢包失敗,就跳轉(zhuǎn)界面
京東支付WAP端 PC端:PC端,通常是打開(kāi)收銀臺(tái),展示一個(gè)二維碼,通過(guò)錢包掃碼支付,下面是京東的微信支付掃碼頁(yè)支付回調(diào):用戶完成支付后,三方支付平臺(tái),會(huì)回調(diào)商戶,通知支付結(jié)果。
同步訂單狀態(tài):支付服務(wù)在確認(rèn)支付完成后,會(huì)向訂單服務(wù)同步支付的結(jié)果,訂單服務(wù)變更訂單的狀態(tài),由未支付-》待發(fā)貨,客戶端通過(guò)輪詢、長(zhǎng)連接,或者服務(wù)端主動(dòng)推送的方式,在界面上變更訂單狀態(tài)。
我們?cè)購(gòu)闹Ц读魉慕嵌瓤匆幌轮Ц稜顟B(tài)的變化:

從未支付,到有支付結(jié)果的終態(tài),中間還有一個(gè)中間狀態(tài) 支付中用戶通過(guò)打開(kāi)錢包--》完成支付--》支付回調(diào),這段時(shí)間的支付流水就處于 支付中
為什么要花這么多篇幅來(lái)講支付的業(yè)務(wù)流程、交互過(guò)程呢?因?yàn)槲艺J(rèn)為,防止訂單的重復(fù)支付,不止是技術(shù)上的問(wèn)題,也是業(yè)務(wù)和產(chǎn)品上的問(wèn)題。
為什么訂單會(huì)重復(fù)支付
未防重導(dǎo)致的重復(fù)支付
我們可以看到PC端支付,是掃描二維碼,這些二維碼,就是對(duì)應(yīng)相應(yīng)的支付流水,假如用戶重復(fù)點(diǎn)擊支付,如果不做防重的的話,會(huì)生成兩筆支付流水,也就是兩個(gè)不同的二維碼,要是用戶分別掃了兩個(gè)不同的支付碼,那么毫無(wú)疑問(wèn),就會(huì)產(chǎn)生重復(fù)支付。
掉單導(dǎo)致的重復(fù)支付
“我明明付款了,為什么我的訂單還沒(méi)支付呢?”

這就是所謂的“掉單”:
外部掉單:三方支付的支付狀態(tài)沒(méi)有同步或者沒(méi)有及時(shí)同步到商城,這叫外部掉單 內(nèi)部掉單:支付服務(wù)的狀態(tài)沒(méi)有同步到訂單,或者客戶端沒(méi)有及時(shí)獲取到訂單狀態(tài),這叫內(nèi)部掉單。
用戶一看,自己付了款,結(jié)果商城里訂單還未付款,但是又特別想要,可能就會(huì)再下一單,這樣就重復(fù)支付了。
多渠道導(dǎo)致的重復(fù)支付
我們國(guó)內(nèi)支付的體驗(yàn)還是非??旖莸?,大家可能沒(méi)有感覺(jué),如果了解過(guò)海外支付的可能了解,很多支付的渠道,消耗的時(shí)間非常長(zhǎng)。
比如用戶保羅選擇了一種支付方式Boleto,結(jié)果支付的網(wǎng)點(diǎn)離保羅他們村太遠(yuǎn)了,保羅又選擇了Paypal支付,保羅去趕集的時(shí)候,又順手去網(wǎng)點(diǎn)把Boleto的這一筆支付了,結(jié)果就重復(fù)支付了。
這種情況大家可能很少遇到,我們可以用美團(tuán)下一個(gè)單,先打開(kāi)微信支付,不要支付啊,接著回到美團(tuán),打開(kāi)支付寶,用支付寶支付完成后,用微信接著支付,大家猜猜,兩筆支付是不是都能成功?答案是可以。

如何防止訂單重復(fù)支付
加鎖
不管是3.申請(qǐng)支付、還是5.支付回調(diào),都應(yīng)該以訂單維度加鎖,防止并發(fā)下的重復(fù)操作。
加鎖,毫無(wú)疑問(wèn),也是分布式鎖,通常我們會(huì)選擇Redis分布式鎖。

緩存結(jié)果
申請(qǐng)支付成功,支付回調(diào)成功,都應(yīng)該緩存結(jié)果。
再申請(qǐng)支付,收到成功回調(diào)的時(shí)候,都應(yīng)該先去檢查支付的狀態(tài)。

支付中流水取消
假如說(shuō),用戶重復(fù)支付了,再次申請(qǐng)支付的時(shí)候,如果已經(jīng)申請(qǐng)支付成功了,那么這筆支付肯定是要拒絕的。
但是,要是已經(jīng)存在的這筆流水還在支付中呢?——我們不確定它是成功還是失敗,肯定是不能拒絕支付的,因?yàn)榭赡苡脩糁Ц妒×耍菭顟B(tài)還沒(méi)同步,這樣肯定是不行的。
所以,我們可以取消掉正在支付中的流水,再進(jìn)行支付。

已支付流水退款
現(xiàn)在又有新的問(wèn)題了,假如發(fā)起支付的時(shí)候,有流水正在支付中,如果第三方支付平臺(tái)不支持取消支付,或者用戶新的支付是通過(guò)不同的渠道,我們希望盡可能提高用戶的支付成功率,怎么辦呢?
我們可以在發(fā)起支付的時(shí)候,訂單還在支付中的情況下,允許用戶發(fā)起多筆支付,在支付回調(diào)的時(shí)候,檢查用戶是否已經(jīng)有成功流水,對(duì)后來(lái)的流水進(jìn)行退款處理。

當(dāng)然,退款是個(gè)很危險(xiǎn)的操作,畢竟錢退了,可就很難追回來(lái),一定要做好風(fēng)險(xiǎn)的控制。
主動(dòng)輪詢&重試防止掉單
主動(dòng)輪詢防止外部掉單
如果因?yàn)楣收蠜](méi)有收到回調(diào),或者沒(méi)有及時(shí)收到回調(diào),就可能會(huì)發(fā)生所謂的外部掉單。
防止外部掉單的關(guān)鍵,就在于,不能傻傻地只等三方的回調(diào)通知,而要主動(dòng)去查詢,用戶發(fā)起支付的3s之后,就可以發(fā)起輪詢了,直到拿到支付流水的最終狀態(tài),主動(dòng)輪詢,一般可以這么實(shí)現(xiàn):

定時(shí)任務(wù)輪詢
使用定時(shí)任務(wù),掃描表中支付中的流水,主動(dòng)查詢支付的狀態(tài),定時(shí)任務(wù)的實(shí)現(xiàn)方式有很多,線程池、調(diào)度框架、分布式調(diào)度框架等等。
定時(shí)任務(wù)輪詢的缺點(diǎn)有兩個(gè):
對(duì)數(shù)據(jù)庫(kù)有一些壓力,觀察監(jiān)控,會(huì)發(fā)現(xiàn)定時(shí)任務(wù)掃表的時(shí)候,有時(shí)候會(huì)造成數(shù)據(jù)庫(kù)的一些“峰刺” 不便調(diào)整頻率,實(shí)際上,用戶發(fā)起一筆支付之后,一般都會(huì)在10s-1min中完成支付,越往后,用戶完成支付,所以輪詢梯度進(jìn)行,會(huì)更合理一些,輪詢的間隔可以設(shè)置成類似這種:3s,10s,30s,3min…… 延時(shí)消息輪詢
另外一種方式就是使用延時(shí)消息,用戶發(fā)起支付之后,發(fā)送一個(gè)延時(shí)消息,消費(fèi)到延時(shí)消息之后,查詢流水支付狀態(tài),沒(méi)有拿到最終狀態(tài),就再發(fā)一個(gè)延時(shí)消息。延時(shí)消息的好處是對(duì)數(shù)據(jù)庫(kù)的壓力沒(méi)有那么大,輪詢的梯度也可以進(jìn)行控制,缺點(diǎn)是實(shí)現(xiàn)起來(lái)復(fù)雜一些,而且要維護(hù)消息隊(duì)列。
同步+異步防止內(nèi)部掉單
支付服務(wù)在收到異步通知回調(diào)、或者主動(dòng)輪詢到流水的最終狀態(tài)后,要通知訂單服務(wù)支付流水的變化,訂單服務(wù)同步更新訂單的狀態(tài),這個(gè)過(guò)程要盡可能保證通知成功,可以采用同步+異步的方式。
同步調(diào)用:支付服務(wù)調(diào)用訂單服務(wù)的通知接口,有可能會(huì)因?yàn)榫W(wǎng)絡(luò)等等的原因失敗,也可以重試,但是根據(jù)經(jīng)驗(yàn),如果網(wǎng)絡(luò)出現(xiàn)一些波動(dòng),重試很可能也會(huì)失敗。 異步通知:支付服務(wù)還應(yīng)該發(fā)送一個(gè)支付成功的消息,訂單服務(wù)可以利用消息隊(duì)列的重試機(jī)制,來(lái)盡可能保證支付狀態(tài)的同步。
這里還有一個(gè)問(wèn)題,客戶端如何同步這個(gè)狀態(tài)?因?yàn)榭赡芊?wù)端更新了訂單狀態(tài),但是客戶端的界面上還是未支付,得用戶主動(dòng)刷新一下,才能拿到最新的狀態(tài),這樣明顯是不太合適的。
服務(wù)端、客戶端的狀態(tài)同步,無(wú)非就拉和推:
拉:很簡(jiǎn)單,就是客戶端在用戶跳回訂單狀態(tài)頁(yè)的時(shí)候,輪詢一會(huì),如果用戶完成支付,通常很短時(shí)間就能獲取到狀態(tài)的變更,當(dāng)然這種方式對(duì)客戶端的性能會(huì)有一些影響,而且很出現(xiàn)狀態(tài)同步“漏網(wǎng)之魚(yú)”的情況。 推:推的實(shí)現(xiàn)有些麻煩,Web通常是用Websocket,對(duì)APP端的推送,一般采用第三方的推送平臺(tái)。
客戶端支付盡可能不外跳
不管從產(chǎn)品的角度,還是技術(shù)的角度,客戶端發(fā)起支付這一步,其實(shí)應(yīng)該盡可能地不要外跳,PC端使用支付服務(wù)生成的支付碼,而不是跳轉(zhuǎn);移動(dòng)端網(wǎng)頁(yè)、APP在應(yīng)用內(nèi)展示支付頁(yè),當(dāng)然這個(gè)是由第三方支付平臺(tái)決定的。

不知道大家留意到了沒(méi)有,現(xiàn)在的支付寶,已經(jīng)做到了不用拉起錢包,在應(yīng)用內(nèi)就可以完成支付,這個(gè)對(duì)于商家的意義還是比較大的,對(duì)用戶體驗(yàn)、支付成功率,都有正面的作用,相信以國(guó)內(nèi)的內(nèi)卷程度,其它支付供應(yīng)商,一定會(huì)“跟進(jìn)”的。
好了,關(guān)于如何防止重復(fù)支付,就講到這里。對(duì)于支付,老三也只是初窺門徑,希望各位大佬不吝指教。
參考:
[1]. 服務(wù)端如何防止重復(fù)支付
