一、背景
悟空和師父一行人正在前往西天取經的路上,師父在線上買了一個福袋,訂單狀態顯示訂單已支付,但是電子福袋狀態為未發送。
悟空來到了這家網站的后台,找到了開發人員“小黑熊”。
悟空:嘿,快查下我師父的訂單,錢都給了,福袋怎么還沒有到?
小黑熊:大聖,我們也收到異常通知了,更新福袋表的時候因網絡原因導致福袋記錄沒有更新成功,所以福袋還是未發送的。
悟空:福袋沒發出來,那為什么訂單狀態還一直是已支付?你這小兒,可不要瞞我!
小黑熊:大聖,我們數據庫用的是MongoDB 3.0,不支持事務啊。
悟空:你說的事務是什么意思?
小黑熊:事務就是保持多個更新或刪除或增加操作,要么都成功,要么都失敗。
悟空:也就是說第一步頂單狀態從未支付到訂單成功已經執行成功了,但是第二步更新福袋的時候失敗了,沒有自動將第一步訂單的狀態給改回去?
小黑熊:是的,大聖。
悟空:那你們怎么沒有退款啊?
小黑熊:大聖,我們也沒有想到有這種異常發生。
悟空:容我看下你們的代碼。
二、“大唐啥都有”網站的代碼
該網站購物的內部邏輯簡化后如下圖所示:
try { order.status = "已支付"; //第一步,更新訂單狀態:訂單已支付 order.save(); //保存訂單 luckyBag.status = "已發送"; // 第二步,更新福袋狀態:福袋已發送 luckyBag.save(); //保存福袋 goodCounts.count -= 1;// 第三步,更新庫存 goodCounts.save(); // 保存庫存 order.status="訂單成功" // 第四步,更新訂單狀態:訂單成功 order.save(); //保存訂單 } catch (excption e) { logError(); }
那這樣的代碼會有什么問題呢?
如果第一步執行成功,第二步執行失敗了,拋出了異常,則第一步訂單狀態還是支付成功的,福袋狀態未更新,也就是師父遇到的問題。
那如何保證兩步操作的一致性呢?(要么都更新,要么都不更新。)
我們都知道SQL中是有事務這種解決方案的,我們先來看看SQL中的事務。
三、SQL 中的事務
之前寫過一篇文章,專門來講SQL中的事務:30分鍾全面解析-SQL事務+隔離級別+阻塞+死鎖。在這里用偽代碼來說明下什么事務。
舉個購買商品的例子:用戶下了一筆單,付款了,然后發放福袋,涉及到訂單表order更新,福袋表luckyBag更新。
start transaction // 開始事務 try { update order // 第一步,更新訂單狀態 update luckyBag // 第二步,更新福袋狀態 commit // 提交兩部操作的更改 } catch (excption e) { rollback // 回滾所有操作 } end transaction // 結束事務
更新訂單狀態和更新福袋狀態兩部操作成功,則全部提交到數據庫執行,如果其中任意一步出現問題,則全部回滾,就像沒有執行更新操作一樣,以保證數據的一致性。
四、那如何優化無事務的代碼?
由於MongoDB 3.0 不支持事務,所以很有可能出現數據不一致的情況(訂單已支付,福袋未發送)。
那我們既然不能享受到事務的一致性,有什么辦法來優化這部分代碼呢?
我們先看下代碼的時序圖:
從上面的順序圖來看,分步保存是有問題的,第一步保存成功后,第二步如果保存失敗,則數據不一致。那我們可以將保存往后移嗎?
我們來看下優化后的時序圖,整體將保存往后移。
偽代碼如下:
try { order.status = "已支付"; //第一步,更新訂單狀態:訂單已支付 luckyBag.status = "已發送"; // 第二步,更新福袋狀態:福袋已發送 goodCounts.count -= 1;// 第三步,更新庫存 order.status="訂單成功" //第一步,更新訂單狀態:訂單已支付 luckyBag.save(); //保存福袋記錄 goodCounts.save(); // 保存庫存記錄 order.save(); //保存訂單記錄 } catch (excption e) { logError(); }
那這種方式又有什么優缺點呢?
優點:前四步的業務邏輯處理任意一步如果出錯了,並不會影響數據庫的記錄
缺點:后三步的保存如果出錯了,和最開始的方案一樣,存在數據不一致的問題。
那如何進行解決這種問題?
五、如何解決無事務的問題?
優化后的代碼還是可能存在數據不一致的情況,那我們怎么來解決?
問題1.如果福袋沒有自動發出去,現在還可以補發嗎?怎么補發?
問題2.可以退款嗎?手動退款還是自動退款?分別有什么優點和缺點?怎么優化?
問題3.如果第三步更新庫存失敗,那又該怎么做呢?
問題4.如何退款失敗,那又該怎么做呢?
圍繞上面幾個問題,我們展開來論述。
問題1.1:對於補發問題,我們怎么來補發呢?
方案1:第二步失敗時,立即重試幾次(第一次3s,第二次間隔8s,第三次間隔20s,為什么間隔時間不一樣?可以留言哦^_^)
方案2:將失敗的數據放到隊列里面(可以是存到數據庫或者redis里面,建議存放到數據庫),定時從隊列里面獲取異常數據,進行重新發送。
問題1.2:自動補發的優點和缺點分別是什么呢?
方案1的優點和缺點
優點:
(1)如果是臨時出現的網絡問題,可以立即在短時間內重試幾次,可以解決問題。
缺點:
(1)如果是接口或數據問題,短時間內重試再多次也是會失敗的;
(2)另外如果有大量失敗,重試也是會占用系統資源的。
方案2的優點和缺點
優點:
(1)將重試放到異步任務中來做,可以減少系統資源的占用;
(2)如果是長時間出現的網絡問題,等網絡恢復后,一定會重試成功;
缺點:
(1)異常數據無法通過重試來解決,則隊列里面的數據將一直會進行重試,無法終止;
(2)如果有大量數據因接口或代碼問題導致失敗,則會積累大量失敗數據,而大量數據進行重試也會對系統資源造成一定壓力;
(3)重試失敗會進行error log的記錄,大量的error log對線上排查問題會造成干擾。
那補發如果一直失敗,是不是還有更好的方式?給用戶退款是不是更合理?(顧客等得很着急,趕緊把錢先退了吧。)這其實就是一種補償措施。
問題2.1 可以退款嗎?
當然可以退款
問題2.2 自動退款的優缺點?
優點:減少運營人員的工作量
缺點:在某些情況下,異常訂單需要多方排查核實才能退款,就不能走自動退款。比如代碼的邏輯沒有handle某些場景,一刀切的退款會導致錢退了,商品還發給了客戶。
問題2.3 怎么優化?
那怎么優化? 提供自動和手動的兩種方式,當某些異常場景需要手動退款的,等開發人員核實后,再進行手動退款。
賬不平怎么處理?通過對賬的方式找出哪些賬不平。
問題3 第三步更新庫存失敗怎么處理?
我們很容易想到的方案是及時retry或 隊列retry。那有什么問題呢?對於秒殺活動,隊列retry肯定不可行。
那我們可以做一次補償操作嗎?(發起退款,更新訂單狀態為失敗。)
答案是可以的。
問題4 如果退款失敗怎么處理
每一步失敗我們都會做補償處理,但是中間某一步補償失敗,我們該怎么處理?比如最后錢退不了。
常見方案:
1.退款失敗后主動報警通知運維人員或開發人員
2.手動退款(缺點:人工操作,容易出錯,比如找訂單找錯了)
或 3.加入隊列,自動退款(缺點:一般退款失敗都是代碼級別問題或微信側問題,所以還是需要排查問題原因,在這期間,所有退款失敗異常都會報警,對日常的監控造成不必要的干擾)
在我現在做的項目都會將退款失敗的消息以下面兩種形式推送給我:
1.微信的模板消息
2.雲服務商提供的日志報警短信服務
這樣方便我去排查問題,以及快速退款。
六、具有補償功能的解決方案
我們可以設計一個具有補償功能的解決方案:
1.如果第一步失敗,則發起退款
2.如果第二步失敗,則更新訂單狀態為失敗,並發起退款
3.如果第三步更新庫存失敗,則退回福袋,且更新訂單狀態為失敗,並發起退款
4.如果第四步更新訂單為成功時失敗,則庫存+1,退回福袋,更新訂單狀態失敗,並發起退款
七、還有哪些不足?
歡迎大家留言討論自家系統是怎么做的?
作 者: Jackson0714
出 處:http://www.cnblogs.com/jackson0714/
關於作者:專注於微軟平台的項目開發。如有問題或建議,請多多賜教!
版權聲明:本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接。
特此聲明:所有評論和私信都會在第一時間回復。也歡迎園子的大大們指正錯誤,共同進步。或者直接私信我
聲援博主:如果您覺得文章對您有幫助,可以點擊文章右下角【推薦】一下。您的鼓勵是作者堅持原創和持續寫作的最大動力!
公眾號:悟空聊架構

