一份詳盡的支付平臺(tái)高可用架構(gòu)設(shè)計(jì)實(shí)踐
我在前一家公司的***個(gè)任務(wù)是開(kāi)發(fā)統(tǒng)一支付平臺(tái),由于公司的業(yè)務(wù)需求,需要接入多個(gè)第三方支付。
圖片來(lái)自包圖網(wǎng)
之前公司的支付都是散落在各個(gè)項(xiàng)目中,極其不利于支付的管理,于是聚合三方支付,統(tǒng)一支付平臺(tái)的任務(wù)就落在我手上。
可以說(shuō)是完全從 0 開(kāi)始設(shè)計(jì),經(jīng)過(guò)一番實(shí)戰(zhàn)總結(jié),我得出了一些架構(gòu)設(shè)計(jì)上的思考。
之前就一直很想把自己的架構(gòu)設(shè)計(jì)思路寫出來(lái),但一直沒(méi)動(dòng)手,前幾天在技術(shù)群里有人問(wèn)到相關(guān)問(wèn)題,我覺(jué)得有必要把它寫出來(lái),以幫助到更多需要開(kāi)發(fā)支付平臺(tái)的開(kāi)發(fā)人員。
組件模式
由于公司業(yè)務(wù)在很多地區(qū)都有,需要提供多種支付途徑,以滿足業(yè)務(wù)的發(fā)展,所以設(shè)計(jì)的支付平臺(tái)需要接入多種第三方支付渠道,如:微信支付、支付寶支付、PayPal、IPayLinks 等等。
我們都知道,每個(gè)第三方支付,都有自己的一套對(duì)外 API,官方都有一套 SDK 來(lái)實(shí)現(xiàn)這些 API,我們應(yīng)該如何組織這些 API 呢?
由于第三方支付渠道會(huì)隨著業(yè)務(wù)的發(fā)展變動(dòng),所以組織這些 SDK 就需要在不影響支付平臺(tái)整體架構(gòu)的前提下可靈活插拔。
這里我使用了組件的思想,將支付 API 拆分成各種組件支付組件、退款組件、訂單組件、賬單組件等等。
那么這樣就可以當(dāng)引入一個(gè)第三方支付 SDK 時(shí),可靈活在組件上面添加需要的 API,架構(gòu)設(shè)計(jì)如下:
通過(guò) Builder 模式根據(jù)請(qǐng)求參數(shù)構(gòu)建對(duì)應(yīng)的組件對(duì)象,將組件與外部分離,隱藏組件構(gòu)建的實(shí)現(xiàn)。組件模式+Builder 模式使得支付平臺(tái)具備了高擴(kuò)展性。
多賬戶體系
在接入各種第三方支付平臺(tái)時(shí),我們又遇到一個(gè)賬戶的問(wèn)題,原因是公司當(dāng)時(shí)的小程序與 App 使用的是不同的微信賬號(hào),因此會(huì)出現(xiàn)微信支付會(huì)對(duì)應(yīng)到多個(gè)賬戶的問(wèn)題。
而我設(shè)計(jì)支付平臺(tái)時(shí),沒(méi)有考慮到這個(gè)問(wèn)題,當(dāng)時(shí)第三方支付只對(duì)應(yīng)了一個(gè)賬戶,而且不同的第三方支付的賬戶之間相互獨(dú)立且不統(tǒng)一。
于是我引入了多賬戶體系,多賬戶體系最重要的一個(gè)核心概念是以賬戶為粒度,接入多個(gè)第三方支付,統(tǒng)一賬戶的參數(shù),構(gòu)建了統(tǒng)一的支付賬戶體系。
支付平臺(tái)無(wú)需關(guān)心不同支付之間的賬戶差異以及第三方支付是否有多少個(gè)賬戶。
此時(shí)我在支付平臺(tái)架構(gòu)圖加上賬戶層:
前端只需要傳遞 AccountId,支付平臺(tái)就可以根據(jù) AccountId 查詢出對(duì)應(yīng)的支付賬戶。
然后通過(guò) Builder 模式構(gòu)建支付賬戶對(duì)應(yīng)的組件對(duì)象,完全屏蔽不同支付之間的差異。
在多賬戶體系里面,可以支持***多個(gè)支付賬戶,完全滿足了公司業(yè)務(wù)的發(fā)展需求。
統(tǒng)一回調(diào)與異步分發(fā)處理
做過(guò)支付開(kāi)發(fā)的同學(xué)都知道,目前的第三方支付都有一個(gè)特點(diǎn),就是支付/退款成功后,會(huì)有一個(gè)支付/退款回調(diào)的功能,目的是為了讓商戶平臺(tái)自行校驗(yàn)該筆訂單是否合法。
比如:防止在支付時(shí),客戶端惡意篡改金額等參數(shù),那么此時(shí)支付成功后,訂單會(huì)處于支付中狀態(tài),需要等待第三方支付的回調(diào)。
如果此時(shí)收到了回調(diào),在校驗(yàn)時(shí)發(fā)現(xiàn)訂單的金額與支付的金額不對(duì),然后將訂單改成支付失敗,以防止資金損失。
回調(diào)的思想是保證最終的一致性,所以我們調(diào)起支付時(shí),并不需要在此時(shí)校驗(yàn)參數(shù)的正確性,只需要在回調(diào)時(shí)校驗(yàn)即可。
講完了回調(diào)的目的,那么我們?nèi)绾蝸?lái)設(shè)計(jì)支付平臺(tái)的回調(diào)呢?由于支付平臺(tái)接入了多個(gè)第三方支付,如果此時(shí)每個(gè)第三方支付設(shè)置一個(gè)回調(diào)地址,那么將會(huì)出現(xiàn)多個(gè)回調(diào)地址。
由于回調(diào)的 API 必須是暴露出去才能接受第三方的回調(diào)請(qǐng)求,所以就會(huì)有安全問(wèn)題。
我們必須在 API 外層設(shè)置安全過(guò)濾,不然很容易出現(xiàn)一些非法訪問(wèn)暴力破解,所以我們需要統(tǒng)一回調(diào) API,統(tǒng)一做安全校驗(yàn),之后再進(jìn)行一層分發(fā)。
分發(fā)的機(jī)制我這里建議用 RocketMQ 來(lái)處理,可能有人會(huì)問(wèn),如果用 RocketMQ 來(lái)做分發(fā)處理,此時(shí)怎么實(shí)時(shí)返回校驗(yàn)結(jié)果到第三方支付呢?
這個(gè)問(wèn)題也是我當(dāng)時(shí)一直頭疼的問(wèn)題,以下是我對(duì)回調(diào)設(shè)計(jì)的一些思考:
①公司的系統(tǒng)是基于 Spring Cloud 微服務(wù)架構(gòu),微服務(wù)之間通過(guò) HTTP 通信,當(dāng)時(shí)有很多個(gè)微服務(wù)接入了我的支付平臺(tái),如果用 HTTP 作分發(fā),可以保證消息返回的實(shí)時(shí)性。
但也會(huì)出現(xiàn)一個(gè)問(wèn)題,由于網(wǎng)絡(luò)不穩(wěn)定,就會(huì)出現(xiàn)請(qǐng)求失敗或超時(shí)的問(wèn)題,接口的穩(wěn)定性得不到保障。
②由于第三方支付如果收到 False 響應(yīng),就在接下來(lái)一段時(shí)間內(nèi)再次發(fā)起回調(diào)請(qǐng)求。
這么做的目的是為了保證回調(diào)的成功率,對(duì)于第三方支付來(lái)說(shuō),這沒(méi)毛病,但對(duì)于商戶支付平臺(tái)來(lái)說(shuō),也許就是一個(gè)比較坑爹的設(shè)計(jì)。
你想一下,假設(shè)有一筆訂單在支付時(shí)惡意篡改了金額,回調(diào)校驗(yàn)失敗,返回 False 到第三方支付,此時(shí)第三方支付會(huì)再重復(fù)發(fā)送回調(diào),無(wú)論發(fā)送多少次回調(diào),都會(huì)校驗(yàn)失敗。
這就額外增加了不必要的交互,當(dāng)然這里也可以用冪等作處理,以下是微信支付回調(diào)的應(yīng)用場(chǎng)景說(shuō)明:
基于以上兩點(diǎn)思考,我認(rèn)為返回 False 到第三方支付是沒(méi)必要的,為了系統(tǒng)的健壯性,我采用了消息隊(duì)列來(lái)做異步分發(fā),支付平臺(tái)收到回調(diào)請(qǐng)求后直接返回 True。
這時(shí)你可能會(huì)提出一個(gè)疑問(wèn),如果此時(shí)校驗(yàn)失敗了,但此時(shí)返回 true,會(huì)不會(huì)出現(xiàn)問(wèn)題?
首先,校驗(yàn)失敗情況,訂單必定是處于支付失敗的狀態(tài),此時(shí)返回 True 的目的是為了減少與第三方支付不必要的遠(yuǎn)程交互。
因?yàn)?RocketMQ 的消息是持久化到磁盤的,所以用消息隊(duì)列來(lái)做異步分發(fā)***的好處,就是可以復(fù)查消息隊(duì)列里面的消息來(lái)排查問(wèn)題,而且消息隊(duì)列可以在業(yè)務(wù)的高峰期進(jìn)行流量削峰。
以下是統(tǒng)一回調(diào)與分發(fā)處理的架構(gòu)設(shè)計(jì)圖:
聚合支付
支付平臺(tái)聚合了多種第三方支付,因此在請(qǐng)求層需要做很多的適配工作,以滿足多種支付的需求。
可能你會(huì)想,直接在適配那里加幾行 if else 不就得了嗎,這么做也沒(méi)問(wèn)題,也可以滿足多種支付的需求,但你有沒(méi)有想過(guò),假設(shè)此時(shí)再加一個(gè)第三方支付,你會(huì)怎么做?
你只能在原有方法上加多個(gè) else 條件,這樣就會(huì)導(dǎo)致請(qǐng)求層代碼不斷地隨著業(yè)務(wù)發(fā)展改變,使得代碼極其不優(yōu)雅,而且也不好維護(hù)。
這時(shí)我們就得用上策略模式,將這些 if else 代碼消除,當(dāng)我們?cè)黾右粋€(gè)第三方支付時(shí),我們只需要新建一個(gè) Strategy 類就可以了,策略模式究竟怎么使用可以看看大話設(shè)計(jì)模式。
因此我在 Builder 模式前加多了一層支付策略層:
請(qǐng)求處理
由于支付平臺(tái)涉及到資金,支付的各種請(qǐng)求與返回,以及異常記錄在一個(gè)支付平臺(tái)中異常重要,因此我們需要記錄每一次的支付請(qǐng)求記錄,以便后續(xù)排查問(wèn)題。
基于這點(diǎn)需求,我在開(kāi)始請(qǐng)求第三方支付之前,設(shè)計(jì)了一層 Handler 層,所有的請(qǐng)求都必須經(jīng)過(guò) Handler 層進(jìn)行處理,Handler 核心方法如下:
- public K handle(T t) {
- K k;
- try {
- before(t);
- k = execute(t);
- after(k);
- } catch (Exception e) {
- exception(t, e);
- }
- return k;
- }
- protected abstract void before(T t);
- protected abstract void after(K k);
- protected abstract void exception(T t, Exception exception);
原則上來(lái)說(shuō),我設(shè)計(jì)的 Handler 層,利用了模版模式,不僅僅可以實(shí)現(xiàn)日志的記錄,還可以實(shí)現(xiàn)多種處理方式,比如請(qǐng)求監(jiān)控,消息推送等等,實(shí)現(xiàn)了 Handler 層的高擴(kuò)展性。
以下是 Handler 層的架構(gòu)設(shè)計(jì)圖:
寫在***
以上就是我的支付平臺(tái)架構(gòu)設(shè)計(jì)思路,總結(jié)來(lái)說(shuō),支付平臺(tái)需要具備可擴(kuò)展性、穩(wěn)定性、高可用性。
因此我在設(shè)計(jì)支付平臺(tái)時(shí)使用了很多設(shè)計(jì)模式以及引入消息隊(duì)列處理回調(diào)分發(fā)的問(wèn)題,使得支付平臺(tái)具備這幾點(diǎn)特性。
希望能夠給你一些啟發(fā)與幫助,***我把支付平臺(tái)整體的架構(gòu)設(shè)計(jì)圖貼出來(lái):