公司有一項(xiàng)儲(chǔ)值卡充值業(yè)務(wù):客戶(hù)在微信公眾號(hào)開(kāi)通儲(chǔ)值卡服務(wù)悦屏,通過(guò)微信支付往卡里面充值逞怨,充值成功后客戶(hù)可收到消息通知,并進(jìn)行消費(fèi)苫纤。
看起來(lái)是一項(xiàng)很簡(jiǎn)單的業(yè)務(wù),最初我們儲(chǔ)值卡團(tuán)隊(duì)的實(shí)現(xiàn)也確實(shí)很簡(jiǎn)單纲缓。我們看看最初的實(shí)現(xiàn):
相信聰明的你一眼就能看出問(wèn)題:
- 壓根沒(méi)有考慮分布式事務(wù)一致性卷拘,比如第 12 步根本沒(méi)有考慮卡系統(tǒng)充值失敗的情況該如何處理,而是默認(rèn)其一定能成功祝高;
- 大部分的處理都是放在前端業(yè)務(wù)系統(tǒng)(除了這里的公眾號(hào)系統(tǒng)恭金,還有 POS 機(jī)系統(tǒng),而 POS 機(jī)是通過(guò)調(diào)公眾號(hào)系統(tǒng)接口來(lái)實(shí)現(xiàn)的)褂策;
- 第 4 步直接下單,第 5 步直接調(diào)微信支付颓屑,壓根沒(méi)有跟卡系統(tǒng)有任何通信:這里默認(rèn)用戶(hù)的充值行為一定是合法的斤寂;
- 在微信的支付回調(diào)中(第 10 步往后),是先處理一系列業(yè)務(wù)邏輯揪惦,最后才調(diào)充值接口遍搞,這里也是默認(rèn)卡充值一定能成功;
看到這里你可能會(huì)大呼開(kāi)發(fā)人員是不是沒(méi)長(zhǎng)腦子器腋?
實(shí)際情況是溪猿,這個(gè)版本的開(kāi)發(fā)是幾年前的事情了,那時(shí)候公司還是創(chuàng)業(yè)早期纫塌,第一目標(biāo)是盡快上線(xiàn)能用诊县,而且客戶(hù)量沒(méi)有那么大,雖然中間也出現(xiàn)過(guò)一些數(shù)據(jù)不一致的情況措左,也都通過(guò)人工處理了事了依痊。
隨著公司業(yè)務(wù)的發(fā)展,用戶(hù)量越來(lái)越大怎披,而且還要和第三方合作(儲(chǔ)值卡作為一種支付方式提供給第三方使用)胸嘁,問(wèn)題出現(xiàn)得也越來(lái)越頻繁,不得不將這塊提上重構(gòu)議程凉逛。
那么性宏,針對(duì)上面提的幾點(diǎn)問(wèn)題,我們大體能想到如下重構(gòu)項(xiàng):
- 將充值業(yè)務(wù)邏輯從前端系統(tǒng)剝離状飞,做成單獨(dú)的服務(wù)毫胜;
- 在下單前书斜,先調(diào)一下卡系統(tǒng)接口,檢查用戶(hù)的充值行為是否合法指蚁,避免后面不必要的麻煩菩佑;
- 在支付回調(diào)中,處理充值失敗的場(chǎng)景凝化;
初步設(shè)計(jì)如下:
這里我們重點(diǎn)討論下對(duì)第 14 步(卡充值接口返回結(jié)果)的處理:
- 如果返回充值成功稍坯,那萬(wàn)事大吉,該干嘛干嘛搓劫;
- 如果失敗呢瞧哟?可能的處理方式如下:
- 繼續(xù)重試,最多重試 3 次枪向,如果成功了勤揩,萬(wàn)事大吉;
- 如果上面重試還是失敗秘蛔,那么調(diào)微信退款陨亡,并將訂單狀態(tài)改成充值失敗深员;
騷年你等等负蠕!
你說(shuō)什么?重試失敗了就去退款倦畅?
實(shí)踐中遮糖,遠(yuǎn)程調(diào)用失敗的一個(gè)很大原因是網(wǎng)絡(luò)超時(shí)(而超時(shí)的很大原因又是對(duì)方負(fù)載過(guò)高),而面對(duì)超時(shí)叠赐,我們是不知道對(duì)方到底有沒(méi)有處理成功的欲账,萬(wàn)一這邊把錢(qián)退掉了,那邊又充值成功咋辦芭概?(我們是 SaaS 服務(wù)商赛不,這時(shí)真正的損失方是我們的商戶(hù),而商戶(hù)無(wú)疑會(huì)找我們索賠的)
一種方案是:
在多次重試失敗后發(fā)起微信退款之前谈山,先調(diào)卡系統(tǒng)查詢(xún)接口俄删,如果查詢(xún)結(jié)果是充值成功,則不退款奏路,繼續(xù)后續(xù)流程畴椰,否則發(fā)起退款;
該方案在實(shí)際中也基本行不通鸽粉,因?yàn)槿绻嵌螘r(shí)間網(wǎng)絡(luò)有問(wèn)題或者對(duì)方服務(wù)器負(fù)載高斜脂,查詢(xún)也有很大概率失敗,或者就算查成功了并返回充值記錄不存在触机,也有可能之前調(diào)的充值接口還在跑(比如處于鎖等待狀態(tài))帚戳。
面對(duì)該問(wèn)題玷或,我們決定用定時(shí)任務(wù)來(lái)解決。在微信支付回調(diào)中片任,如果多次調(diào)卡充值接口失敗偏友,我們不發(fā)起退款,也不進(jìn)行后續(xù)流程对供,而是在數(shù)據(jù)庫(kù)中寫(xiě)入一條異常記錄位他,然后結(jié)束本次處理。
在定時(shí)任務(wù)中(比如 10 分鐘一次)产场,我們?nèi)〕瞿切┊惓S涗浂焖瑁{(diào)卡系統(tǒng)相關(guān)接口核對(duì)最終狀態(tài),如果充值成功了京景,則補(bǔ)充執(zhí)行充值成功的后續(xù)流程窿冯,否則發(fā)起微信退款,并執(zhí)行其他充值失敗流程(如改訂單狀態(tài)确徙,給用戶(hù)發(fā)通知醒串、回調(diào)業(yè)務(wù)系統(tǒng)等)。
為了防止錢(qián)退了后卡又充值成功鄙皇,定時(shí)任務(wù)中只處理 1 小時(shí)前的數(shù)據(jù)厦凤。
另一個(gè)隱藏的問(wèn)題是,在前面的充值流程中育苟,直到微信支付回調(diào),卡系統(tǒng)都沒(méi)有關(guān)于這次充值行為的任何記錄椎木。這可能會(huì)導(dǎo)致后續(xù)一系列問(wèn)題违柏,其中一個(gè)問(wèn)題是,在最初下單(步驟 5)到最終充值(步驟 13)這段時(shí)間內(nèi)香椎,一旦任何變量(充值規(guī)則)發(fā)生改變漱竖,這次充值就有可能會(huì)失敗(或者導(dǎo)致數(shù)據(jù)差錯(cuò))畜伐。這個(gè)時(shí)間差短則幾十毫秒馍惹,長(zhǎng)則幾分鐘十幾分鐘都有可能。另一個(gè)次要問(wèn)題是玛界,一旦發(fā)生充值異常,卡系統(tǒng)自身是不知情的(因?yàn)闆](méi)有任何記錄),對(duì)卡系統(tǒng)的任何查詢(xún)也都不會(huì)反映這次充值行為堰酿。
為了解決該問(wèn)題徒像,我們引入預(yù)充值的概念。在下單后調(diào)微信支付前笨枯,先同步調(diào)卡系統(tǒng)的預(yù)充值接口薪丁,該接口計(jì)算充值合法性并生成一條預(yù)充值記錄遇西,該記錄包含充值賬號(hào)、充值金額严嗜、支付金額粱檀、充值單號(hào)等關(guān)鍵信息,狀態(tài)為“充值中”漫玄。
在微信支付回調(diào)中茄蚯,將預(yù)充值狀態(tài)改成“充值成功”,并處理一些其他邏輯称近。
綜合第队,最終方案如圖:
總結(jié):
- 任何涉及到分布式事務(wù)的地方都是復(fù)雜的,必須小心設(shè)計(jì)刨秆;
- 遠(yuǎn)程過(guò)程處理不具有時(shí)序性凳谦,設(shè)計(jì)時(shí)必須考慮進(jìn)去(如退款后最終又充值成功的情況);
- 現(xiàn)實(shí)中的設(shè)計(jì)很多時(shí)候做不到完美衡未,我們要做的是保證出現(xiàn)異常的概率最小化并設(shè)置最終檢查哨兵(上面的定時(shí)任務(wù))尸执;
- 就算增設(shè)了哨兵,也不排除需要人工干預(yù)的可能性缓醋,因而在設(shè)計(jì)上盡量保證需要人工干預(yù)時(shí)有跡可循如失、方便處理;
- 遠(yuǎn)程調(diào)用需要有重試機(jī)制(上面只說(shuō)了對(duì)充值接口的重試送粱,其實(shí)其他接口也一樣需要有重試機(jī)制)褪贵;
- 記住一句話(huà):網(wǎng)絡(luò)總是不可靠的;