小小邏輯判斷符的錯誤使用,資損幾萬塊
分享是最有效的學習方式。
博客:https://blog.ktdaddy.com/
故事
這是一個真實事件,三年前老貓負責公司的支付資產業(yè)務。為了響應上級號召,加強國央企之間的合作,公司新談了一個支付對接的渠道(當然這個支付渠道其實很冷門的,也是為了對接而對接,具體哪個渠道也不方便透露),由于原始支付系統的第三方支付可拓展性設計得還不錯的,所以老貓對接的也是比較快的,熟悉對方的對接文檔之后對著編碼就好了,差不多花了三天的時間就完成聯調了。一切看似很順利地上線了。
時隔幾天,收到了一個快遞包裹,是一袋價值53塊錢的“原皮腰果”,當時詫異,翻看了各大消費平臺,都沒有之前的下單記錄,后來和媳婦確認了一下,她也沒有下單。“難道是某個崇拜哥的小姑娘送的?不能吧”當時心里美滋滋地yy著。
不過之后的一個客訴問題,引起了老貓的重視,老貓排查下來發(fā)現一個很重大的問題,錢款的扣除和實際的訂單狀態(tài)對不上。說白了就是訂單完結了,但是賬戶資產并沒有完成扣除。我瞬間明白了之前那個“原皮腰果”是怎么回事兒了,當時在生產測試渠道的時候,在公司內部商城提交了訂單,但是并沒有付款,然而訂單卻成功了。
再三確認之后,確實存在這一問題。一瞬間整個人心態(tài)崩了,頭皮發(fā)麻,口干舌燥,心臟“突突突”。怎么辦?怎么辦?生產還不知道涉及多少單子,沒辦法,兜不住了,先把這件事情往上拋吧(向上級領導匯報)。
具體原因是什么呢?我們來看一下對接第三方支付的大概時序流程。

我們一般在對接第三方支付渠道的時候會有上面一些基本流程。
1、當我們內部生成待支付單之后會請求外部第三方支付渠道,此時第三方支付渠道內部會生成待支付單。
2、我們第三方支付單創(chuàng)建成功返回之后,一般內部系統會喚起收銀臺,然后用戶確認支付。
3、第三方支付成功返回消息之后,整個支付就算已經完結了。
但是問題就出在了第三方支付渠道還有一個定時異步通知的任務,并且我們也對接了這個接口。這個異步通知的功能主要是會定時告知我們支付系統第三方支付單的狀態(tài)。而且無論成功與否,都會輪詢告知,例如,如果是待支付,對方會告知狀態(tài)是0,如果已完成支付,對方會告知狀態(tài)是1。我們拿到支付結果之后就會執(zhí)行后續(xù)的訂單完成流程。
收到異步通知之后的代碼處理判斷如下:
事故代碼
//校驗交易狀態(tài)
if (!Objects.equals(notifyModel.getTradeStatus(), TradeStatus.SUCCESS.getCode())
&& amtConvertY(notifyModel.getTradeAmt()).compareTo(thirdPartyCharge.getAmount()) != 0) {
LOGGER.error("交易狀態(tài)不正確:{}", notifyModel.getOutTradeNo());
throw new BusinessRuntimeException(Errors.PAY_NOTIFY_IS_FAIL);
}
正確代碼
//校驗交易狀態(tài)
if (!Objects.equals(notifyModel.getTradeStatus(), TradeStatus.SUCCESS.getCode())
|| amtConvertY(notifyModel.getTradeAmt()).compareTo(thirdPartyCharge.getAmount()) != 0) {
LOGGER.error("交易狀態(tài)不正確:{}", notifyModel.getOutTradeNo());
throw new BusinessRuntimeException(Errors.PAY_NOTIFY_IS_FAIL);
}
相信眼尖的小伙伴已經發(fā)現了,其實就是“||”和“&”的區(qū)別。第一種情況當對方告知狀態(tài)為0的時候,其實并不會被攔截掉,而是直接走了往后的流程,于是悲劇就發(fā)生了。
直接說一下最終的處理,最終其實還是比較幸運的。由于,我們本身已經對接了微信以及支付寶的支付渠道,再加上這個渠道的支付使用的頻率還是非常少的,很多用戶不太會使用這個渠道進行支付,所以最終盤算下來整個的資損金額差不多是3w左右,另外的是其中有個不幸中的萬幸。是因為老貓在這之前做了一套資金追討系統,該系統可以定位出那些用戶“空手套白狼”了。并且能給這些用戶生成對應的待支付訂單,用戶可以通過這些待支付訂單最終完成資金的補償付款,最終完成了資金了追討。所以事后,完全盤點之后發(fā)現一共的資損是1600左右。
最終,也算是有驚無險。但是這次的經歷給了老貓上了一課。
下面總結一下我們在做支付賬務系統的過程中應該如何進行資金安全相關的設計,最終做到防患于未然。

資金安全設計
針對資金安全的問題,不限于通過技術手段避免資損,其實很多時候我們還需要結合數據核對、監(jiān)控等措施,做到快速發(fā)現資損并且止損。下面我們來一一盤點一下資金安全設計的一些點,希望能給大家一些幫助。
資損風險分析
風險要素
在我們做支付資產系統的時候,我們其實需要好好盤點一下資損風險,這些風險可能來自于各個方面。下面涉及,
資金流:我們實際的產品業(yè)務中,尤其是支付資產的時候,其實往往會有很多類型的資產形態(tài),可能是積分,可能是現金,當然還有可能是優(yōu)惠券等等。看到這些金額的時候,我們需要確保上下游系統的一致性、金額計算的正確性、逆向金額不能大于正向金額等等。
交互:我們需要考慮客戶端展示內容是否正確。尤其是小數點展示的精確位上,很容易出現客戶端展現信息不全的問題,實際其實為99.99元,但是展示的時候卻為99.9或者9.99。
資金規(guī)則:資金規(guī)則是為資產本身的產品形態(tài)規(guī)則,舉個例子,發(fā)放優(yōu)惠券這個行為,每個人發(fā)多少,發(fā)的時間點,發(fā)放的人數,發(fā)放的門檻等等。再比如某個積分資產的限額,其中又涵蓋著日限額以及月限額。
異常:存在資損風險很多時候其實由于異常導致的,拋開系統本身異常之外,我們還要考慮網絡抖動異常,其他服務異常。如果系統本身的事務處理不好,或者最終一致性沒有做好,就很有可能造成資損。
技術風險
系統發(fā)生資損么,很大一部分就是系統沒有設計好或者是編碼過程中的粗心,例如上面老貓的真實案例。那么且拋開粗心這個人為因素,我們盤點一下本身技術風險,這些技術風險場景主要來源于多并發(fā)、冪等、分布式事務、上下游服務超時、數據計算精度、接口協議、校驗邏輯的不嚴謹等等。
上面羅列的這些很多都為一致性的問題。我們一個個來看。
1、并發(fā):多線程、同時對數據進行讀寫處理的時候,就有可能造成一致性的問題,例如用戶資產重復支付,積分超發(fā)等等,如果在系統層面還用了緩存的話,還有可能存在緩存未刷新,導致數據庫和緩存不一致的情況。
2、冪等:在支付資產系統中,接口冪等性是十分必要的,接口的冪等能夠很大程度上避免(1)中提到的資產重復支付問題、下單重復問題以及網絡重發(fā)問題。關于冪等詳細設計,其實老貓在以前的文章中也有梳理,大家有興趣的可以看這里【前任開發(fā)在代碼里下毒,支付下單居然沒加冪等】
3、服務超時:系統所依賴的服務執(zhí)行結果返回慢,造成上下游數據狀態(tài)不一致,例如核心的支付服務調用底層的資產服務進行扣款,結果由于資產扣款邏輯返回超時,導致兩邊數據不一致。
4、接口規(guī)范:尤其是對接第三方接口的時候,文檔上的必填字段和非必填字段如果本身文檔就有出入,可能就是致命的。
5、事務:其中包含本地事務以及分布式事務,研發(fā)在開發(fā)過程中對事務理解不夠透徹,使用不嚴謹,最終導致數據不一致。
6、數據精度:主要在金額四舍五入的場景,最終導致精度丟失。或者上下游系統精度不一致。
防止資損
如果說想要徹底的避免資損,并不是一件容易的事情,系統鏈路復雜,任何一個環(huán)節(jié)出現問題都有可能導致最終的資損。我們雖然是技術,但是我們的眼光其實不能夠僅僅局限在技術的眼光去看待資損這個事情,除了技術側盡量規(guī)避資損發(fā)生之外,其實還有其他方案,例如下圖。

上圖準確來說其實是一個防止資損,或者說盡量保證企業(yè)損失最小的一系列的手段以及方案。這些步驟,其實從時間上來看是不同的時間線。以下咱們來一一看一下。
1、技術規(guī)避:
技術側我們當然要保證我們自身代碼的嚴謹性。這里主要提及的還是上述所說的一致性的問題。我們在系統開發(fā)的過程中要挖掘系統可能出現問題的點,其中可能包含事務的使用、接口需要做好冪等設計,系統和系統交互過程中需要考慮接口的重試機制等等。當然這些都是咱們研發(fā)在實際開發(fā)的過程中需要注意的點。
2、對賬發(fā)現:
很多時候,資產支付系統上線出問題,并不是直接的日志異常。這種異常可能還好,容易被發(fā)現,因為已經卡流程了。怕就是怕在不知不覺的情況下,看似風平浪靜,其實內部數據已經一團亂麻。資損已經產生了,就像老貓上面遇到的這種情況。這種情況的發(fā)生其實主要還是由于沒有做好相關的對賬措施。從而導致了悲劇的發(fā)生。其實如果我們能夠做到每日對賬,可能問題就能及時被發(fā)現。
對賬方式:我們可以從上層系統一路往下游系統進行對賬。例如,我們有這樣幾個系統,例如,上層電商業(yè)務系統,訂單系統,支付核心系統,第三方支付系統。那么我們對賬的方式就如下。

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

當然很多時候單據數量可能由于業(yè)務的原因是不對等的,所以這個時候可能還需要進行一定的數據清洗,然后才能進行對賬處理。做得好點的話,可以將對不齊的單子能夠自動告警,告警方式可以是短信或者郵件方式,當然也可以支持相關人員能夠看到每日的對賬看板。
這種對賬的方式可以協助我們及時發(fā)現系統上的問題。老貓之前對接的那個渠道,其實還沒有來得及做對賬。因為那時候我也疏忽了,認為當前第三方渠道比較冷門,所以就偷個懶沒有做對賬。然而最終還是逃不過“墨菲定律”。所以咱們研發(fā)在做系統上的事兒的時候還是不要抱有任何的僥幸心理。
3、應急止損:
如果真的到了這一步,其實悲劇已經發(fā)生了,這個時候其實是比較考驗心態(tài)的。因為系統漏洞已經造成了資損,并且資損還在持續(xù)。這時候內心就會燥熱,像老貓那樣口干舌燥,著急得像熱鍋上的螞蟻。這個時候如果越想早點修復問題,可能越容易出錯。很多時候人在著急得時候往往會病急亂投醫(yī)。為了快速解決問題,修一下bug就直接上線。這種很容易導致錯上加錯。
所以當我們意識到問題已經發(fā)生了,就要向上匯報了,讓上級知道這個事情,然后一起看一下問題的處理。這樣的話多個人一起把控修復問題肯定比當事人一個人默默修復問題來得好。所謂“當局者迷旁觀者清”是有道理的,這樣也至少可以降低二次錯誤的概率。所以出現問題后,一定不能慌了手腳。唯一要做的就是冷靜,然后一步步梳理處理的步驟。
當然如果有條件的話,可以根據當前的業(yè)務模式開發(fā)一個資金追討系統來防范未然,當然這個系統真的希望是一輩子都用不上,然而這個系統可能是最后的一道屏障了。
總結
以上就是老貓的一段經歷,大家可以當做樂子看個熱鬧。如果真的對你有所幫助,也希望能夠得到你的點贊和收藏。當然,如果你也恰好維護同樣的系統,對于這樣的系統維護有其他新的認知,也歡迎大家能夠在評論區(qū)留言。

浙公網安備 33010602011771號