真實案例引起的對系統健壯性的思考


大年初四(2012年1月26日)上午,我在重慶移動某營業廳的自助客戶端使用招商銀行信用卡為我妻子充話費(我妻子的手機已經停機)。在插入信用卡並輸入密碼后,系統提示正在交易。大約幾秒后,我的手機收到招行的短信,提示消費100元,但自助客戶端仍然顯示正在交易。此時的我已經有了不詳的預感。果然,在等待大約一分鍾,系統提示操作失敗,之后系統崩潰,彈出了一個Windows命令窗口。因為我妻子的手機停機了,所以立刻可以確認上一次充值確實是失敗的。而我的手機能收到信用卡的消費信息,則可以確認銀行確實已經支付了100元(我之后查詢了我的信用卡賬單,確實存在這一筆消費記錄)。

之后,我又用現金充值話費,此次操作成功了,但卻顯示贈送的3元話費失敗,提示相同Id的贈送記錄沖突。這是因為系統規定了約束,要求當月同一個號碼只能享受一次優惠。但問題是,我之前用信用卡的充值並未成功,也未收到贈送的話費。

我沒有機會能夠看到該自助充值系統的設計與代碼,但以我的開發經驗,可以直觀感受到這是事務出現了問題。這個簡單的充值操作,實際上完成了四個職責:
1)調用第三方的銀行支付服務,付費;
2)獲取優惠策略,並計算優惠金額;
3)保存優惠記錄,便於滿足優惠的約束條件;
4)充值(包括付費金額+優惠金額);

顯然,這四個操作必須放在一個事務范圍內,並遵循ACID原則中的一致性原則。由於在該操作中,至少對第三方銀行支付服務的調用是跨系統跨資源的,因此,事務必須是分布式事務。目前看到的系統問題,顯然是在充值時,系統出現了故障,卻未能將前面的兩個操作回滾,導致執行結果不一致。結果,我悲催了:銀行扣了款,優惠沒落着,費用沒充上。

從直觀表現看,我甚至有理由懷疑,對於充值的整個操作,系統是否使用了事務??因為,倘若將這四個不同的操作作為一個服務放在事務中時,應該不會在系統提示正在交易時,銀行就扣款成功。不過,考慮到對於這樣的業務需求,使用事務基本上已經成為了常識,這個系統的設計者或者實現者應該不會犯如此低級的錯誤,那我只能善良地認為,該系統沒有能夠很好或正確地使用分布式事務。

我不知道,該系統是基於什么平台開發,是使用了.NET的DTC,還是Java的JTA。然而基於分布式事務的基本原理來看,這是一種將對多個資源和服務的訪問放在同一個事務中的情況。此外,系統調用的第三方銀行支付服務必然也是使用了事務的,它會作為整個分布式事務的事務提交樹中的子節點,而支付服務的事務則為根事務,是整個事務的總體協調者。由於訪問的資源並不相同,即使各個操作放在了自己的事務中,也無法保證滿足ACID,因此,這里應該使用兩端式提交(two-phase commit)。

重慶移動在提出需求時,必然首先考慮自身的利益,因而系統充值服務包含的四個操作,其執行順序必然是:首先調用第三方銀行支付服務,如果成功,再獲取優惠策略,並獲得優惠金額;然后充值(從故障表現看,似乎記錄優惠信息的操作卻在充值操作之前)。考慮簡單的情況,假設后三個操作訪問的是同一個資源(主要應該是數據庫)。那么支付服務事務作為根節點,應該協調銀行付費服務事務和充值事務的投票結果,然后再決定是否提交。當所有的參與者表示Prepare,才會提交。而在提交過程中如果出現問題,就必須回滾事務。從故障表現來看,似乎該事務並未采用兩段式提交,因為它沒有協調投票結果的過程(因為我的手機首先收到了銀行的消費信息,優惠記錄也保存了,否則不會出現優惠記錄ID沖突)。此外,故障出現時,系統一直顯示“正在處理……”字樣,並在約1分鍾左右提示故障。這說明系統可能考慮了Timeout值的設置。如果采用了兩段式提交,在各個參與者准備就緒后,如果出現了問題,就應該存在未決(In-Doubt)事務,在規定時間內沒有解決,分布式事務會中止整個事務,並回滾。

所以,我在這里有理由相信該系統即使使用了事務,也沒有很好地用好事務,尤其是分布式事務。在這里,第三方銀行服務是沒有任何問題的,它自身的事務必然是完整的,但此時它作為整個事務的參與者,是事務提交樹的子節點,卻沒有被很好地協調。

當故障出現后,系統在提示“操作失敗”后的表現是崩潰,而不是回到主界面,這也說明了系統連基本的異常處理也可能沒有做好。

這里,事實上還存在一個小插曲。那就是在我詢問了營業廳的營業員后,該營業員打開機器,查詢了日志文件夾下的交易日志,並沒有查詢到我的充值記錄。后來,她才醒悟,說道自助客戶端並不會記錄銀行卡充值的交易信息。這讓我倍感納悶。雖然現金充值和銀行卡充值是兩種不同的充值方式,但從抽象層面來看,它們的行為是完全是一致的,充值方式不過是充值策略的兩種體現罷了。從設計的角度來看,這是一個典型的Template Method模式。因為對於Charge操作來看,除了支付的實現不同之外,其余操作包括充值、獲得優惠策略並計算優惠金額、事務處理、異常處理、資源管理以及日志記錄,都應該是完全相同的,為何不能在系統中統一處理呢?顯然,它應該是定義在應用服務層的一個統一服務接口(它可以是一個抽象類,也可以作為接口,並另外定義一個抽象類實現它,並實現共同的邏輯),並提供Cash和Card的兩個實現類。系統會根據輸入實例化不同的實現類。

而對於系統維護而言,日志本身就是必不可少的信息(也許系統內部有日志可供維護人員查詢,但日志可以是分級的,以便於不同角色根據需要對日志進行查詢)。對於這種涉及到金錢交易的業務,日志記錄更顯得重要,因為它能夠減少很多消費糾紛。這樣的設計真的讓我很不解。不錯,我沒有看到系統的實現(我很有興趣能夠看看這個系統的設計與實現,可惜沒有這個機會),但根據這些故障表現,確實可以分析得到,這樣的系統沒有很好地保障系統的健壯性。這個真實案例,很可以值得我們軟件從業人員深思。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM