支付系統的要求:安全、高效。安全是基本,高效是追求。
要達成兩個目標,難免會遇到各種坑,下面挑幾個典型的問題來講述,並附上簡單的應對方案。
請求超時問題
網絡的可靠性要依賴硬件,所以只要是網絡調用,必然要考慮超時問題,另外因為支付系統一般內部驗證操作多,請求處理時間長,比一般系統超時的概率更大。
支付系統內的每一個請求都應該謹慎處理,而對於無法確定結果的超時請求更不能輕易確定終態,絕對不能像一個簡單的網頁請求一樣重試一次。
一般采取保守策略,將交易狀態保持在一個無害的默認狀態(處理中或未支付),等待下次觸發處理。
請求超時本身易處理,但它導致的后續問題會很多,下面會提到。
終態判斷處理問題
返回碼映射
終態的判斷應該是支付系統內最重要也是最容易踩坑的地方,這個處理的復雜程度真的太依賴三方系統的狀態碼設置了。由於成功和處理中的狀態只有一種,而錯誤則會有各種各樣的原因,有的錯誤可以重試,有的錯誤是系統錯誤。分清交易失敗的原因,關系到系統如何下一步處理交易,所以錯誤明細碼的設計十分重要。
對於一個返回碼設計良好的系統,如微信、支付寶,有業務結果碼和明細錯誤碼之分,我們進行終態判斷和返回碼映射時,可以首先以業務結果碼為准,在業務結果為失敗時,再去檢查明細錯誤碼。
而一個設計不那么好的系統,將業務結果碼和明細錯誤碼混淆在一起,判斷結果就比較坑,要么將錯誤碼列出對比,要么用很危險的else
。
此問題無法真正避免,只能給出謹慎映射,多向三方系統求證的建議。
查詢無交易記錄
無交易記錄應該是最危險的返回碼了,偏偏因為偶發的網絡波動,交易請求受理超時,這個碼還不可避免。 正常邏輯的情況下,無交易記錄是沒發到三方系統,當然是失敗,可是如果在代付交易中,三方系統告訴你,別輕易置失敗,萬一你參數傳錯了呢,錢付出去我們可不賠喲~你還那么有自信么。。。
解決方案中最保守的方式當然是作為處理中來處理,然后人工介入處理,這個只能用在交易量不是太大,網絡偏穩定的情況下,目前我們只在代付交易中使用此策略。
另外一種方式是搭配請求時的響應信息來判斷,如果三方系統響應信息為成功時,查詢為無此交易,那自然是參數或系統邏輯等問題,迅速報警通知處理。如果請求受理時為超時,那么便可以認為是網絡問題沒有發送成功了,有時候還是要對自己的代碼有一些信心的。
交易及時性問題
交易及時性不是一個很嚴重的問題,甚至在支付系統中,太有及時性的交易還會使用戶不太放心。但作為一個程序員,追求效率是天性嘛,我們還是希望盡早獲取到交易結果,但這也可能導致踩坑。
太早的查詢
查詢太早導致問題會出現在兩種場景:請求超時、三方系統設計問題。
- 請求超時:請求超時時,系統在過了超時時間后斷開連接不再阻塞,立刻發起查詢請求的話,三方系統可能剛接收到請求,正在進行參數驗證,數據還未落地,此時會收到無此交易的響應,我們將交易作為失敗處理后,交易可能在之后成功。所以查詢一定要有延遲,一定要給三方系統足夠的時間來處理交易。
- 三方系統設計問題:如xx,在受理交易時使用了中間件,中間件掛掉后,我們查詢無此交易,但他們重啟中間件后又處理交易,竟然又成功了。這個最好在之前能問清三方系統的處理方式,並針對性地設置查詢延遲。
頻繁的查詢
太多頻繁的查詢是無意義的,交易正在三方系統中處理,查詢不會使交易被迅速處理,還會造成網絡資源和系統資源的浪費,如果你還記得與三方系統的每一次交互都要重視,那么查詢日志也沒法看了。
解決此問題,要:
- 避免“過早”的查詢,這要考慮三方系統的處理速度;
- 合理設置查詢時間間隔,一些交易需要更長的處理時間,可以設置梯度時間間隔;
- 處理無意義的查詢,如查詢“無此交易”,那么進行多少次結果都不會變,再進行查詢就是無意義的;
隔日賬問題
隔日賬問題在對賬過程中不可避免,由於服務器時間有差異,交易處理也需要時間,在凌晨附近發生的交易可能會遭遇此問題,這會給對賬造成一定的困擾,但合理的處理方式不會有太大的問題:
自己系統與三方系統對賬文件不一致時調用查詢接口在缺失交易的系統內查詢,先確認交易的存在,再分析交易時間。如在隔日附近,則暫不處理,待次日對賬文件的對比。
如某一系統內交易不存在,或交易不太可能會發生隔日賬問題,這便需要系統之間人工來處理了,不過這不也是對賬的意義所在么。
並發效率問題
並發鎖
並發問題在所有系統內都會存在,只是支付系統內處理不好后果會很嚴重,處理方式一般是事務、互斥鎖。
支付系統單系統內使用這些方式也沒有問題,只是鎖的粒度會略大,至少需要保證一個模塊內交易處理的原子性。 分布式系統內就要考慮分布式鎖了,這些業內也都有很多解決方案了。
只是加鎖就意味着效率損耗,合理拆分出交易核心模塊,並對這些模塊添加鎖。另外使用合理的“進程-數據”分配方式,也會減少鎖沖突。
冪等
保持交易中的冪等很重要,它是避免重復支付的基石。 即使系統設計完全,我們還是要追求業務邏輯上的冪等,這也就意味着更多的查詢確認,同時意味着效率下降。
效率下降不可避免,我們可以使用緩存來降低效率下降的幅度,在緩存中設置交易狀態標識,對交易狀態標識異常的交易再去數據庫查詢。
異步拆分尷尬
異步可以抗並發,提高效率,放之四海皆准。支付系統對異步的依賴更強是因為支付系統由於其處理流程冗長更易達到效率瓶頸。
面對異步我們首先要解決的問題是異步拆分的粒度問題,粗粒度的拆分效率能提升的效率有限,細粒度的拆分調控起來不易,處理異步拆分的粒度,看交易量吧,不做過度設計。
進行異步拆分時,每一步都需要一個觸發進程,此進程可以是常進程輪詢,也可以是cron進程,事件機制來觸發自然是更好的,但它對消息隊列的要求很高,設計也較復雜。
除此之外還需要一個確認進程,確保下一步的進程順利接收處理了,在后續進程受理失敗時,能夠及時重試處理。
測試坑
測試是開發中必不可少的步驟,自己測和測試來測,總要完全走一遍流程才敢放心上線。
支付測試略坑了: 首先測試環境的布置,支付系統牽涉到多個三方系統的交互,靠譜的系統都會提供測試系統,可是難免有些系統不提供測試環境,或者測試限制頗多,限支付行,限金額等,還要提防其測試系統忽然就掛了。然后是線上測試,更要小心翼翼,一個不慎就是資金損失。
為了提供良好的測試環境,我們引入“MOCK”功能,mock 中文意思為“模仿”,即通過模擬三方系統的返回值來測試本系統的穩定性。
但 mock 的代碼侵入性略強,完整的 mock 模塊必然有if else
語句的存在,由於支付相關的系統較多,要搭建完整的 mock 系統不容易,單點 mock 需要各處埋點。整體 mock 的又不便於測試特定功能。
小結
支付的坑包括但不限於本文介紹的這些,可能還會有其他奇怪的問題,文章沒有介紹到。
若想盡量避免支付系統的坑,那么一定要保持着保守的態度,將狀態或交易保持無害。有些需要事務操作,但無法使用典型事務的場景,將次要的一開始執行,即使出了問題,有重試、回滾等操作,也不會造成影響。
支付總結暫時到此為止。