天天看點

支付系統的防重設計

“目前在網際網路應用的大部分支付場景中,對接支付寶、微信移動支付産品這樣需要使用者參與支付流程的支付方式已經變得非常普遍,類似的還有PC端銀行網銀支付;而通過綁定使用者銀行卡、對接銀行卡快捷支付通道直接扣款的支付方式,雖然還在電商、保險、網際網路金融、租房等行業被廣泛應用,但是随着微信錢包、支付寶錢包這類移動網際網路支付方式的興起,使用者規模的迅速增長,再加上使用者銀行卡資訊安全、直連銀行通道關閉等因素使用者市場佔有率正在逐漸減少”。

實際上,這種需要用戶端參與支付流程的方式相比銀行卡快捷支付直接扣款這類支付方式,在支付系統的流程及訂單結構等設計上是存在較大差異的,其中訂單的防重失效機制的設計更是一個比較棘手的問題。

參與過支付系統開發或在業務系統中開發支付功能的同學可能會遇到類似這樣的業務需求:

使用者在外賣網站或App上購買了點了一份外賣,并通過微信支付進行付款,系統在收到使用者支付完成的消息後,提示使用者付款成功并派單給餐館?

初看這個問題,可能很多同學都會有疑問,這不是一個很簡單的支付流程嗎?大部分支付場景不都是這樣的麼?

其實是這樣的,作為正常的支付流程來講,上述場景并沒有什麼問題,在整個系統鍊運作穩定的情況下,可能大部分參與者并不會有什麼感覺;但是,作為一個具備專業精神的小碼農來說,還是有很多異常場景需要考慮的,不然就可能會因為系統流程上設計的缺陷而給公司和使用者體驗造成比較大的傷害。

那麼上述需求中,會有什麼樣的異常場景呢?與支付系統防重失效機制的設計有什麼關聯?

我們可以先來看一下以上場景在系統流程中的運作情況(需要放大檢視):

支付系統的防重設計

在上面的流程中,雖然從使用者角度看可能隻是幾秒鐘的事情,但實際上整個系統鍊是經曆了一個比較長的調用過程。具體如下:

● 使用者在點外賣的過程中選擇微信支付後,App會将支付請求發送給外賣背景系統,如果在整個外賣平台中,支付系統是一個獨立的系統,則外賣業務背景服務會在生成業務訂單後将支付請求發送給獨立的“支付系統”進行處理;

● 此時支付系統作為獨立的中間系統會處理外賣平台業務背景發送過來的支付請求,記錄其業務訂單号并生成對應支付系統自身的支付流水号,并對支付流水進行狀态初始化(這裡涉及一個業務訂單号&支付訂單号如何比對問題,會在後面的讨論中闡述);

● 支付系統調用微信統一下單接口進行預支付(這裡的操作方式就是類網銀式的支付方式,先進行預支付然後由使用者跳到站外進行支付),并同步得到微信支付傳回的預支付訂單資訊,支付系統此時需要更新支付訂單為pending狀态表示處于預支付狀态;

● 然後支付系統将預支付資訊同步給調用方—外賣業務背景,外賣業務背景再同步給外賣App;

● 外賣App會根據預支付訂單資訊通過用戶端支付SDK喚起微信支付用戶端,由使用者操作微信支付用戶端直接向微信支付發起付款動作,需要注意的是,此時調用鍊已經轉移到了站外,實際上此時使用者是否支付或是否支付成功,無論是支付系統還是外賣系統及App本身都是無法直接感覺到支付結果的,需要逐層回調;

● 微信支付會通過循環調用的方式主動将支付結果回調給支付系統,再由支付系統回調給外賣業務系統,最後在使用者直接感覺前由App主動查詢外賣業務系統訂單支付狀态,同時提示使用者支付成功或支付進行中這樣的資訊;

從上面的流程可以了解到,實際上大家平時在通過App購物時支付的一瞬間是經曆了很複雜的流程。那麼,不知道在支付的過程中有沒有這樣的體驗?

在點外賣後付款了,微信也提示支付成功了,但是外賣App卻始終不顯示點餐成功?即使選擇重新支付也提示支付中,不允許重複支付?或者選擇重新支付以後外賣App顯示也顯示點餐成功了,但是之前支付的錢卻不見了,隻能打客服投訴,各種麻煩?

上述問題,在目前支付流程的設計上是必然會發生的,目前作者所在的公司也有類似的問題,雖然這種問題發生的機率可能不是特别高,但是絕對是破壞使用者體驗以及增加了客服的工作量,處理得是否得當是衡量一套支付系統是否強大的核心名額之一。

那麼怎樣的設計才能很好地解決此類問題呢?

從流程上看使用者選擇微信支付并喚起微信錢包付款後,實際上外賣平台支付系統已經感覺不到系統的狀态了,也就說此時使用者是否完成了支付,平台是無法同步感覺的,隻能依賴于微信的主動通知回調,一般來說目前主流的支付公司都有一套完整的商戶通知邏輯,會在支付完成後實時通知到商戶。

但是很多時候會有多種因素導緻這種通知被延遲,比較常見的因素主要有網絡、自身平台系統服務當機、第三方管道通知服務故障等。

也就說會有使用者支付了點外賣的錢,系統卻沒有實時顯示支付成功的問題,也就是我們常說的短時掉單問題;或者使用者沒有及時支付,重新付款時卻會被提示“支付中請勿重複送出”,也就是支付防重問題。

對于掉單問題的處理,可以根據業務場景進行考慮,但無論是哪種方案,越快速補償業務越能夠有效地提升使用者體驗,減少系統異常處理流程,讓防重機制更加靈敏,在避免重複支付問題的同時提高支付成功率。

另外,是否允許使用者重複支付,如何利用沖正機制有效提高使用者體驗的同時快速保障使用者權益,也是需要在整體方案中進行考慮的方面。

根據業務時效性要求不同,大緻有兩種方案:

異步補償機制。

具體來說,就是支付流程按照正常的流程走,通過采用旁挂定時的方式掃描系統中一定時間政策範圍内的pengding狀态的訂單,通過微信提供的訂單查詢接口主動輪詢,一旦支付狀态查詢到終态即刻觸發系統回調,完成支付訂單及業務邏輯的補償;

另一方面,如果pengding狀态訂單通過輪詢方式沒有查詢到最終狀态則需要設定一定的重複輪詢政策,例如5分鐘、10分鐘、20分鐘、1小時、3小時、8小時、24小時這樣,并在超過政策規定的時間及輪詢次數後将支付流水更新為失效終态,并提供訂單查詢接口供業務平台完成自身業務訂單邏輯的更新。

系統示意圖如下:

支付系統的防重設計

通過旁挂式的方式,支付主流程會變的相對簡單,隻需要考慮正常的收單場景,對于很多業務實時性不太高的支付場景,這種方式也夠用。但對于業務實時性要求非常高,并且對使用者體驗有極緻要求的場景來說,這種方式顯然也是存在明顯問題的。

我們還是拿點外賣這件事來說,外賣背景在接收到使用者通過App發送的點餐支付請求後生成外賣訂單并将支付請求發送給平台支付系統,支付系統一般來說會首先進行訂單防重判斷,即已經發起過的成功支付/支付中的請求不被允許發起第二次,支付成功的交易不允許重複發起。

但是等待支付或支付失敗的交易很多公司内部支付系統都會被要求允許發起二次付款,在外賣點餐環節,如果使用者點餐了但是并沒有立刻進行支付或者支付由于某種原因失敗了,是可以重新發起付款的,在這種情況下,支付系統就會面臨一個問題,由于不知道在進行預支付後使用者是否完成了支付,對于是否應該繼續讓使用者發起支付請求,防重邏輯就會變的遲鈍,如果允許使用者支付則可能出現重複扣款的問題,不允許則會影響使用者體驗,為了讓整個機制變得合理,是以需要依賴于上述系統的補償機制來進行回盤或失效處理。

這裡會遇到以下三種情況:

1、使用者最終未支付,則系統安裝一定的輪詢機制進行後續的訂單失效處理即可;

2、使用者完成了支付,支付系統遲遲收不到微信的回調,通過逐漸輪詢的方式系統也會進行後續的訂單回調補償;但這裡的問題是,如果異步補償系統對訂單的輪詢不夠及時(在支付訂單量比較大的情況下,通過定時輪詢的方式在時效性上較差),那麼就會導緻一個比較尴尬的情況,使用者付完了錢,但是外賣訂單很長時間顯示未支付,無法進行派單,在輪詢補償系統完成回調後觸發派單操作,但往往很可能已經過了飯點,并且很可能使用者已經觸發了申訴流程,外賣平台需要進行退款操作(增加客服工作量);

3、則是使用者當時并未及時支付,在訂單失效前的某個時間,使用者可能會選擇重新付款,因為此時支付系統訂單并未失效,會處于支付中狀态,觸發防重機制,無法再次發起付款;

對于2、3兩種情況,如果需要很好的滿足業務要求,就要提高支付系統時效性,提高訂單防重失效、快速回盤的處理時間。要達到這樣的效果,往往單純的依賴旁挂式的處理方案是很難達到的,而是需要讓實時支付流程的設計變得更加智能和靈敏。

實時支付流程優化設計

為解決上面的問題我們需要在實時支付流程中加入異常優化機制,從整個流程的設計上去解決,讓整個支付系統變得更加智能和靈敏,雖然這種方式看似讓支付主流程變得複雜了很多,但從優化使用者體驗、提高系統靈敏度的角度看,這種複雜度是值得的并且是可以通過技術細節屏蔽的。

那麼具體應該怎樣去設計這樣的流程?

詳細如圖所示(需點選放大):

支付系統的防重設計

如上圖所示,支付系統接收到前端發起的支付請求,系統首先需要進行防重判斷,這裡為了有效地防止并發請求,采用Codis鎖的方式,即一筆業務支付訂單請求發送到支付系統後首先擷取Codis全局鎖,如果存在鎖則說明訂單正常被處理/未被正常處理,此時我們需要進行鎖更新時間判斷,如果鎖的更新時間與系統目前時間差<=10s(可根據業務場景進行動态調整,即10s内同一筆商戶訂單号的支付請求不允許被多次發起),則很有可能此時系統正在處理這筆支付請求,應該正常進行防重處理;

相反,如果擷取的Codis鎖的更新時間與系統目前時間差>10s,則此時會存在兩種情況,一種就是這筆支付訂單沒有被正常支付,是應該被允許重新發起支付的;另一種可能則是使用者可能支付成功了,隻是管道在支付結果回調的過程中出了問題導緻系統掉單。這兩種情況混在一起,系統并不能立刻識别出到底屬于何種情況。

這個問題是一個非常普遍和典型的問題,幾乎很多公司都會遇到。

此時,支付系統有兩種選擇,一種選擇是執行嚴格的防重政策,即要求所有對接支付平台的業務系統每次調用支付請求都必須生成不同的商戶訂單号,支付系統對于同一個訂單号無論支付成功與否都不允許将此商戶支付單号重複發送給支付平台,這種方案與第三方支付公司的接口約定一緻。

這種防重政策粗暴簡單,本質上是将邏輯的複雜性傳到給了業務系統,也會讓業務變的難受,如果支付平台在後期經曆過重建,需要推動業務線切換的話,也往往會招緻業務系統的反對。

那麼如何讓支付平台本身來屏蔽這種複雜的細節,讓業務盡可能無感覺?

兩個訂單号

● 商戶訂單号:業務系統發起的向支付系統發起支付請求是生成的在商戶系統中唯一辨別一筆訂單的标記。

● 支付訂單号:業務系統向支付系統發起支付請求後,支付平台本身生成的系統唯一辨別一筆支付流水的标記,并且是支付系統與第三方支付管道互動的唯一流水辨別。

為了達到以上目标需要在支付系統内部采用1:3(舉例)的訂單模型,即1筆業務訂單号可以對應支付系統3筆支付訂單流水,并且每筆支付流水允許被發起的條件是上筆支付流水資料庫訂單狀态是未支付成功,并且需要在目前這筆支付流水重新生成後将上筆支付流水放入訂單動态實效隊列,進行快速失效處理。

之是以采用以上方式,原因在于超過3次時間間隔超過30s(政策可以根據業務實踐進行動态調整)還未完成支付的情況,系統基本可以認定屬于惡意點選行為,可以直接拒絕此筆業務訂單重新發起支付了。

需要動态将上筆支付訂單快速置為實效的原因在于,我們需要在内部設定一個邏輯:“如果支付訂單處于實效狀态并在後面接收到了第三方支付成功的回調,則需要系統自動發起該筆支付訂單的原路退款邏輯,并確定該筆訂單不會被通知到商戶側”。這種現象之是以出現,在于我們為了提高系統的實時性允許了少量重複扣款的情況發生,并進行了自動沖正邏輯。

當然,在細節的處理上我們是在目前流水發起前對上筆流水已經進行了一輪訂單實時查詢,如果結果為支付成功,則此次請求會直接傳回支付成功(或者,也可以提示已經支付成功,App主動查詢支付系統的訂單狀态來完成回盤)。

如果目前訂單再次預支付成功,在同步傳回預支付結果前需要更新Codis中訂單鎖的時間及發起次數。同時,在接受到第三方正常的支付成功回調後完成訂單狀态更新及商戶通知後消除Codis鎖。

上述政策,為解決防重&二次支付問題提供了一種方案,當然還有很多細節的代碼邏輯是需要考慮完善的,例如,實時查詢逾時的政策、退款的觸發時間、使用者提示等。

此外,如果使用者不再選擇再次發起付款,系統中的存量訂單也需要通過文中早些時候介紹過的異步補償機制逐漸将進行失效處理(具體政策機制可參考圖示及之前的概述),隻是如果在異步補償機制過程中發現掉單的訂單,是否正常回盤或自動給使用者退款,就需要具體情況具體分析了。

原文釋出時間為:2018-09-5

本文作者:無敵碼農

本文來自雲栖社群合作夥伴“

程式員小灰

”,了解相關資訊可以關注“

”。

繼續閱讀