天天看點

圖解|訂單系統中的補償事務

閱讀目錄

  • 一、背景
  • 二、“大唐啥都有”網站的代碼
  • 三、SQL 中的事務
  • 四、那如何優化無事務的代碼?
  • 五、如何解決無事務的問題?
  • 六、具有補償功能的解決方案

悟空和師父一行人正在前往西天取經的路上,師父線上上買了一個福袋,訂單狀态顯示訂單已支付,但是電子福袋狀态為未發送。

圖解|訂單系統中的補償事務

悟空來到了這家網站的背景,找到了開發人員“小黑熊”。

悟空:嘿,快查下我師父的訂單,錢都給了,福袋怎麼還沒有到?

小黑熊:大聖,我們也收到異常通知了,更新福袋表的時候因網絡原因導緻福袋記錄沒有更新成功,是以福袋還是未發送的。

悟空:福袋沒發出來,那為什麼訂單狀态還一直是已支付?你這小兒,可不要瞞我!

小黑熊:大聖,我們資料庫用的是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中的事務: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.如果第二步失敗,則更新訂單狀态為失敗,并發起退款

繼續閱讀