小小邏輯判斷符的錯誤使用,資損幾萬塊

程序员老猫發表於2024-04-14

分享是最有效的學習方式。

部落格:https://blog.ktdaddy.com/

故事

這是一個真實事件,三年前老貓負責公司的支付資產業務。為了響應上級號召,加強國央企之間的合作,公司新談了一個支付對接的渠道(當然這個支付渠道其實很冷門的,也是為了對接而對接,具體哪個渠道也不方便透露),由於原始支付系統的第三方支付可擴充性設計得還不錯的,所以老貓對接的也是比較快的,熟悉對方的對接文件之後對著編碼就好了,差不多花了三天的時間就完成聯調了。一切看似很順利地上線了。

時隔幾天,收到了一個快遞包裹,是一袋價值53塊錢的“原皮腰果”,當時詫異,翻看了各大消費平臺,都沒有之前的下單記錄,後來和媳婦確認了一下,她也沒有下單。“難道是某個崇拜哥的小姑娘送的?不能吧”當時心裡美滋滋地yy著。

不過之後的一個客訴問題,引起了老貓的重視,老貓排查下來發現一個很重大的問題,錢款的扣除和實際的訂單狀態對不上。說白了就是訂單完結了,但是賬戶資產並沒有完成扣除。我瞬間明白了之前那個“原皮腰果”是怎麼回事兒了,當時在生產測試渠道的時候,在公司內部商城提交了訂單,但是並沒有付款,然而訂單卻成功了。

再三確認之後,確實存在這一問題。一瞬間整個人心態崩了,頭皮發麻,口乾舌燥,心臟“突突突”。怎麼辦?怎麼辦?生產還不知道涉及多少單子,沒辦法,兜不住了,先把這件事情往上拋吧(向上級領導彙報)。

具體原因是什麼呢?我們來看一下對接第三方支付的大概時序流程。

時序

我們一般在對接第三方支付渠道的時候會有上面一些基本流程。

1、當我們內部生成待支付單之後會請求外部第三方支付渠道,此時第三方支付渠道內部會生成待支付單。

2、我們第三方支付單建立成功返回之後,一般內部系統會喚起收銀臺,然後使用者確認支付。

3、第三方支付成功返回訊息之後,整個支付就算已經完結了。

但是問題就出在了第三方支付渠道還有一個定時非同步通知的任務,並且我們也對接了這個介面。這個非同步通知的功能主要是會定時告知我們支付系統第三方支付單的狀態。而且無論成功與否,都會輪詢告知,例如,如果是待支付,對方會告知狀態是0,如果已完成支付,對方會告知狀態是1。我們拿到支付結果之後就會執行後續的訂單完成流程。

收到非同步通知之後的程式碼處理判斷如下:

事故程式碼

    //校驗交易狀態
    if (!Objects.equals(notifyModel.getTradeStatus(), TradeStatus.SUCCESS.getCode())
            && amtConvertY(notifyModel.getTradeAmt()).compareTo(thirdPartyCharge.getAmount()) != 0) {
        LOGGER.error("交易狀態不正確:{}", notifyModel.getOutTradeNo());
        throw new BusinessRuntimeException(Errors.PAY_NOTIFY_IS_FAIL);
    }

正確程式碼

    //校驗交易狀態
    if (!Objects.equals(notifyModel.getTradeStatus(), TradeStatus.SUCCESS.getCode())
            || amtConvertY(notifyModel.getTradeAmt()).compareTo(thirdPartyCharge.getAmount()) != 0) {
        LOGGER.error("交易狀態不正確:{}", notifyModel.getOutTradeNo());
        throw new BusinessRuntimeException(Errors.PAY_NOTIFY_IS_FAIL);
    }

相信眼尖的小夥伴已經發現了,其實就是“||”和“&”的區別。第一種情況當對方告知狀態為0的時候,其實並不會被攔截掉,而是直接走了往後的流程,於是悲劇就發生了。

直接說一下最終的處理,最終其實還是比較幸運的。由於,我們本身已經對接了微信以及支付寶的支付渠道,再加上這個渠道的支付使用的頻率還是非常少的,很多使用者不太會使用這個渠道進行支付,所以最終盤算下來整個的資損金額差不多是3w左右,另外的是其中有個不幸中的萬幸。是因為老貓在這之前做了一套資金追討系統,該系統可以定位出那些使用者“空手套白狼”了。並且能給這些使用者生成對應的待支付訂單,使用者可以透過這些待支付訂單最終完成資金的補償付款,最終完成了資金了追討。所以事後,完全盤點之後發現一共的資損是1600左右。

最終,也算是有驚無險。但是這次的經歷給了老貓上了一課。

下面總結一下我們在做支付賬務系統的過程中應該如何進行資金安全相關的設計,最終做到防患於未然。

概覽

資金安全設計

針對資金安全的問題,不限於透過技術手段避免資損,其實很多時候我們還需要結合資料核對、監控等措施,做到快速發現資損並且止損。下面我們來一一盤點一下資金安全設計的一些點,希望能給大家一些幫助。

資損風險分析

風險要素

在我們做支付資產系統的時候,我們其實需要好好盤點一下資損風險,這些風險可能來自於各個方面。下面涉及,

資金流:我們實際的產品業務中,尤其是支付資產的時候,其實往往會有很多型別的資產形態,可能是積分,可能是現金,當然還有可能是優惠券等等。看到這些金額的時候,我們需要確保上下游系統的一致性、金額計算的正確性、逆向金額不能大於正向金額等等。

互動:我們需要考慮客戶端展示內容是否正確。尤其是小數點展示的精確位上,很容易出現客戶端展現資訊不全的問題,實際其實為99.99元,但是展示的時候卻為99.9或者9.99。

資金規則:資金規則是為資產本身的產品形態規則,舉個例子,發放優惠券這個行為,每個人發多少,發的時間點,發放的人數,發放的門檻等等。再比如某個積分資產的限額,其中又涵蓋著日限額以及月限額。

異常:存在資損風險很多時候其實由於異常導致的,拋開系統本身異常之外,我們還要考慮網路抖動異常,其他服務異常。如果系統本身的事務處理不好,或者最終一致性沒有做好,就很有可能造成資損。

技術風險

系統發生資損麼,很大一部分就是系統沒有設計好或者是編碼過程中的粗心,例如上面老貓的真實案例。那麼且拋開粗心這個人為因素,我們盤點一下本身技術風險,這些技術風險場景主要來源於多併發、冪等、分散式事務、上下游服務超時、資料計算精度、介面協議、校驗邏輯的不嚴謹等等。

上面羅列的這些很多都為一致性的問題。我們一個個來看。

1、併發:多執行緒、同時對資料進行讀寫處理的時候,就有可能造成一致性的問題,例如使用者資產重複支付,積分超發等等,如果在系統層面還用了快取的話,還有可能存在快取未重新整理,導致資料庫和快取不一致的情況。

2、冪等:在支付資產系統中,介面冪等性是十分必要的,介面的冪等能夠很大程度上避免(1)中提到的資產重複支付問題、下單重複問題以及網路重發問題。關於冪等詳細設計,其實老貓在以前的文章中也有梳理,大家有興趣的可以看這裡【前任開發在程式碼裡下毒,支付下單居然沒加冪等

3、服務超時:系統所依賴的服務執行結果返回慢,造成上下游資料狀態不一致,例如核心的支付服務呼叫底層的資產服務進行扣款,結果由於資產扣款邏輯返回超時,導致兩邊資料不一致。

4、介面規範:尤其是對接第三方介面的時候,文件上的必填欄位和非必填欄位如果本身文件就有出入,可能就是致命的。

5、事務:其中包含本地事務以及分散式事務,研發在開發過程中對事務理解不夠透徹,使用不嚴謹,最終導致資料不一致。

6、資料精度:主要在金額四捨五入的場景,最終導致精度丟失。或者上下游系統精度不一致。

防止資損

如果說想要徹底的避免資損,並不是一件容易的事情,系統鏈路複雜,任何一個環節出現問題都有可能導致最終的資損。我們雖然是技術,但是我們的眼光其實不能夠僅僅侷限在技術的眼光去看待資損這個事情,除了技術側儘量規避資損發生之外,其實還有其他方案,例如下圖。

步驟

上圖準確來說其實是一個防止資損,或者說盡量保證企業損失最小的一系列的手段以及方案。這些步驟,其實從時間上來看是不同的時間線。以下咱們來一一看一下。

1、技術規避:

技術側我們當然要保證我們自身程式碼的嚴謹性。這裡主要提及的還是上述所說的一致性的問題。我們在系統開發的過程中要挖掘系統可能出現問題的點,其中可能包含事務的使用、介面需要做好冪等設計,系統和系統互動過程中需要考慮介面的重試機制等等。當然這些都是咱們研發在實際開發的過程中需要注意的點。

2、對賬發現:

很多時候,資產支付系統上線出問題,並不是直接的日誌異常。這種異常可能還好,容易被發現,因為已經卡流程了。怕就是怕在不知不覺的情況下,看似風平浪靜,其實內部資料已經一團亂麻。資損已經產生了,就像老貓上面遇到的這種情況。這種情況的發生其實主要還是由於沒有做好相關的對賬措施。從而導致了悲劇的發生。其實如果我們能夠做到每日對賬,可能問題就能及時被發現。

對賬方式:我們可以從上層系統一路往下游系統進行對賬。例如,我們有這樣幾個系統,例如,上層電商業務系統,訂單系統,支付核心系統,第三方支付系統。那麼我們對賬的方式就如下。

對賬

咱進行對賬的過程中,我們一般是系統之間進行兩兩單據對賬,對賬維護有兩個方面,第一個是總數量,第一個是總金額。如果我們發現所有的系統能夠兩兩賬目對齊,那麼系統就沒有太大問題。

清洗對賬

當然很多時候單據數量可能由於業務的原因是不對等的,所以這個時候可能還需要進行一定的資料清洗,然後才能進行對賬處理。做得好點的話,可以將對不齊的單子能夠自動告警,告警方式可以是簡訊或者郵件方式,當然也可以支援相關人員能夠看到每日的對賬看板。

這種對賬的方式可以協助我們及時發現系統上的問題。老貓之前對接的那個渠道,其實還沒有來得及做對賬。因為那時候我也疏忽了,認為當前第三方渠道比較冷門,所以就偷個懶沒有做對賬。然而最終還是逃不過“墨菲定律”。所以咱們研發在做系統上的事兒的時候還是不要抱有任何的僥倖心理。

3、應急止損:

如果真的到了這一步,其實悲劇已經發生了,這個時候其實是比較考驗心態的。因為系統漏洞已經造成了資損,並且資損還在持續。這時候內心就會燥熱,像老貓那樣口乾舌燥,著急得像熱鍋上的螞蟻。這個時候如果越想早點修復問題,可能越容易出錯。很多時候人在著急得時候往往會病急亂投醫。為了快速解決問題,修一下bug就直接上線。這種很容易導致錯上加錯。

所以當我們意識到問題已經發生了,就要向上彙報了,讓上級知道這個事情,然後一起看一下問題的處理。這樣的話多個人一起把控修復問題肯定比當事人一個人默默修復問題來得好。所謂“當局者迷旁觀者清”是有道理的,這樣也至少可以降低二次錯誤的機率。所以出現問題後,一定不能慌了手腳。唯一要做的就是冷靜,然後一步步梳理處理的步驟。

當然如果有條件的話,可以根據當前的業務模式開發一個資金追討系統來防範未然,當然這個系統真的希望是一輩子都用不上,然而這個系統可能是最後的一道屏障了。

總結

以上就是老貓的一段經歷,大家可以當做樂子看個熱鬧。如果真的對你有所幫助,也希望能夠得到你的點贊和收藏。當然,如果你也恰好維護同樣的系統,對於這樣的系統維護有其他新的認知,也歡迎大家能夠在評論區留言。

相關文章